From 3738d94789ce2b74d3132e8cd135a178fa40018d Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Sun, 4 Jun 2023 18:17:54 +0530 Subject: [PATCH] feat(android): wire up import/export settings to UI --- CHANGELOG.md | 4 + .../msfjarvis/claw/android/ui/LobstersApp.kt | 16 +++ .../ui/datatransfer/DataTransferScreen.kt | 116 ++++++++++++++++++ .../android/ui/navigation/Destinations.kt | 6 +- .../claw/android/viewmodel/ClawViewModel.kt | 1 + 5 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 android/src/main/kotlin/dev/msfjarvis/claw/android/ui/datatransfer/DataTransferScreen.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index d763d3a4..86688a3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +* Backup and restore options for saved posts + ## [1.28.0] - 2023-06-03 ### Changed diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/LobstersApp.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/LobstersApp.kt index 4ffd19bc..9a90138f 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/LobstersApp.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/LobstersApp.kt @@ -18,6 +18,7 @@ import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.NewReleases import androidx.compose.material.icons.filled.Whatshot import androidx.compose.material.icons.outlined.FavoriteBorder +import androidx.compose.material.icons.outlined.ImportExport import androidx.compose.material.icons.outlined.NavigateBefore import androidx.compose.material.icons.outlined.NewReleases import androidx.compose.material.icons.outlined.Whatshot @@ -49,6 +50,7 @@ import androidx.navigation.navDeepLink import androidx.paging.compose.collectAsLazyPagingItems import com.deliveryhero.whetstone.compose.injectedViewModel import dev.msfjarvis.claw.android.R +import dev.msfjarvis.claw.android.ui.datatransfer.DataTransferScreen import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationRail import dev.msfjarvis.claw.android.ui.decorations.NavigationItem @@ -149,6 +151,14 @@ fun LobstersApp( Text(text = stringResource(R.string.app_name), fontWeight = FontWeight.Bold) } }, + actions = { + IconButton(onClick = { navController.navigate(Destinations.DataTransfer.route) }) { + Icon( + imageVector = Icons.Outlined.ImportExport, + contentDescription = "Data transfer options" + ) + } + }, ) }, bottomBar = { @@ -237,6 +247,12 @@ fun LobstersApp( getProfile = viewModel::getUserProfile, ) } + composable(route = Destinations.DataTransfer.route) { + DataTransferScreen( + context = context, + dataTransferRepository = viewModel.dataTransferRepository, + ) + } } } } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/datatransfer/DataTransferScreen.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/datatransfer/DataTransferScreen.kt new file mode 100644 index 00000000..441f03b9 --- /dev/null +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/datatransfer/DataTransferScreen.kt @@ -0,0 +1,116 @@ +/* + * Copyright © 2023 Harsh Shandilya. + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + */ +package dev.msfjarvis.claw.android.ui.datatransfer + +import android.content.Context +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts.CreateDocument +import androidx.activity.result.contract.ActivityResultContracts.GetContent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.NorthEast +import androidx.compose.material.icons.outlined.SouthWest +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.Text +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.unit.dp +import dev.msfjarvis.claw.android.viewmodel.DataTransferRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +private const val MIME_TYPE = "application/json" + +@Composable +fun DataTransferScreen( + context: Context, + dataTransferRepository: DataTransferRepository, + modifier: Modifier = Modifier, +) { + val coroutineScope = rememberCoroutineScope() + Column(modifier = modifier) { + ImportOption(context, coroutineScope, dataTransferRepository) + ExportOption(context, coroutineScope, dataTransferRepository) + } +} + +@Composable +private fun ImportOption( + context: Context, + coroutineScope: CoroutineScope, + dataTransferRepository: DataTransferRepository, +) { + val importAction = + rememberLauncherForActivityResult(GetContent()) { uri -> + if (uri == null) return@rememberLauncherForActivityResult + coroutineScope.launch { + context.contentResolver.openInputStream(uri)?.use { stream -> + dataTransferRepository.importPosts(stream) + } + } + } + SettingsActionItem( + title = "Import saved posts", + description = "Import saved posts from a previously generated export", + icon = Icons.Outlined.SouthWest, + ) { + importAction.launch(MIME_TYPE) + } +} + +@Composable +private fun ExportOption( + context: Context, + coroutineScope: CoroutineScope, + dataTransferRepository: DataTransferRepository, +) { + val exportAction = + rememberLauncherForActivityResult(CreateDocument(MIME_TYPE)) { uri -> + if (uri == null) return@rememberLauncherForActivityResult + coroutineScope.launch { + context.contentResolver.openOutputStream(uri)?.use { stream -> + dataTransferRepository.exportPosts(stream) + } + } + } + SettingsActionItem( + title = "Export posts to file", + description = "Write all saved posts into a JSON file that can be imported at a later date", + icon = Icons.Outlined.NorthEast, + ) { + exportAction.launch("claw-export.json") + } +} + +@Composable +private fun SettingsActionItem( + title: String, + modifier: Modifier = Modifier, + description: String? = null, + icon: ImageVector? = null, + onClick: (() -> Unit)? = null, +) { + ListItem( + headlineContent = { Text(title) }, + supportingContent = { description?.let { Text(it) } }, + leadingContent = { + icon?.let { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.height(32.dp), + ) + } + }, + modifier = modifier.clickable { onClick?.invoke() }, + ) +} diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/navigation/Destinations.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/navigation/Destinations.kt index 60b1d052..4d760ea1 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/navigation/Destinations.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/ui/navigation/Destinations.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2021-2022 Harsh Shandilya. + * Copyright © 2021-2023 Harsh Shandilya. * Use of this source code is governed by an MIT-style * license that can be found in the LICENSE file or at * https://opensource.org/licenses/MIT. @@ -31,6 +31,10 @@ sealed class Destinations { override val route = "user/$placeholder" } + object DataTransfer : Destinations() { + override val route: String = "datatransfer" + } + companion object { val startDestination get() = Hottest diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ClawViewModel.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ClawViewModel.kt index 31f837e9..c04f0db5 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ClawViewModel.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ClawViewModel.kt @@ -41,6 +41,7 @@ constructor( private val savedPostsRepository: SavedPostsRepository, private val commentsRepository: CommentsRepository, private val linkMetadataRepository: LinkMetadataRepository, + val dataTransferRepository: DataTransferRepository, private val pagingSourceFactory: LobstersPagingSource.Factory, @IODispatcher private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() {