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"
|
override val route = "user/$PLACEHOLDER"
|
||||||
}
|
}
|
||||||
|
|
||||||
data object DataTransfer : Destinations() {
|
|
||||||
override val route = "datatransfer"
|
|
||||||
}
|
|
||||||
|
|
||||||
data object Search : Destinations() {
|
data object Search : Destinations() {
|
||||||
override val route = "search"
|
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.MainActivity
|
||||||
import dev.msfjarvis.claw.android.R
|
import dev.msfjarvis.claw.android.R
|
||||||
import dev.msfjarvis.claw.android.SearchActivity
|
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.ClawAppBar
|
||||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar
|
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationBar
|
||||||
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationRail
|
import dev.msfjarvis.claw.android.ui.decorations.ClawNavigationRail
|
||||||
|
@ -282,21 +281,16 @@ fun LobstersPostsScreen(
|
||||||
setWebUri("https://lobste.rs/u/$username")
|
setWebUri("https://lobste.rs/u/$username")
|
||||||
UserProfile(username = username, getProfile = viewModel::getUserProfile)
|
UserProfile(username = username, getProfile = viewModel::getUserProfile)
|
||||||
}
|
}
|
||||||
composable(route = Destinations.DataTransfer.route) {
|
composable(route = Destinations.Settings.route) {
|
||||||
DataTransferScreen(
|
SettingsScreen(
|
||||||
context = context,
|
context = context,
|
||||||
|
openLibrariesScreen = { navController.navigate(Destinations.AboutLibraries.route) },
|
||||||
importPosts = viewModel::importPosts,
|
importPosts = viewModel::importPosts,
|
||||||
exportPosts = viewModel::exportPosts,
|
exportPostsAsJson = viewModel::exportPostsAsJson,
|
||||||
exportPostsAsHtml = viewModel::exportPostsAsHtml,
|
exportPostsAsHtml = viewModel::exportPostsAsHtml,
|
||||||
snackbarHostState = snackbarHostState,
|
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) {
|
composable(route = Destinations.AboutLibraries.route) {
|
||||||
LibrariesContainer(modifier = Modifier.fillMaxSize())
|
LibrariesContainer(modifier = Modifier.fillMaxSize())
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,35 +6,181 @@
|
||||||
*/
|
*/
|
||||||
package dev.msfjarvis.claw.android.ui.screens
|
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.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
|
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.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 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
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
|
context: Context,
|
||||||
openLibrariesScreen: () -> Unit,
|
openLibrariesScreen: () -> Unit,
|
||||||
openDataTransferScreen: () -> Unit,
|
snackbarHostState: SnackbarHostState,
|
||||||
|
importPosts: suspend (InputStream) -> Unit,
|
||||||
|
exportPostsAsJson: suspend (OutputStream) -> Unit,
|
||||||
|
exportPostsAsHtml: suspend (OutputStream) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
Column {
|
Column {
|
||||||
SettingsActionItem(
|
ListItem(
|
||||||
title = "Data transfer",
|
headlineContent = { Text("Libraries") },
|
||||||
description = "Export and import your saved posts",
|
leadingContent = {
|
||||||
icon = Icons.Filled.ImportExport,
|
Icon(
|
||||||
onClick = openDataTransferScreen,
|
imageVector = Icons.AutoMirrored.Filled.LibraryBooks,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.height(32.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable(onClick = openLibrariesScreen),
|
||||||
)
|
)
|
||||||
SettingsActionItem(
|
Text(
|
||||||
title = "Libraries",
|
text = "Data transfer",
|
||||||
icon = Icons.AutoMirrored.Filled.LibraryBooks,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
onClick = openLibrariesScreen,
|
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 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) =
|
suspend fun exportPostsAsHtml(output: OutputStream) =
|
||||||
dataTransferRepository.exportPostsAsHTML(output)
|
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() }
|
val posts = withContext(dbDispatcher) { savedPostQueries.selectAllPosts().executeAsList() }
|
||||||
withContext(ioDispatcher) { json.encodeToStream(serializer, posts, output) }
|
withContext(ioDispatcher) { json.encodeToStream(serializer, posts, output) }
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue