diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-22 18:26:14 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-22 18:26:14 +0000 |
| commit | 91edd1f5f29a3336fa9e3bd1762c70ea74e3e48d (patch) | |
| tree | 6ebe8615b61d174348a8467aa1fd7dbbfdb03a0a /app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt | |
| parent | 9da3fbcdef71e90d52045531c7c18faff02f2ff6 (diff) | |
Split save/Stash actions, follow links in-app via reader pipeline, tune scroll, fix title entities
- ReaderScreen: bookmark icon is now a purely local save (just flips isSaved,
no network) -- Stash archiving is a separate explicit action (new cloud
icon), since it's a heavier networked action the user should trigger on
purpose rather than have bundled into "save." Verified via direct DB
queries and (once I stopped misreading small screenshots) the icon
updates immediately in-session for both actions.
- New ReaderWebView.kt: shared WebView factory for the dark reader palette
and link handling, used by both ReaderScreen and the new LinkReaderScreen.
Links tapped inside an article's WebView now open in a lightweight
in-app reader (extracted via the same Readability pipeline, no DB
persistence) instead of falling through to the system browser --
recursive, so a chain of links stays in-app. Verified live: tapping a
link mid-article navigated to a real extracted page with a working back
stack.
- WebView scroll tuning: overscroll glow and scrollbars off, on both the
article reader and the new link reader -- aimed at the reported "still a
little choppy" scrolling on top of the previous reload-on-recompose fix.
- FeedParser: some feeds (kottke.org among them) double-encode titles --
the XML parser resolves outer XML escaping, but leftover HTML entities
like "“" were showing up literally in the title text. Titles are
now run through a second HTML-entity decode pass. Verified: a kottke
title that previously showed a literal “ now renders the actual
curly quote.
All changes verified live on the emulator against the real 107-feed dataset.
Diffstat (limited to 'app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt')
| -rw-r--r-- | app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt | 51 |
1 files changed, 25 insertions, 26 deletions
diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt index 2177d72..ced9482 100644 --- a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt +++ b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt @@ -2,7 +2,6 @@ package org.terst.feedreader.ui import android.content.Intent import android.net.Uri -import android.webkit.WebView import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -15,6 +14,8 @@ import androidx.compose.material.icons.filled.Article import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Bookmark import androidx.compose.material.icons.filled.BookmarkBorder +import androidx.compose.material.icons.filled.CloudDone +import androidx.compose.material.icons.filled.CloudUpload import androidx.compose.material.icons.filled.OpenInBrowser import androidx.compose.material.icons.outlined.Article import androidx.compose.material3.CircularProgressIndicator @@ -63,7 +64,7 @@ private const val THIN_CONTENT_THRESHOLD = 500 */ @OptIn(ExperimentalFoundationApi::class) @Composable -fun ReaderScreen(feedId: String, initialArticleId: String, onBack: () -> Unit) { +fun ReaderScreen(feedId: String, initialArticleId: String, onBack: () -> Unit, onOpenLink: (String) -> Unit) { val context = LocalContext.current val db = remember { AppDatabase.get(context) } var articleIds by remember(feedId) { mutableStateOf<List<String>?>(null) } @@ -91,13 +92,13 @@ fun ReaderScreen(feedId: String, initialArticleId: String, onBack: () -> Unit) { val pagerState = rememberPagerState(initialPage = initialIndex) { safeIds.size } HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page -> - ReaderContent(articleId = safeIds[page], onBack = onBack) + ReaderContent(articleId = safeIds[page], onBack = onBack, onOpenLink = onOpenLink) } } @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun ReaderContent(articleId: String, onBack: () -> Unit) { +private fun ReaderContent(articleId: String, onBack: () -> Unit, onOpenLink: (String) -> Unit) { val context = LocalContext.current val db = remember { AppDatabase.get(context) } val scope = rememberCoroutineScope() @@ -204,6 +205,20 @@ private fun ReaderContent(articleId: String, onBack: () -> Unit) { }) { Icon(Icons.Default.OpenInBrowser, contentDescription = "Open in browser") } + // Purely local: just flips isSaved (shows up in the Saved list). Does NOT + // touch Stash -- that's a separate, explicit action below, since archiving + // to Stash is a heavier/networked action the user should trigger on purpose. + IconButton(onClick = { + val a = currentArticle ?: return@IconButton + val newSaved = !a.isSaved + article = a.copy(isSaved = newSaved) + scope.launch { db.articleDao().setSaved(a.id, newSaved) } + }) { + Icon( + if (currentArticle?.isSaved == true) Icons.Default.Bookmark else Icons.Default.BookmarkBorder, + contentDescription = "Save locally" + ) + } IconButton( enabled = !isSaving, onClick = { @@ -214,13 +229,13 @@ private fun ReaderContent(articleId: String, onBack: () -> Unit) { val client = StashClient(Net.client, settings.stashBaseUrlNow(), settings.stashApiKeyNow()) when (val result = client.saveArticle(a.link, a.title)) { is StashSaveResult.Saved -> { - db.articleDao().setSaved(a.id, true) db.articleDao().setStashArtifactId(a.id, result.artifact.id) + article = a.copy(stashArtifactId = result.artifact.id) snackbarHostState.showSnackbar("Saved to Stash") } is StashSaveResult.AlreadySaved -> { - db.articleDao().setSaved(a.id, true) db.articleDao().setStashArtifactId(a.id, result.existingId) + article = a.copy(stashArtifactId = result.existingId) snackbarHostState.showSnackbar("Already in Stash") } is StashSaveResult.Failed -> @@ -231,7 +246,7 @@ private fun ReaderContent(articleId: String, onBack: () -> Unit) { } ) { Icon( - if (currentArticle?.isSaved == true) Icons.Default.Bookmark else Icons.Default.BookmarkBorder, + if (currentArticle?.stashArtifactId != null) Icons.Default.CloudDone else Icons.Default.CloudUpload, contentDescription = "Save to Stash" ) } @@ -254,15 +269,14 @@ private fun ReaderContent(articleId: String, onBack: () -> Unit) { AndroidView( modifier = Modifier.fillMaxSize(), factory = { ctx -> - WebView(ctx).apply { - settings.javaScriptEnabled = false - loadDataWithBaseURL(baseUrl, wrapHtml(content), "text/html", "UTF-8", null) + newReaderWebView(ctx, onOpenLink).apply { + loadDataWithBaseURL(baseUrl, wrapReaderHtml(content), "text/html", "UTF-8", null) loadedContent.value = content } }, update = { webView -> if (loadedContent.value != content) { - webView.loadDataWithBaseURL(baseUrl, wrapHtml(content), "text/html", "UTF-8", null) + webView.loadDataWithBaseURL(baseUrl, wrapReaderHtml(content), "text/html", "UTF-8", null) loadedContent.value = content } }, @@ -278,18 +292,3 @@ private fun ReaderContent(articleId: String, onBack: () -> Unit) { } } } - -// Hardcoded dark palette, not @media (prefers-color-scheme) -- the app itself always -// forces dark colorScheme() regardless of system setting (see MainActivity), so the -// reader content should match rather than following a system light/dark that the rest -// of the app ignores. -private fun wrapHtml(bodyContent: String): String = """ - <html><head><meta name="viewport" content="width=device-width, initial-scale=1"> - <style> - body { font-family: sans-serif; font-size: 18px; line-height: 1.5; padding: 16px; - background-color: #121212; color: #e1e1e1; } - a { color: #8ab4f8; } - img { max-width: 100%; height: auto; } - </style></head> - <body>$bodyContent</body></html> -""".trimIndent() |
