| Age | Commit message (Collapse) | Author |
|
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.
|
|
- 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).
|
|
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).
|
|
- 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.
|
|
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).
|
|
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
- 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
|
|
- 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
|
|
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
|