diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Options.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Options.kt
new file mode 100644
index 00000000..e9badec4
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Options.kt
@@ -0,0 +1,58 @@
+package dev.msfjarvis.lobsters.ui.settings
+
+import android.content.Context
+import androidx.activity.compose.registerForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.Composable
+import dev.msfjarvis.lobsters.data.backup.BackupHandler
+import dev.msfjarvis.lobsters.utils.Strings
+import dev.msfjarvis.lobsters.utils.get
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+private const val JSON_MINE = "application/json"
+
+@Composable
+fun BackupOption(
+ context: Context,
+ backupHandler: BackupHandler,
+ coroutineScope: CoroutineScope,
+) {
+ val result =
+ registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri ->
+ if (uri == null) return@registerForActivityResult
+ context.contentResolver.openOutputStream(uri)?.use {
+ coroutineScope.launch(Dispatchers.IO) {
+ it.write(backupHandler.exportSavedPosts().toByteArray(Charsets.UTF_8))
+ }
+ }
+ }
+ SettingsActionItem(
+ Strings.SettingsBackup.get(),
+ Strings.SettingsBackupDescription.get(),
+ onClick = { result.launch("Claw-export.json") }
+ )
+}
+
+@Composable
+fun RestoreOption(
+ context: Context,
+ backupHandler: BackupHandler,
+ coroutineScope: CoroutineScope,
+) {
+ val result =
+ registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
+ if (uri == null) return@registerForActivityResult
+ context.contentResolver.openInputStream(uri)?.use {
+ coroutineScope.launch(Dispatchers.IO) {
+ backupHandler.importSavedPosts(it.readBytes().toString(Charsets.UTF_8))
+ }
+ }
+ }
+ SettingsActionItem(
+ title = Strings.SettingsRestore.get(),
+ description = Strings.SettingsRestoreDescription.get(),
+ onClick = { result.launch(JSON_MINE) }
+ )
+}
diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Settings.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Settings.kt
new file mode 100644
index 00000000..9a43d2c2
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/settings/Settings.kt
@@ -0,0 +1,95 @@
+package dev.msfjarvis.lobsters.ui.settings
+
+import android.content.Context
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.Icon
+import androidx.compose.material.ListItem
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import dev.msfjarvis.lobsters.data.backup.BackupHandler
+import dev.msfjarvis.lobsters.utils.Strings
+import dev.msfjarvis.lobsters.utils.get
+import kotlinx.coroutines.CoroutineScope
+
+@Composable
+fun LobstersSettings(
+ backupHandler: BackupHandler,
+) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ Scaffold(
+ topBar = { SettingsTopBar(context) },
+ content = { SettingsBody(context, backupHandler, scope) },
+ )
+}
+
+@Composable
+fun SettingsTopBar(
+ context: Context,
+) {
+ TopAppBar(
+ title = { Text(Strings.Settings.get()) },
+ navigationIcon = {
+ Icon(
+ Icons.Default.ArrowBack,
+ contentDescription = Strings.Settings.get(),
+ modifier =
+ Modifier.padding(start = 16.dp).clickable { (context as ComponentActivity).finish() },
+ )
+ },
+ )
+}
+
+@Composable
+fun SettingsBody(
+ context: Context,
+ backupHandler: BackupHandler,
+ scope: CoroutineScope,
+) {
+ LazyColumn {
+ item {
+ BackupOption(
+ context,
+ backupHandler,
+ scope,
+ )
+ }
+ item {
+ RestoreOption(
+ context,
+ backupHandler,
+ scope,
+ )
+ }
+ }
+}
+
+@Composable
+fun SettingsActionItem(
+ title: String,
+ description: String? = null,
+ singleLineDescription: Boolean = true,
+ icon: ImageVector? = null,
+ onClick: (() -> Unit)? = null,
+) {
+ ListItem(
+ text = { Text(title) },
+ secondaryText = { description?.let { Text(it) } },
+ icon = { icon?.let { Icon(icon, null, Modifier.height(32.dp)) } },
+ singleLineSecondaryText = singleLineDescription,
+ modifier = Modifier.clickable { onClick?.invoke() },
+ )
+}
diff --git a/common/src/androidMain/kotlin/dev/msfjarvis/lobsters/utils/StringValue.kt b/common/src/androidMain/kotlin/dev/msfjarvis/lobsters/utils/StringValue.kt
index 8af20951..c561b196 100644
--- a/common/src/androidMain/kotlin/dev/msfjarvis/lobsters/utils/StringValue.kt
+++ b/common/src/androidMain/kotlin/dev/msfjarvis/lobsters/utils/StringValue.kt
@@ -11,13 +11,18 @@ private fun stringEnumMapper(stringEnum: Strings): Int {
Strings.AvatarContentDescription -> R.string.avatar_content_description
Strings.ChangeSortingOrder -> R.string.change_sorting_order
Strings.HottestPosts -> R.string.hottest_posts
+ Strings.NewestPosts -> R.string.newest_posts
Strings.NoSavedPost -> R.string.no_saved_posts
Strings.OpenComments -> R.string.open_comments
Strings.RefreshPostsContentDescription -> R.string.refresh_posts_content_description
Strings.RemoveFromSavedPosts -> R.string.remove_from_saved_posts
Strings.SavedPosts -> R.string.saved_posts
Strings.SubmittedBy -> R.string.submitted_by
- Strings.NewestPosts -> R.string.newest_posts
+ Strings.Settings -> R.string.settings
+ Strings.SettingsBackup -> R.string.settings_backup
+ Strings.SettingsBackupDescription -> R.string.settings_backup_desc
+ Strings.SettingsRestore -> R.string.settings_restore
+ Strings.SettingsRestoreDescription -> R.string.settings_restore_desc
}
}
diff --git a/common/src/androidMain/res/values/strings.xml b/common/src/androidMain/res/values/strings.xml
index 0de8ea60..74e570c2 100644
--- a/common/src/androidMain/res/values/strings.xml
+++ b/common/src/androidMain/res/values/strings.xml
@@ -11,4 +11,9 @@
Open comments
Change sort order
Newest
+ Settings
+ Backup saved posts
+ Export saved posts in a JSON file that can be restored later
+ Restore saved posts
+ Import a previously exported copy of saved posts. Existing saved posts are not cleared
diff --git a/common/src/commonMain/kotlin/dev/msfjarvis/lobsters/utils/Strings.kt b/common/src/commonMain/kotlin/dev/msfjarvis/lobsters/utils/Strings.kt
index 13d946a9..cff1c835 100644
--- a/common/src/commonMain/kotlin/dev/msfjarvis/lobsters/utils/Strings.kt
+++ b/common/src/commonMain/kotlin/dev/msfjarvis/lobsters/utils/Strings.kt
@@ -13,4 +13,9 @@ enum class Strings {
SavedPosts,
SubmittedBy,
NewestPosts,
+ Settings,
+ SettingsBackup,
+ SettingsBackupDescription,
+ SettingsRestore,
+ SettingsRestoreDescription,
}
diff --git a/common/src/jvmMain/kotlin/dev/msfjarvis/lobsters/utils/StringValue.kt b/common/src/jvmMain/kotlin/dev/msfjarvis/lobsters/utils/StringValue.kt
index 27f54788..c2a10b61 100644
--- a/common/src/jvmMain/kotlin/dev/msfjarvis/lobsters/utils/StringValue.kt
+++ b/common/src/jvmMain/kotlin/dev/msfjarvis/lobsters/utils/StringValue.kt
@@ -16,6 +16,13 @@ private fun stringEnumMapper(stringEnum: Strings): String {
Strings.SavedPosts -> "Saved"
Strings.SubmittedBy -> "submitted by %1s"
Strings.NewestPosts -> "Newest"
+ Strings.Settings -> "Settings"
+ Strings.SettingsBackup -> "Backup saved posts"
+ Strings.SettingsBackupDescription ->
+ "Export saved posts in a JSON file that can be restored later"
+ Strings.SettingsRestore -> "Restore saved posts"
+ Strings.SettingsRestoreDescription ->
+ "Import a previously exported copy of saved posts. Existing saved posts are not cleared"
}
}