From 91edd1f5f29a3336fa9e3bd1762c70ea74e3e48d Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Wed, 22 Jul 2026 18:26:14 +0000 Subject: 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. --- .../org/terst/feedreader/ui/LinkReaderScreen.kt | 112 +++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt (limited to 'app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt') diff --git a/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt b/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt new file mode 100644 index 0000000..ff764c4 --- /dev/null +++ b/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt @@ -0,0 +1,112 @@ +package org.terst.feedreader.ui + +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.OpenInBrowser +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import org.terst.feedreader.readability.ReaderExtractor + +/** + * A lightweight, non-persisted reader view for links tapped inside an article's + * WebView -- extracts the linked page with the same Readability pipeline as the + * main reader, but doesn't touch the DB (it's not a subscribed article), and + * recursively re-opens itself for any further links tapped inside, so link-hopping + * stays inside the app's reader instead of dropping into a raw WebView or the + * system browser. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LinkReaderScreen(url: String, onBack: () -> Unit, onOpenLink: (String) -> Unit) { + val context = LocalContext.current + var title by remember(url) { mutableStateOf(null) } + var content by remember(url) { mutableStateOf(null) } + var failed by remember(url) { mutableStateOf(false) } + + LaunchedEffect(url) { + val extracted = ReaderExtractor.extract(context, url) + val extractedContent = extracted?.content + if (extractedContent != null) { + title = extracted.title + content = extractedContent + } else { + failed = true + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { + Text(title ?: Uri.parse(url).host ?: "Article", maxLines = 1, overflow = TextOverflow.Ellipsis) + }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.Default.ArrowBack, contentDescription = "Back") + } + }, + actions = { + IconButton(onClick = { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + }) { + Icon(Icons.Default.OpenInBrowser, contentDescription = "Open in browser") + } + } + ) + } + ) { padding -> + Box(modifier = Modifier.fillMaxSize().padding(padding)) { + val currentContent = content + when { + currentContent != null -> { + val loadedContent = remember(url) { mutableStateOf(null) } + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { ctx -> + newReaderWebView(ctx, onOpenLink).apply { + loadDataWithBaseURL(url, wrapReaderHtml(currentContent), "text/html", "UTF-8", null) + loadedContent.value = currentContent + } + }, + update = { webView -> + if (loadedContent.value != currentContent) { + webView.loadDataWithBaseURL(url, wrapReaderHtml(currentContent), "text/html", "UTF-8", null) + loadedContent.value = currentContent + } + }, + onRelease = { webView -> webView.destroy() } + ) + } + failed -> Text( + "Couldn't extract a reader view for this page -- try \"Open in browser\" instead.", + modifier = Modifier.align(Alignment.Center).padding(24.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + else -> CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } + } + } +} -- cgit v1.2.3