summaryrefslogtreecommitdiff
path: root/app/src/main
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-08-23 00:47:56 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-08-23 00:47:56 +0000
commitc8b6c5344c7194e2ef3336446919b66714062560 (patch)
tree5cd4c30244fb02c6128cc78bccdc50ba52e47fd2 /app/src/main
parent5f3850ab73903010269df6009204a96f9d3e68ac (diff)
Restore scroll position within an article on Next/Previous
Next/Previous replaces this screen's own back-stack entry with a fresh one for the neighboring article, which recreates the WebView from scratch -- going back to an article you'd been partway through always landed you back at the top. Tracks each article's scroll offset in an in-memory, process-lifetime map (ReaderScrollPositions) as the WebView scrolls, and restores it once the freshly-loaded page finishes laying out.
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt7
-rw-r--r--app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt27
2 files changed, 32 insertions, 2 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 4c74e5a..2bdaf64 100644
--- a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
@@ -519,7 +519,12 @@ private fun ReaderContent(
ctx,
onOpenLink,
onLongPressLink = { longPressedLink = it },
- onYouTubeVideo = { youtubeVideoId = it }
+ onYouTubeVideo = { youtubeVideoId = it },
+ onScrollChanged = { scrollY -> ReaderScrollPositions.set(articleId, scrollY) },
+ // The page's real content height isn't known until layout finishes
+ // -- scrolling to a saved offset any earlier would just clamp back
+ // to 0 on a page that hasn't grown tall enough yet.
+ onPageFinished = { webView -> webView.scrollTo(0, ReaderScrollPositions.get(articleId)) }
).apply {
loadDataWithBaseURL(baseUrl, wrapReaderHtml(content, article?.title, article?.publishedAt, article?.imageUrl), "text/html", "UTF-8", null)
loadedContent.value = content
diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt
index f0873c8..31fcb02 100644
--- a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt
@@ -23,6 +23,18 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
+// In-memory only (process lifetime, not persisted to disk) -- scoped to "don't lose my
+// place if Next/Previous takes me away and back," not to surviving an app restart. A
+// process-wide singleton rather than screen-scoped state so it survives an Activity
+// recreation (rotation) too, not just Compose recomposition within the same screen instance.
+object ReaderScrollPositions {
+ private val positions = mutableMapOf<String, Int>()
+ fun get(articleId: String): Int = positions[articleId] ?: 0
+ fun set(articleId: String, scrollY: Int) {
+ positions[articleId] = scrollY
+ }
+}
+
private fun isYouTubeWatchUrl(uri: Uri): Boolean {
val host = uri.host?.lowercase() ?: return false
return host == "youtu.be" || host.endsWith(".youtube.com") || host == "youtube.com"
@@ -68,14 +80,27 @@ fun newReaderWebView(
context: Context,
onOpenLink: (String) -> Unit,
onLongPressLink: (String) -> Unit = {},
- onYouTubeVideo: (String) -> Unit = {}
+ onYouTubeVideo: (String) -> Unit = {},
+ onScrollChanged: (Int) -> Unit = {},
+ onPageFinished: (WebView) -> Unit = {}
): WebView =
WebView(context).apply {
settings.javaScriptEnabled = false
overScrollMode = View.OVER_SCROLL_NEVER
isVerticalScrollBarEnabled = false
isHorizontalScrollBarEnabled = false
+ // Reports the WebView's own scroll offset as the user reads, so a caller (ReaderScreen)
+ // can remember how far into an article they'd gotten and restore it -- e.g. after
+ // Next/Previous replaces this screen's back-stack entry with a fresh one for the same
+ // article, which recreates the WebView from scratch and would otherwise always land
+ // back at the top.
+ setOnScrollChangeListener { _, _, scrollY, _, _ -> onScrollChanged(scrollY) }
webViewClient = object : WebViewClient() {
+ override fun onPageFinished(view: WebView, url: String?) {
+ super.onPageFinished(view, url)
+ onPageFinished(view)
+ }
+
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
// A subframe (an <iframe> in the article body, e.g. a non-YouTube oEmbed
// like Twitter/Instagram, or an ad) fires this same callback the instant