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" } }