mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-14 21:07:04 +05:30
refactor(android): further simplify settings page navigation
This commit is contained in:
parent
988cf117c2
commit
1885859d2f
6 changed files with 165 additions and 205 deletions
|
@ -1,177 +0,0 @@
|
|||
/*
|
||||
* Copyright © 2023-2024 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.filled.Download
|
||||
import androidx.compose.material.icons.filled.Upload
|
||||
import androidx.compose.material.icons.filled.WebStories
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
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 java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val JSON_MIME_TYPE = "application/json"
|
||||
private const val HTML_MIME_TYPE = "application/html"
|
||||
|
||||
@Composable
|
||||
fun DataTransferScreen(
|
||||
context: Context,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
exportPosts: suspend (OutputStream) -> Unit,
|
||||
exportPostsAsHtml: suspend (OutputStream) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
Column(modifier = modifier) {
|
||||
JsonImportOption(context, coroutineScope, importPosts, snackbarHostState)
|
||||
JsonExportOption(context, coroutineScope, exportPosts, snackbarHostState)
|
||||
HtmlExportOption(context, coroutineScope, exportPostsAsHtml, snackbarHostState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun JsonImportOption(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
val importAction =
|
||||
rememberLauncherForActivityResult(GetContent()) { uri ->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openInputStream(uri)?.use { stream ->
|
||||
importPosts(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully imported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsActionItem(
|
||||
title = "Import saved posts",
|
||||
description = "Import saved posts from a previously generated JSON export",
|
||||
icon = Icons.Filled.Download,
|
||||
) {
|
||||
importAction.launch(JSON_MIME_TYPE)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun JsonExportOption(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
exportPosts: suspend (OutputStream) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
GenericExportOption(
|
||||
title = "Export posts to file",
|
||||
description = "Write all saved posts into a JSON file that can be imported at a later date",
|
||||
icon = Icons.Filled.Upload,
|
||||
fileName = "claw-export.json",
|
||||
mimeType = JSON_MIME_TYPE,
|
||||
context = context,
|
||||
coroutineScope = coroutineScope,
|
||||
exportPosts = exportPosts,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HtmlExportOption(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
exportPosts: suspend (OutputStream) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
GenericExportOption(
|
||||
title = "Export posts as bookmarks",
|
||||
description = "Write all saved posts into a HTML file that can be imported by web browsers",
|
||||
icon = Icons.Filled.WebStories,
|
||||
fileName = "claw-export.html",
|
||||
mimeType = HTML_MIME_TYPE,
|
||||
context = context,
|
||||
coroutineScope = coroutineScope,
|
||||
exportPosts = exportPosts,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GenericExportOption(
|
||||
title: String,
|
||||
description: String,
|
||||
icon: ImageVector,
|
||||
fileName: String,
|
||||
mimeType: String,
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
exportPosts: suspend (OutputStream) -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
val exportAction =
|
||||
rememberLauncherForActivityResult(CreateDocument(mimeType)) { uri ->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openOutputStream(uri)?.use { stream ->
|
||||
exportPosts(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully exported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsActionItem(title = title, description = description, icon = icon) {
|
||||
exportAction.launch(fileName)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
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() },
|
||||
)
|
||||
}
|
||||
|
||||
/** Shows a Snackbar but dismisses any existing ones first. */
|
||||
private suspend fun SnackbarHostState.showSnackbarDismissing(text: String) {
|
||||
currentSnackbarData?.dismiss()
|
||||
showSnackbar(text)
|
||||
}
|
|
@ -31,10 +31,6 @@ sealed class Destinations {
|
|||
override val route = "user/$PLACEHOLDER"
|
||||
}
|
||||
|
||||
data object DataTransfer : Destinations() {
|
||||
override val route = "datatransfer"
|
||||
}
|
||||
|
||||
data object Search : Destinations() {
|
||||
override val route = "search"
|
||||
}
|
||||
|
|
|
@ -65,7 +65,6 @@ import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
|||
import dev.msfjarvis.claw.android.MainActivity
|
||||
import dev.msfjarvis.claw.android.R
|
||||
import dev.msfjarvis.claw.android.SearchActivity
|
||||
import dev.msfjarvis.claw.android.ui.datatransfer.DataTransferScreen
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawAppBar
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar
|
||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationRail
|
||||
|
@ -282,21 +281,16 @@ fun LobstersPostsScreen(
|
|||
setWebUri("https://lobste.rs/u/$username")
|
||||
UserProfile(username = username, getProfile = viewModel::getUserProfile)
|
||||
}
|
||||
composable(route = Destinations.DataTransfer.route) {
|
||||
DataTransferScreen(
|
||||
composable(route = Destinations.Settings.route) {
|
||||
SettingsScreen(
|
||||
context = context,
|
||||
openLibrariesScreen = { navController.navigate(Destinations.AboutLibraries.route) },
|
||||
importPosts = viewModel::importPosts,
|
||||
exportPosts = viewModel::exportPosts,
|
||||
exportPostsAsJson = viewModel::exportPostsAsJson,
|
||||
exportPostsAsHtml = viewModel::exportPostsAsHtml,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.Settings.route) {
|
||||
SettingsScreen(
|
||||
openLibrariesScreen = { navController.navigate(Destinations.AboutLibraries.route) },
|
||||
openDataTransferScreen = { navController.navigate(Destinations.DataTransfer.route) },
|
||||
)
|
||||
}
|
||||
composable(route = Destinations.AboutLibraries.route) {
|
||||
LibrariesContainer(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
|
|
|
@ -6,35 +6,181 @@
|
|||
*/
|
||||
package dev.msfjarvis.claw.android.ui.screens
|
||||
|
||||
import android.content.Context
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
|
||||
import androidx.compose.material.icons.filled.ImportExport
|
||||
import androidx.compose.material.icons.filled.Upload
|
||||
import androidx.compose.material.icons.filled.WebStories
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ElevatedButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.msfjarvis.claw.android.ui.datatransfer.SettingsActionItem
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val JSON_MIME_TYPE = "application/json"
|
||||
private const val HTML_MIME_TYPE = "application/html"
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
context: Context,
|
||||
openLibrariesScreen: () -> Unit,
|
||||
openDataTransferScreen: () -> Unit,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
exportPostsAsJson: suspend (OutputStream) -> Unit,
|
||||
exportPostsAsHtml: suspend (OutputStream) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
Column {
|
||||
SettingsActionItem(
|
||||
title = "Data transfer",
|
||||
description = "Export and import your saved posts",
|
||||
icon = Icons.Filled.ImportExport,
|
||||
onClick = openDataTransferScreen,
|
||||
ListItem(
|
||||
headlineContent = { Text("Libraries") },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.LibraryBooks,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(32.dp),
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = openLibrariesScreen),
|
||||
)
|
||||
SettingsActionItem(
|
||||
title = "Libraries",
|
||||
icon = Icons.AutoMirrored.Filled.LibraryBooks,
|
||||
onClick = openLibrariesScreen,
|
||||
Text(
|
||||
text = "Data transfer",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
modifier = Modifier.padding(all = 16.dp),
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.SpaceAround, modifier = Modifier.fillMaxWidth()) {
|
||||
ImportPosts(context, coroutineScope, snackbarHostState, importPosts)
|
||||
ExportPosts(
|
||||
context,
|
||||
coroutineScope,
|
||||
snackbarHostState,
|
||||
exportPostsAsJson,
|
||||
exportPostsAsHtml,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImportPosts(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
) {
|
||||
val importAction =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openInputStream(uri)?.use { stream ->
|
||||
importPosts(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully imported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
ElevatedButton(onClick = { importAction.launch(JSON_MIME_TYPE) }) { Text(text = "Import") }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExportPosts(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
exportPostsAsJson: suspend (OutputStream) -> Unit,
|
||||
exportPostsAsHtml: suspend (OutputStream) -> Unit,
|
||||
) {
|
||||
val jsonExportAction =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument(JSON_MIME_TYPE)) { uri
|
||||
->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openOutputStream(uri)?.use { stream ->
|
||||
exportPostsAsJson(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully exported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
val htmlExportAction =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument(HTML_MIME_TYPE)) { uri
|
||||
->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
coroutineScope.launch {
|
||||
context.contentResolver.openOutputStream(uri)?.use { stream ->
|
||||
exportPostsAsHtml(stream)
|
||||
snackbarHostState.showSnackbarDismissing("Successfully exported posts")
|
||||
}
|
||||
}
|
||||
}
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
ElevatedButton(onClick = { expanded = true }) {
|
||||
Text(text = "Export")
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("JSON") },
|
||||
onClick = {
|
||||
expanded = false
|
||||
jsonExportAction.launch("claw-export.json")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Filled.Upload, contentDescription = "Export as JSON")
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Bookmarks") },
|
||||
onClick = {
|
||||
expanded = false
|
||||
htmlExportAction.launch("claw-export.html")
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.WebStories,
|
||||
contentDescription = "Export as browser bookmarks",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows a Snackbar but dismisses any existing ones first. */
|
||||
private suspend fun SnackbarHostState.showSnackbarDismissing(text: String) {
|
||||
currentSnackbarData?.dismiss()
|
||||
showSnackbar(text)
|
||||
}
|
||||
|
|
|
@ -188,7 +188,8 @@ constructor(
|
|||
|
||||
suspend fun importPosts(input: InputStream) = dataTransferRepository.importPosts(input)
|
||||
|
||||
suspend fun exportPosts(output: OutputStream) = dataTransferRepository.exportPosts(output)
|
||||
suspend fun exportPostsAsJson(output: OutputStream) =
|
||||
dataTransferRepository.exportPostsAsJson(output)
|
||||
|
||||
suspend fun exportPostsAsHtml(output: OutputStream) =
|
||||
dataTransferRepository.exportPostsAsHTML(output)
|
||||
|
|
|
@ -43,7 +43,7 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun exportPosts(output: OutputStream) {
|
||||
suspend fun exportPostsAsJson(output: OutputStream) {
|
||||
val posts = withContext(dbDispatcher) { savedPostQueries.selectAllPosts().executeAsList() }
|
||||
withContext(ioDispatcher) { json.encodeToStream(serializer, posts, output) }
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue