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 21:07:05 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-22 21:07:05 +0000
commitd7c05609692153670dc136082da4e0566d7838a6 (patch)
treeae0a4b5ca3a04eb61cba73fa8a3c3c873faf984f /app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
parent8e06fc84c0b540f8ba87afa19d4c08eab7af80d9 (diff)
Remove reader-mode heuristics entirely; explicit per-feed opt-in only
Reader mode was still switching kottke articles even after the v3 opt-out and the "un-hide only if thin" fallback -- both were still heuristic (content-length-based), and kottke's posts are routinely short by design, not paywalled, so the heuristics kept firing on ordinary content. Removed the auto-extract-on-thin-content logic entirely: reader mode now NEVER turns on by itself, under any condition. - FeedEntity.readerModeDisabled -> readerModeEnabled (opt-in, default off for every feed -- schema v4, full table rebuild since Room's migration validation requires an exact column match and a bare ADD COLUMN left the old column orphaned; reproduced live as a crash, "Migration didn't properly handle: feeds"). - The reader's toggle icon now sets reader mode for the article's WHOLE FEED, persists it immediately, and confirms via snackbar ("Reader mode enabled/disabled for X") -- both in the reader screen and the Feeds list's own toggle. This is the only way reader mode ever turns on. - ReaderExtractor: replaced the "un-hide everything if thin" paywall fallback (which is what pulled kottke's site footer into the result -- confirmed live, twice) with one that only un-hides elements whose class/id actually names them as paywall/premium/subscriber content (West Hawaii Today's hidden div is literally class="paywall paywall_<id>"). This turned out not to be the kottke bug's actual cause either -- see below -- but it's a real, independent correctness improvement over the previous blanket version, so it stays. - Also added: noscript-wrapped lazy-load image fallback (a common pattern pairs a placeholder <img> with a real one inside <noscript>, which our JS-enabled extraction WebView never parses into DOM per the HTML spec) and lazy data-src/srcset resolution generally. Root cause of the kottke footer bug, found by fetching and inspecting the actual page structure rather than guessing again: it's not a CSS-hidden-element issue at all. kottke's real post content lives in a plain, undecorated <div class="post"> that's a sibling (not ancestor) of an ads container and a "Socials & More" footer widget, all under one shared <div class="grid-container">. Because kottke's posts are so short, Readability's generic paragraph-density scoring doesn't find enough signal in .post alone and climbs to a broader container that happens to include the neighboring footer. No un-hide logic is involved at any point -- this reproduces identically on the plain, unmodified Readability pass. This is a genuine limit of heuristic content extraction on very short-form posts, not something to keep patching with more heuristics. Reset kottke's setting back to off (its correct default) after testing.
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.kt87
1 files changed, 47 insertions, 40 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 4becea6..806926c 100644
--- a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
+++ b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt
@@ -59,10 +59,6 @@ import org.terst.feedreader.stash.StashClient
import org.terst.feedreader.stash.StashSaveResult
import java.net.URLEncoder
-// Below this length (of the plain feed-provided HTML), assume it's a teaser/summary,
-// not the real article body, and auto-fall-back to on-device Readability extraction.
-private const val THIN_CONTENT_THRESHOLD = 500
-
/**
* Swipeable wrapper: snapshots the article-id order of the list this reader
* was opened from (Unread/Saved/a specific feed) ONCE at entry, then pages
@@ -191,44 +187,53 @@ private fun ReaderContent(
feedContent = loaded.feedContentHtml
readerContent = loaded.extractedContent
- val plainLength = feedContent?.replace(Regex("<[^>]*>"), "")?.trim()?.length ?: 0
- if (loadedFeed?.readerModeDisabled != true && readerContent == null && plainLength < THIN_CONTENT_THRESHOLD) {
- // Feed content is too thin to be the real article -- auto-extract, since in this
- // case reader mode isn't really an alternative view, it's the only real content.
- isExtracting = true
- val extracted = ReaderExtractor.extract(context, loaded.link)
- isExtracting = false
- extracted?.content?.let {
- readerContent = it
- db.articleDao().setExtractedContent(articleId, it)
- }
+ // Reader mode is NEVER heuristic/automatic (no more "content looks thin, switch
+ // for you") -- it strictly follows the feed's persisted, explicitly-set opt-in.
+ // Once a feed has it on, articles from it default to the extracted view and
+ // auto-extract on open (that's the point of persisting it per-feed: no re-tapping
+ // per article), but nothing here ever turns it on by itself.
+ if (loadedFeed?.readerModeEnabled == true) {
isReaderMode = true
+ if (readerContent == null) {
+ isExtracting = true
+ val extracted = ReaderExtractor.extract(context, loaded.link)
+ isExtracting = false
+ extracted?.content?.let {
+ readerContent = it
+ db.articleDao().setExtractedContent(articleId, it)
+ }
+ }
}
}
- fun toggleReaderMode() {
+ // The toggle now sets reader mode for the ARTICLE'S FEED, persisted immediately, with
+ // a confirmation snackbar -- not a per-article, this-session-only view flip. Tapping it
+ // is the one and only way reader mode ever turns on.
+ fun toggleFeedReaderMode() {
+ val f = feed ?: return
val a = article ?: return
- if (feed?.readerModeDisabled == true) return
- if (isReaderMode) {
- isReaderMode = false
- return
- }
- if (readerContent != null) {
- isReaderMode = true
- return
- }
+ val newEnabled = !f.readerModeEnabled
+ feed = f.copy(readerModeEnabled = newEnabled)
scope.launch {
- isExtracting = true
- val extracted = ReaderExtractor.extract(context, a.link)
- isExtracting = false
- val content = extracted?.content
- if (content != null) {
- readerContent = content
- db.articleDao().setExtractedContent(articleId, content)
+ db.feedDao().update(f.copy(readerModeEnabled = newEnabled))
+ val feedLabel = f.title.ifBlank { "this feed" }
+ if (newEnabled) {
+ if (readerContent == null) {
+ isExtracting = true
+ val extracted = ReaderExtractor.extract(context, a.link)
+ isExtracting = false
+ val content = extracted?.content
+ if (content != null) {
+ readerContent = content
+ db.articleDao().setExtractedContent(articleId, content)
+ }
+ }
isReaderMode = true
+ snackbarHostState.showSnackbar("Reader mode enabled for $feedLabel")
} else {
- snackbarHostState.showSnackbar("Couldn't extract a reader view for this article")
+ isReaderMode = false
+ snackbarHostState.showSnackbar("Reader mode disabled for $feedLabel")
}
}
}
@@ -257,14 +262,16 @@ private fun ReaderContent(
},
actions = {
val currentArticle = article
- // Only offer the toggle when there's an actual feed-provided alternative to
- // switch back to (if we already had to auto-extract for thin feed content,
- // reader mode IS the only content) and the feed hasn't opted out of it.
- if (feedContent != null && feed?.readerModeDisabled != true) {
- IconButton(enabled = !isExtracting, onClick = { toggleReaderMode() }) {
+ // Sets reader mode for the whole feed (persisted), not just this article --
+ // shown whenever the article belongs to an actual subscribed feed.
+ if (feed != null) {
+ IconButton(enabled = !isExtracting, onClick = { toggleFeedReaderMode() }) {
Icon(
- if (isReaderMode) Icons.Filled.Article else Icons.Outlined.Article,
- contentDescription = "Toggle reader view"
+ if (feed?.readerModeEnabled == true) Icons.Filled.Article else Icons.Outlined.Article,
+ contentDescription = if (feed?.readerModeEnabled == true)
+ "Reader mode enabled for this feed -- tap to disable"
+ else
+ "Reader mode disabled for this feed -- tap to enable"
)
}
}