summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-22 18:26:14 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-22 18:26:14 +0000
commit91edd1f5f29a3336fa9e3bd1762c70ea74e3e48d (patch)
tree6ebe8615b61d174348a8467aa1fd7dbbfdb03a0a /app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
parent9da3fbcdef71e90d52045531c7c18faff02f2ff6 (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 "&#8220;" 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 &#8220; 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.kt51
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()