diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt index 0076139c..3466c890 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -197,6 +197,14 @@ class BridgeService : Service() { remoteSideContext.database.setScopeNotes(id, content) } + override fun getAllScopeNotes(): Map { + return remoteSideContext.database.getAllScopeNotes() + } + + override fun setAllScopeNotes(notes: Map) { + remoteSideContext.database.setAllScopeNotes(notes) + } + override fun getScriptingInterface() = remoteSideContext.scriptManager override fun getE2eeInterface() = remoteSideContext.e2eeImplementation diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/storage/ScopeNotes.kt b/app/src/main/kotlin/me/rhunk/snapenhance/storage/ScopeNotes.kt index 2f4f7957..93d87b62 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/storage/ScopeNotes.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/storage/ScopeNotes.kt @@ -30,3 +30,27 @@ fun AppDatabase.deleteScopeNotes(id: String) { database.execSQL("DELETE FROM notes WHERE id = ?", arrayOf(id)) } } + +fun AppDatabase.getAllScopeNotes(): Map { + return database.rawQuery("SELECT id, content FROM notes", null).use { + val map = mutableMapOf() + while (it.moveToNext()) { + map[it.getString(0)] = it.getString(1) + } + map + } +} + +fun AppDatabase.setAllScopeNotes(notes: Map) { + executeAsync { + database.beginTransaction() + try { + for ((id, content) in notes) { + database.execSQL("INSERT OR REPLACE INTO notes (id, content) VALUES (?, ?)", arrayOf(id, content)) + } + database.setTransactionSuccessful() + } finally { + database.endTransaction() + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt index a7cd5dd0..85d3bf13 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/home/HomeSettings.kt @@ -24,6 +24,11 @@ import androidx.compose.ui.window.Dialog import androidx.core.content.edit import androidx.core.net.toUri import androidx.navigation.NavBackStackEntry +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager import kotlinx.coroutines.launch import me.rhunk.snapenhance.common.action.EnumAction import me.rhunk.snapenhance.common.bridge.InternalFileHandleType @@ -31,17 +36,15 @@ import me.rhunk.snapenhance.common.ui.ThemeChooserDialog import me.rhunk.snapenhance.common.ui.ThemeMode import me.rhunk.snapenhance.common.ui.ThemePreferences import me.rhunk.snapenhance.common.ui.rememberAsyncMutableState +import me.rhunk.snapenhance.storage.getAllScopeNotes +import me.rhunk.snapenhance.storage.setAllScopeNotes +import me.rhunk.snapenhance.task.UpdateCheckWorker import me.rhunk.snapenhance.ui.manager.Routes import me.rhunk.snapenhance.ui.setup.Requirements import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper import me.rhunk.snapenhance.ui.util.AlertDialogs +import me.rhunk.snapenhance.ui.util.openFile import me.rhunk.snapenhance.ui.util.saveFile -import androidx.work.WorkManager -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.Constraints -import androidx.work.NetworkType -import me.rhunk.snapenhance.task.UpdateCheckWorker import java.util.concurrent.TimeUnit class HomeSettings : Routes.Route() { @@ -426,6 +429,67 @@ class HomeSettings : Routes.Route() { } } } + + RowTitle(title = translation["friend_notes_title"]) + ShiftedRow { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(5.dp) + ) { + Text( + text = translation["friend_notes_description"], + modifier = Modifier.weight(1f) + ) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Button(onClick = { + runCatching { + val notes = context.database.getAllScopeNotes() + if (notes.isEmpty()) { + context.shortToast(translation["friend_notes_no_notes_to_backup"]) + return@runCatching + } + val json = context.gson.toJson(notes) + activityLauncherHelper.saveFile("friend_notes_backup.json", "application/json") { uri -> + context.androidContext.contentResolver.openOutputStream(uri.toUri())?.use { + it.write(json.toByteArray()) + } + context.shortToast(translation["friend_notes_backup_success"]) + } + }.onFailure { + context.log.error("Failed to backup notes", it) + context.longToast(translation.format("friend_notes_backup_failure", "error" to (it.localizedMessage ?: ""))) + } + }) { + Text(text = translation["backup_button"]) + } + Button(onClick = { + runCatching { + activityLauncherHelper.openFile("application/json") { uri -> + context.androidContext.contentResolver.openInputStream(uri.toUri())?.use { + val json = it.reader().readText() + val notes = context.gson.fromJson>(json, object : com.google.gson.reflect.TypeToken>() {}.type) + context.database.setAllScopeNotes(notes) + context.shortToast(translation["friend_notes_restore_success"]) + } + } + }.onFailure { + context.log.error("Failed to restore notes", it) + context.longToast(translation.format("friend_notes_restore_failure", "error" to (it.localizedMessage ?: ""))) + } + }) { + Text(text = translation["restore_button"]) + } + } + } + } + } + RowTitle(title = translation["debug_title"]) Row( horizontalArrangement = Arrangement.SpaceBetween, @@ -489,4 +553,4 @@ class HomeSettings : Routes.Route() { Spacer(modifier = Modifier.height(routes.bottomPadding)) } } -} +} \ No newline at end of file diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl index 18573491..8ed330f8 100644 --- a/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl +++ b/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl @@ -75,6 +75,10 @@ interface BridgeInterface { oneway void setScopeNotes(String id, String content); + Map getAllScopeNotes(); + + oneway void setAllScopeNotes(in Map notes); + IScripting getScriptingInterface(); E2eeInterface getE2eeInterface(); diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index e9cd823a..91bec032 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -71,7 +71,16 @@ "message_logger_summary": "{messageCount} messages\n{storyCount} stories", "export_button": "Export", "clear_button": "Clear", + "backup_button": "Backup", + "restore_button": "Restore", "view_logger_history_button": "View Logger History", + "friend_notes_title": "Friend Notes", + "friend_notes_description": "Backup or restore your friend notes.", + "friend_notes_backup_success": "Backup successful", + "friend_notes_backup_failure": "Failed to backup notes! {error}", + "friend_notes_restore_success": "Restore successful", + "friend_notes_restore_failure": "Failed to restore notes! {error}", + "friend_notes_no_notes_to_backup": "No notes to backup", "updates_title": "Updates", "auto_update_check": "Auto check for updates", "update_check_frequency": "Update check frequency", diff --git a/composer/build.gradle.kts b/composer/build.gradle.kts index 665816bc..66e0ac05 100644 --- a/composer/build.gradle.kts +++ b/composer/build.gradle.kts @@ -20,6 +20,14 @@ tasks.register("installTypeScript", org.gradle.api.tasks.Exec::class) { } else { commandLine("npm", "install", "typescript") } + doLast { + if (!Os.isFamily(Os.FAMILY_WINDOWS)) { + val tscScript = project.file("node_modules/.bin/tsc") + if (tscScript.exists()) { + tscScript.setExecutable(true) + } + } + } } tasks.register("compileTsc", org.gradle.api.tasks.Exec::class) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt index 23999aa9..7ac442d2 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt @@ -255,6 +255,10 @@ class BridgeClient( fun setScopeNotes(id: String, content: String?) = safeServiceCall { service.setScopeNotes(id, content) } + fun getAllScopeNotes(): Map = safeServiceCall { service.getAllScopeNotes() } + + fun setAllScopeNotes(notes: Map) = safeServiceCall { service.setAllScopeNotes(notes) } + fun getScriptingInterface(): IScripting = safeServiceCall { service.scriptingInterface } fun getE2eeInterface(): E2eeInterface = safeServiceCall { service.e2eeInterface }