feat: Friend notes backup/restore

This commit is contained in:
ΞTΞRNAL
2025-10-07 22:01:45 +05:30
parent 3daf5ea89b
commit 0303947f84
7 changed files with 128 additions and 7 deletions

View File

@@ -197,6 +197,14 @@ class BridgeService : Service() {
remoteSideContext.database.setScopeNotes(id, content)
}
override fun getAllScopeNotes(): Map<String, String> {
return remoteSideContext.database.getAllScopeNotes()
}
override fun setAllScopeNotes(notes: Map<String, String>) {
remoteSideContext.database.setAllScopeNotes(notes)
}
override fun getScriptingInterface() = remoteSideContext.scriptManager
override fun getE2eeInterface() = remoteSideContext.e2eeImplementation

View File

@@ -30,3 +30,27 @@ fun AppDatabase.deleteScopeNotes(id: String) {
database.execSQL("DELETE FROM notes WHERE id = ?", arrayOf(id))
}
}
fun AppDatabase.getAllScopeNotes(): Map<String, String> {
return database.rawQuery("SELECT id, content FROM notes", null).use {
val map = mutableMapOf<String, String>()
while (it.moveToNext()) {
map[it.getString(0)] = it.getString(1)
}
map
}
}
fun AppDatabase.setAllScopeNotes(notes: Map<String, String>) {
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()
}
}
}

View File

@@ -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<Map<String, String>>(json, object : com.google.gson.reflect.TypeToken<Map<String, String>>() {}.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))
}
}
}
}

View File

@@ -75,6 +75,10 @@ interface BridgeInterface {
oneway void setScopeNotes(String id, String content);
Map<String, String> getAllScopeNotes();
oneway void setAllScopeNotes(in Map<String, String> notes);
IScripting getScriptingInterface();
E2eeInterface getE2eeInterface();

View File

@@ -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",

View File

@@ -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) {

View File

@@ -255,6 +255,10 @@ class BridgeClient(
fun setScopeNotes(id: String, content: String?) = safeServiceCall { service.setScopeNotes(id, content) }
fun getAllScopeNotes(): Map<String, String> = safeServiceCall { service.getAllScopeNotes() }
fun setAllScopeNotes(notes: Map<String, String>) = safeServiceCall { service.setAllScopeNotes(notes) }
fun getScriptingInterface(): IScripting = safeServiceCall { service.scriptingInterface }
fun getE2eeInterface(): E2eeInterface = safeServiceCall { service.e2eeInterface }