mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-14 22:17:03 +05:30
feat(android): add the ability to export posts as browser bookmarks
This commit is contained in:
parent
311f7c0574
commit
338ad07db7
5 changed files with 119 additions and 11 deletions
|
@ -16,6 +16,7 @@ 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
|
||||
|
@ -30,25 +31,28 @@ import java.io.OutputStream
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val MIME_TYPE = "application/json"
|
||||
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) {
|
||||
ImportOption(context, coroutineScope, importPosts, snackbarHostState)
|
||||
ExportOption(context, coroutineScope, exportPosts, snackbarHostState)
|
||||
JsonImportOption(context, coroutineScope, importPosts, snackbarHostState)
|
||||
JsonExportOption(context, coroutineScope, exportPosts, snackbarHostState)
|
||||
HtmlExportOption(context, coroutineScope, exportPostsAsHtml, snackbarHostState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImportOption(
|
||||
private fun JsonImportOption(
|
||||
context: Context,
|
||||
coroutineScope: CoroutineScope,
|
||||
importPosts: suspend (InputStream) -> Unit,
|
||||
|
@ -72,19 +76,64 @@ private fun ImportOption(
|
|||
description = "Import saved posts from a previously generated export",
|
||||
icon = Icons.Filled.Download,
|
||||
) {
|
||||
importAction.launch(MIME_TYPE)
|
||||
importAction.launch(JSON_MIME_TYPE)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExportOption(
|
||||
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(MIME_TYPE)) { uri ->
|
||||
rememberLauncherForActivityResult(CreateDocument(mimeType)) { uri ->
|
||||
if (uri == null) {
|
||||
coroutineScope.launch { snackbarHostState.showSnackbarDismissing("No file selected") }
|
||||
return@rememberLauncherForActivityResult
|
||||
|
@ -97,11 +146,11 @@ private fun ExportOption(
|
|||
}
|
||||
}
|
||||
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.Filled.Upload,
|
||||
title = title,
|
||||
description = description,
|
||||
icon = icon,
|
||||
) {
|
||||
exportAction.launch("claw-export.json")
|
||||
exportAction.launch(fileName)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -322,6 +322,7 @@ fun LobstersPostsScreen(
|
|||
context = context,
|
||||
importPosts = viewModel::importPosts,
|
||||
exportPosts = viewModel::exportPosts,
|
||||
exportPostsAsHtml = viewModel::exportPostsAsHtml,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -179,6 +179,9 @@ constructor(
|
|||
|
||||
suspend fun exportPosts(output: OutputStream) = dataTransferRepository.exportPosts(output)
|
||||
|
||||
suspend fun exportPostsAsHtml(output: OutputStream) =
|
||||
dataTransferRepository.exportPostsAsHTML(output)
|
||||
|
||||
fun markPostAsRead(postId: String) {
|
||||
viewModelScope.launch { readPostsRepository.markRead(postId) }
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ import dev.msfjarvis.claw.database.local.SavedPost
|
|||
import dev.msfjarvis.claw.database.local.SavedPostQueries
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -50,4 +52,53 @@ constructor(
|
|||
val posts = withContext(dbDispatcher) { savedPostQueries.selectAllPosts().executeAsList() }
|
||||
withContext(ioDispatcher) { json.encodeToStream(serializer, posts, output) }
|
||||
}
|
||||
|
||||
suspend fun exportPostsAsHTML(output: OutputStream) {
|
||||
fun computeTimestamp(post: SavedPost): Long {
|
||||
val temporal = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(post.createdAt)
|
||||
val instant = Instant.from(temporal)
|
||||
return instant.toEpochMilli()
|
||||
}
|
||||
|
||||
val posts = withContext(dbDispatcher) { savedPostQueries.selectAllPosts().executeAsList() }
|
||||
val header =
|
||||
"""
|
||||
<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
||||
<!-- This is an automatically generated file.
|
||||
It will be read and overwritten.
|
||||
DO NOT EDIT! -->
|
||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||
<TITLE>Bookmarks</TITLE>
|
||||
<H1>Bookmarks</H1>
|
||||
"""
|
||||
.trimIndent()
|
||||
val html = buildString {
|
||||
append(header)
|
||||
append("<DD><p>\n")
|
||||
for (post in posts) {
|
||||
append(
|
||||
"""
|
||||
<DT><A HREF="${post.url.ifEmpty { post.commentsUrl }}" ADD_DATE="${computeTimestamp(post)}" PRIVATE="0" TAGS="${post.tags.joinToString(",")}">${post.title}</A>
|
||||
<DD>${post.title}
|
||||
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
}
|
||||
append(
|
||||
"""
|
||||
<DT><A HREF="https://example.com/" ADD_DATE="0" PRIVATE="0" TAGS="delete,me,pls">Padding post</A>
|
||||
<DD>Linkding ignores the last entry so this pads the difference for imports
|
||||
|
||||
"""
|
||||
.trimIndent()
|
||||
)
|
||||
append("</DD></p>\n")
|
||||
}
|
||||
withContext(ioDispatcher) {
|
||||
val writer = output.bufferedWriter()
|
||||
writer.write(html)
|
||||
writer.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue