summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader
AgeCommit message (Collapse)Author
2026-07-23Rebrand to FeeDreader; overflow-menu cleanup; deferred read-marking; ↵Peter Stone
folder-organized feed list - Rename app to FeeDreader with an owl launcher icon and a paywall-avoidance note in Settings - Move Open original/Save to Stash into the reader's overflow menu; add Mark unread there too - Only mark an article read when navigating away via Previous/Next, not on open or on back - Saved list no longer crosses out read items; its count includes them - Back from Unread (the root screen) now opens the feed list instead of exiting the app - Feed list: clearer separator above the per-feed list, folder grouping (from OPML categories) with case-insensitive sort, and a virtual "Last fetch failed" section at the top - Reader extraction: cap unsized decorative SVG icons (fixes Ars Technica's giant section-badge icon), force native loading="lazy" images to load eagerly in the hidden extraction WebView, and drop a lightbox's duplicate caption only when a real figcaption sibling exists - Backlog notes on swipe-to-next research, extraction test coverage, and an async-rendered-gallery race confirmed live on Ars Technica Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
2026-07-23Drop swipe-to-next-article; bottom Previous/Next bar; smaller titles; bug reportPeter Stone
- Dropped swipe-to-next-article entirely, per explicit instruction. Two different fixes for the gesture interfering with WebView scrolling had already been tried and didn't resolve the reported jerkiness (one even broke swipe outright and was reverted) -- rather than keep tuning gesture detection, removed the gesture altogether. - ReaderScreen: new bottom bar with explicit Previous/Next buttons (disabled at the ends of the article queue), replacing swipe. This is the first slice of the "bottom action strip" backlog item. Verified live: Previous/Next correctly page through the article queue with the right enabled/disabled state at each end. - Smaller title text: reader top bar title (titleMedium, was TopAppBar's default titleLarge) and article list row titles (bodyMedium, was ListItem's default bodyLarge). - New "Report bug" action in the reader's overflow menu: a dialog for a short description, which gets bundled with the article's title/URL/id, feed id, reader-mode state, and save/Stash state into one text block copied to the clipboard on confirm (verified live, including the "Copied to clipboard" confirmation) -- enough to reproduce an issue in the tungsten emulator without needing live device access. - BACKLOG.md: marked the done slice of the bottom-strip item, added a note on the scroll-jerkiness investigation so far (two ruled-out theories) since swipe removal may or may not affect it -- worth a fresh look now that nothing intercepts touches above the WebView at all.
2026-07-23Fix crash on blank-guid articles; simplify browser back; backlog notesPeter Stone
- FeedParser: a feed with a literal empty <guid>/<id> element (confirmed live: a Mother Jones entry) produced an ArticleEntity with id = "" -- the fallback chain only checked for null, not blank, so an empty-but- present tag passed straight through. An empty-string primary key broke Navigation Compose's route matching and crashed on tap ("tapping this article crashes the app"). Fixed both RSS and Atom paths to treat blank the same as null before falling back to a link-based id. Schema v5 migration deletes any row already on-device with the bad id; it'll come back with a real, hash-based id on the next fetch of that feed. Verified live: zero blank-id rows after the migration and a fresh fetch, no crash. - BrowserScreen: back (both the system gesture and the in-app arrow) now always closes the screen directly, rather than stepping through in-page WebView history first. Reported live as not making sense for multi-page browsing (search results -> article -> another link, etc.) -- you'd have to tap back that many times just to leave. There's no separate in-page-back affordance now, matching how every other screen in this app behaves. - Tried moving swipe-to-next-article detection off Compose's gesture system entirely (a passive native OnTouchListener on the WebView, never consuming) on the theory that Compose's gesture arbitration was the source of reported scroll jerkiness. Reverted after confirming live that swipe stopped working at all with that approach -- a worse regression than unproven jerkiness. Back to the known-working Compose-based detector; the jerkiness complaint itself is still open. - BACKLOG.md: added bug-report-button, smaller-title-text, and bottom-action-strip-with-auto-hide notes.
2026-07-22Remove reader-mode heuristics entirely; explicit per-feed opt-in onlyPeter Stone
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.
2026-07-22Resolve lazy-loaded images/media before running ReadabilityPeter Stone
Many sites lazy-load media via JS: the real URL sits in a data-src/ data-srcset attribute and only gets copied into src/srcset once the site's own script runs -- which never happens in the reader WebView (JS is off by design, so articles can't run arbitrary third-party scripts). Left alone this showed either a placeholder/category icon stretched to fill the article width (reported live with a screenshot: a giant flask icon in place of the real photo on an Ars Technica article), or nothing but alt text (reported for NYT articles' images and videos). Before either Readability pass, walk <img>/<source> elements and copy common lazy-load attributes (data-src, data-original, data-lazy-src, data-srcset) into src/srcset, falling back to the first srcset candidate if src is still empty. Also strip width/height attributes on images -- they're sized for the site's real photo, not whatever was in the unresolved src, so our own max-width:100%/height:auto sizing now takes over based on the (correctly resolved) image's actual dimensions. Verified live: the Ars Technica article that previously showed the stretched flask icon now renders clean, with no image where the broken one used to appear and the full real article text intact.
2026-07-22Fix extraction regression on kottke; add Older view, mark-read-from-herePeter Stone
- ReaderExtractor: the un-hide-before-extracting fix (for soft paywalls like West Hawaii Today) was unconditional -- it also un-hid normal, legitimately-collapsed chrome (kottke's footer/membership notice), which Readability then folded into the result alongside the real article. Reported live with a screenshot showing the extracted content wrapped in site footer boilerplate. Now tries the normal (hidden-respecting) pass first and only falls back to the un-hide pass if that came back thin (<500 chars), keeping whichever pass actually found more text. Verified live: the same kottke article now stays clean and stable (no delayed switch to the footer version). - New Unread/Older split: the default Unread view is now clamped to the last 72 hours (RECENT_WINDOW_MS); the rest of the unread backlog moved to a new "Older" view (new FeedListScreen row, ArticleDao.observeUnread now takes a cutoff, observeOlderUnread/observeUnreadCount/ observeOlderUnreadCount added). Undated articles are kept in the main Unread view rather than silently dropped from both. Verified live: 150 recent / 1172 older, summing to the prior combined total. - "Mark read from here": long-press an article row to mark it and everything older in that same filtered list as read in one batch (re-added ArticleDao.markReadBatch, this time as an explicit, confirmed action -- not the implicit on-scroll version that was reverted earlier). Verified live: confirmed a 147-article batch: the tapped article and older were marked read, newer ones above it were not. - BACKLOG.md: added a YouTube-embeds note (JS is off in the reader WebView, so embeds currently show broken -- rough shape is a tap-to-play thumbnail rather than turning JS on for third-party content).
2026-07-22Un-hide CSS-gated content before running ReadabilityPeter Stone
Some "soft" paywalls (West Hawaii Today's included) deliver the full article in the initial HTML response to every visitor, then hide it with CSS (inline or via stylesheet) pending a client-side subscription check -- the content itself isn't withheld by the server, only its visibility. Readability has a built-in heuristic that skips anything hidden this way, treating it as boilerplate, which is exactly what was discarding real, already-delivered article text and leaving only the visible teaser/ subscribe prompt. Before parsing, walk the live document (getComputedStyle needs an attached node) and force display/visibility back on for anything hidden via display:none or visibility:hidden, then clone and hand that off to Readability as before. Verified live against a real West Hawaii Today article: extractedContent went from 588 characters (the RSS teaser) to 3458 (the full body).
2026-07-22Fix scroll regression and feed-setting reset on re-importPeter Stone
- ReaderScreen: the direction-ratio gesture rewrite (using a generic, non-oriented detectDragGestures to compute a custom dx/dy ratio) broke scrolling entirely -- reported live as "the article view doesn't scroll at all anymore." Root cause: detectDragGestures' touch-slop detection is isotropic (claims the first bit of movement in ANY direction before it knows which way the gesture is going), unlike detectHorizontalDragGestures which is orientation-aware and simply never engages for a vertical- dominant drag. Reverted to detectHorizontalDragGestures, keeping the distance-based (not edge-anchored) commit threshold from the previous fix. Verified live: a long article now scrolls properly on a diagonal swipe, and a deliberate horizontal swipe still changes pages. - AddFeedScreen: both the OPML import and single-URL add built a bare FeedEntity(id, title, siteUrl) and upserted it (REPLACE) even when a feed with that id already existed -- silently wiping readerModeDisabled (and etag/lastModified/lastError) back to defaults on every re-import or re-add. This is exactly what made a feed's reader-mode toggle appear not to persist. Now preserves the existing row via .copy() when one exists. Verified live: set kottke.org's readerModeDisabled=1, re-imported the same OPML file (107 feeds), and confirmed the flag was still 1 afterward.
2026-07-22Remove mark-as-read-on-scrollPeter Stone
Explicitly asked to revert: scrolling the article list shouldn't mark anything read on its own -- only opening an article (or the existing manual actions) should. Removed the LazyListState/snapshotFlow tracking in ArticleListScreen and the now-unused ArticleDao.markReadBatch. Verified: unread count in the DB is unchanged after scrolling through the list (1332 before and after).
2026-07-22Fix accidental page-swipe while scrolling; bring linked-article reader to parityPeter Stone
- ReaderScreen: HorizontalPager's full-surface drag detection was treating ordinary vertical scrolling inside the embedded WebView (a native View, which doesn't arbitrate with Compose's gesture system the way a pure-Compose scrollable child would) as an attempted horizontal page-turn on anything less than a perfectly vertical swipe -- i.e. most real thumb scrolling. Tried gating it to edge-zone-only drags first, but that collides with Android's own edge-swipe-back gesture on gesture-nav devices (reproduced live: an edge swipe triggered system back, not the pager). Landed on disabling the Pager's built-in gesture and driving it from a custom detector that works anywhere on screen but only commits to a page turn once a drag is clearly horizontal-dominant (>2x the vertical movement) -- ordinary scrolling, even sloppy/diagonal, is left completely unconsumed for the WebView. Verified live: a deliberately diagonal mid-screen scroll now stays on the same article; a decisive horizontal swipe still changes pages. - LinkReaderScreen: was missing the bookmark/Stash/reader-toggle/menu actions entirely -- reported after tapping a link left "all my icons... gone." Brought to parity with the main article reader: save locally and save to Stash now lazily create a standalone ArticleEntity (feedId=null, id = hash of the URL, same scheme ShareReceiverActivity uses) on first use; reader-mode toggle falls back to the live page (JS on) instead of always forcing extraction; same login/find-another-source menu. Verified live: all 5 icons present once extraction finishes, bookmarking a linked article correctly creates a standalone DB row.
2026-07-22Add site login and alternate-source search for paywalled articlesPeter Stone
Deliberately not a paywall-bypass mechanism -- that space (archive.today-style proxies, "Bypass Paywalls" extensions) is legally grey, adversarial toward publishers, and constantly breaking. Instead: - BrowserScreen: a real interactive mini-browser (JS + DOM storage on, ordinary navigation, no Readability interception) for the two cases where the user needs to actually USE a page rather than read it. Reached from a new overflow menu on the article reader: - "Log in to this site" opens the article's own URL there so the user can sign into a site they already subscribe to. Needs no extra plumbing: Android's CookieManager is one process-wide store shared by every WebView in the app, so a session cookie set here is picked up automatically by the reader's (JS-off) extraction WebView on the next visit to that domain -- same as a normal browser remembering you're signed in. - "Find another source" runs a web search on the article's title in the same browser, so the user can pick a different, non-paywalled outlet covering the same story themselves -- plain search/discovery, not circumvention. Verified live: search opened a real, interactive Google results page with working title-bar sync; back navigation (via BackHandler, goBack() when the WebView has history, else pop the screen) returned cleanly to the article.
2026-07-22Split save/Stash actions, follow links in-app via reader pipeline, tune ↵Peter Stone
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.
2026-07-22Smooth scrolling, dark reader, mark-read-on-scroll, feed names, per-feed ↵Peter Stone
reader-mode opt-out - ReaderScreen: AndroidView's update() was reloading the WebView's HTML on every recomposition (e.g. isSaving flipping mid-read), resetting scroll position -- this is what made scrolling feel unsmooth. Now only reloads when the content string actually changes. - ReaderScreen: reader content now uses a hardcoded dark palette instead of browser-default white, matching the app's always-dark theme. - ArticleListScreen: mark-as-read-on-scroll -- once an article has scrolled fully above the viewport it's marked read, the standard feed-reader behavior, via a new batched ArticleDao.markReadBatch(). Verified against the on-device DB (scrolled-past articles flipped to isRead=1, the still-visible one stayed unread). - ArticleListItem/queries: LEFT JOIN feeds for a feedTitle column, shown per row in the Unread/Saved aggregated views and as a reader subtitle; the per-feed article list's top bar now shows the real feed title instead of a generic "Articles". - FeedEntity.readerModeDisabled (schema v3, migration 2->3): per-feed opt-out of the Readability fallback/toggle, toggled from a new icon in FeedListScreen. Added for kottke.org specifically -- confirmed its thin linkblog posts were making Readability extract the page footer instead of the actual post; disabling reader mode for that feed now shows its own (short but correct) content instead. All verified live on the emulator against the real 107-feed dataset.
2026-07-22Fix crash loop: WebView touched off main thread during extraction cancellationPeter Stone
Root-caused via emulator stress test (rapid swiping through many articles): withTimeoutOrNull's cancellation, and LaunchedEffect disposal when a page is swiped away, both invoke invokeOnCancellation on kotlinx.coroutines.DefaultExecutor rather than the calling coroutine's Main dispatcher. The handler called webView.stopLoading()/destroy() directly, crashing with "A WebView method was called on thread ..." on essentially every session once enough thin-content articles were swiped past mid-extraction -- the reported crash loop. Now hops back to the main looper before touching the WebView, and guards against a double destroy() race between the JS-callback and cancellation paths. Also fixed a real (non-crashing) leak: the visible reader WebView in ReaderScreen was never destroy()-ed when its page left the pager, via AndroidView's onRelease. Reproduced pre-fix in ~80 rapid swipes on an emulator; post-fix, 320+ swipes with real 107-feed data ran clean with stable native heap.
2026-07-22Fix Unread/startup crash; add reader-mode toggle and swipe navigationPeter Stone
- Root cause of the reported crashes: observeUnread/observeByFeed/ observeSaved used SELECT *, pulling feedContentHtml/extractedContent (potentially large per-article blobs) just to render a list row. With 107 feeds worth of real articles this blows past Android's CursorWindow/memory limits, scaling with item count -- and since Unread is now the landing screen, it crashed at startup too. Added an ArticleListItem projection excluding the heavy content columns; list queries now only select what a row actually renders. - FilterRuleEngine now takes a small Titled interface (title/feedId) instead of the full entity, so it works with both ArticleEntity and the new lighter projection. - Reader-mode toggle: a button in the reader to manually switch between feed-provided content and Readability-extracted full text, matching Inoreader's full-text-view toggle (previously only an automatic fallback when feed content was thin). - Swipe between articles: ReaderScreen now wraps a HorizontalPager over a snapshot of the current list's article-id order, so swiping moves to the next/previous article. Snapshotted rather than live, since marking an article read would otherwise drop it from the live Unread query mid-swipe and shift every later page underneath the user. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
2026-07-22First round of testing feedback: images, unread landing, dark, deletePeter Stone
- Fix OPML import status message reporting the raw pre-dedup count (144) instead of the actual unique feed count after Room's upsert collapses duplicates (108) -- cosmetic bug, the DB was already correct. - Article thumbnails: extract media:thumbnail/enclosure or the first <img> in feed content, show via Coil in the article list. - App now lands on Unread instead of the feed list; a top-bar icon gets from there to the feed list (forward nav, not back-stack pop, since Unread is now the root). - Dark color scheme by default. - Delete-feed action (with confirmation) on each feed row -- was entirely missing. Deletes its non-saved articles too, so they don't linger as orphaned rows in the cross-feed Unread view; saved articles are kept regardless. - Room migration 1->2 for the new imageUrl column (real migration, not destructive fallback, since there's now real on-device subscription data from testing). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
2026-07-22Initial commit: Feeds, an Android RSS readerPeter Stone
Feed subscriptions (RSS 2.0 + Atom, hand-rolled parser), OPML import/export, WorkManager periodic fetch with conditional GET, Room-backed local storage (feeds/articles/filter rules), keyword mute/highlight filtering. Reader mode: on-device extraction via Mozilla's Readability.js (vendored from github.com/mozilla/readability, pinned to release 0.6.0) run in a hidden WebView, for feed articles whose provided content is too thin to be the real article body. Save: explicit action (from a feed article, or the Android share-sheet target for any URL) archives to Stash via POST /artifacts?sync=true, while a local on-device copy (Readability-extracted) is always kept so saved articles read offline regardless of Stash's reachability. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL