summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt
AgeCommit message (Collapse)Author
37 hoursMake an image zoomable even when the feed didn't spell out where it livesPeter Stone
Tapping an image in a Ftrain article did nothing. The zoom affordance was there, and the rendering path was the ordinary one -- the feed's own HTML, with reader mode off -- but a tap went nowhere at all. Tap detection works by wrapping every article image in a link to a scheme nothing can navigate to, which the WebView then hands to shouldOverrideUrlLoading. That href has to carry the complete image URL on its own, so an image whose src was relative got no wrapper and no tap: better an image that can't be zoomed, the reasoning went, than a link that goes nowhere. Ftrain's <content:encoded> is entirely site-relative -- every image in it is src="/art/uploads/..." -- so not one of them was zoomable. That reasoning had an unexamined premise. The page itself resolves those srcs perfectly well, against the base URL loadReaderHtml already hands loadDataWithBaseURL: the article's own link. Resolving them the same way at HTML-assembly time produces exactly the URL the WebView went and fetched, so the wrapper can be built after all. Both reader surfaces now pass that base URL down to the zoom pass. Where there genuinely is no base URL to resolve against -- nothing else in the app loads reader HTML without one today -- the old behavior stands and the image stays un-zoomable rather than acquiring a dead link, and a data: or javascript: src is still refused however it was reached, since the viewer could never open one. This is why the report could only have come from a feed with reader mode off. Reader mode never hits it: Readability rewrites every src to an absolute URL before the extracted HTML reaches us, which is also why the same images zoom fine the moment the feed's reader toggle is on. TDD: site-relative and path-relative resolution, and the whole trip through wrapReaderHtml, were written first against the unused parameter and went red on assertion mismatches (3 of 38 in ImageZoomTest), green on the resolution itself. The data:-src case was written at the same time and passed immediately -- it pins existing behavior against the new code path rather than having driven it. Verified on the emulator against the reported article itself, subscribed live from ftrain.com/feed.xml with reader mode off: on the previous build, a tap on the header image left the screen byte-identical; on this one the same tap at the same point opens the full-screen viewer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
2026-10-21Fix stray promo headers, site-URL subscribing, and YouTube playbackPeter Stone
Three reported bugs. **"West Hawaii today articles get a bunch of stray Trending headers."** The publisher's widget platform injects promo blocks between the real paragraphs, inside the article's own container, so every structural signal tells Readability they belong. What survives is a heading labelling nothing, because the links under it are filled in by a script that never runs in the reader. Confirmed on 9 of the 12 articles in that feed, two each. Blocks marked `data-mrf-recirculation` are now removed -- the attribute says what the block is for in the widget platform's own words, which is a better signal than the word "Trending", since an article may legitimately discuss what is trending. Nothing longer than a label and a few headlines is ever removed. Also applied to already-cached articles, in place rather than by discarding them: the article was fine, only the litter has to go. **"Can't subscribe to heathercoxrichardson.substack.com."** Adding a feed handed whatever URL it was given straight to the XML parser, so a site's home page -- the only URL a reader can actually see and copy -- failed on the root element. That page advertises its feed the way sites have since 2002, in a <link rel="alternate"> in the head. It is now read, resolved against the page, and fetched. Only what the page declares: guessing /feed and /rss would add a burst of 404s to every failed add and eventually subscribe somebody to the wrong thing. **"YouTube play is misconfigured."** It was, and YouTube said so: the player dialog loaded the embed as its own top-level document, which answers with "Video player configuration error -- Error 153". An embed has to be embedded. It now sits in an iframe on a minimal host page. Measured in the app's own WebView: top-level, no requests for the video stream and the error on screen; framed, the stream loads and there is no error. **"Long click on YouTube placeholder should open the URL directly."** It does now. Worth recording why this is not simply the link's href: for an image inside a link, WebView's hit test reports the image's URL, so a long-press on the thumbnail arrives as the i.ytimg.com JPEG. The video id is resolved out of that. Verified on a device for all four: the Substack site URL adds "Letters from an American" and fetches 20 articles; a West Hawaii Today article extracts with no Trending headers and its body intact; the video plays; and a long-press reaches the YouTube app. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
2026-10-12Stop reader mode passing a site's cookie notice off as the articlePeter Stone
Reported live: reader mode on a New Republic feature showed nothing but the cookie disclaimer, and had cached it. Reproduced outside the app by running ReaderExtractor's own injected script (Readability.js and all) against the real page. With the article body present, extraction is perfect. With the body absent from the DOM at the moment the script runs, Readability's first pass -- which discards "unlikely candidates", and the Didomi banner's class is didomi-notice-banner -- comes back under its own 500-character threshold, so it retries with that stripping switched off. The banner's single paragraph is 510 characters, i.e. just over the threshold, so it comes back as the article with the right title attached. Nothing on the Kotlin side looked at that. Any non-blank content counted as ExtractionResult.Success, was written to articles.extractedContent, and was never reconsidered -- so one bad load became a permanent cookie disclaimer, which is the "extractedContent cached: true" in the report. ConsentNotice recognises such a result: a hard plain-text length cap first (nothing long is ever rejected, however much consent boilerplate it quotes), then either a known CMP container id -- Readability strips classes but keeps ids -- or two or more distinct consent phrases. The accept/retry/give-up decision moves out of the WebView callback into decideEvalOutcome so it can be tested without a device. A consent notice is now "not ready yet", which routes it into the retry the extractor already does for an empty result; if it survives all six attempts that is what the page serves, and it fails as ExtractionFailure.ConsentInterstitial rather than being accepted. A fixed extractor cannot repair what is already on disk, so loadCachedExtraction drops a cached notice on read and re-extracts, in both ReaderScreen and LinkReaderScreen. No schema change: the repair happens when the article is next opened. Reader mode is still never turned on heuristically -- replacing a discarded notice does not flip it. The bug report now also carries the last extraction error, without which a report about bad reader content cannot say whether extraction failed. Tests: 59 -> 78. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
2026-09-29Reflow reader text live while pinching, and let a zoomed image panPeter Stone
Pinching in the reader used to scale the whole WebView like a photo and snap back to the real font size on release. It now drives WebSettings.textZoom as the fingers move, so the text itself resizes and reflows during the gesture with nothing to snap back from; images and margins stay put. The reload at the new saved size is what returns textZoom to 100%, so the two never overlap -- and loading reader HTML now goes through ReaderWebView.loadReaderHtml, which does both together, so a future load site can't forget the reset. Reader view for a tapped link now honors the saved font size and the same pinch gesture, instead of always rendering at the 18px default. A zoomed image can be panned again. The viewer computed its pan bounds from values captured when the dialog first composed -- an empty viewport around a not-yet-decoded image -- because a pointerInput(Unit) block keeps the lambda it was created with. Those bounds allowed exactly one offset, 0, so zooming worked and dragging did nothing. The whole pinch/pan step is now one pure function the gesture callback calls with values it reads fresh each time.
2026-09-18Tap an article image to open it full-screen, with pinch/double-tap zoomPeter Stone
Article images could be looked at but never enlarged -- a photo or a chart in a feed was stuck at whatever size the reader column gave it. Tapping one now opens a full-screen viewer: pinch to zoom (up to 6x), double-tap to toggle between fitted and 3x, drag to pan while zoomed, and dismiss with back, the close button, or a swipe down. Detecting the tap is the whole difficulty. The reader is a WebView, and its JavaScript is off deliberately (untrusted article content), so no in-page script can notice a tap on an <img> and report it out. WebView's own built-in zoom isn't the answer either: pinching in the reader is already taken, on purpose, by the reader-font-size gesture, and page zoom scales the article's whole layout rather than giving an image the screen. So wrapReaderHtml now wraps every image that isn't already inside a link in a link to a scheme nothing can navigate to (feedreader-image:<url>), which WebView hands to shouldOverrideUrlLoading -- the interception point that already routes ordinary links and YouTube thumbnails. The new onImageTap callback opens ImageZoomDialog instead of following it. Images that already sit inside a link are left alone: ReaderExtractor's synthesized YouTube thumbnail is exactly that shape, and re-wrapping it would swallow the tap that plays the video. Those stay zoomable from the other side -- a link pointing straight at an image file is also treated as a zoom target, which additionally fixes WordPress's default "link to media file" markup sending a tap off to LinkReaderScreen to run Readability over a JPEG. The long-press-link handler strips the scheme back off, so the actions menu still offers the real image URL. The viewer is Compose + Coil (AsyncImage), matching what the article list's thumbnails already use, so a just-scrolled-past image usually comes straight from Coil's cache; zoom is a graphicsLayer transform, not a re-decode. Panning is bounded by where the image actually is rather than by the viewport -- ContentScale.Fit letterboxes anything that isn't the screen's aspect ratio, and bounding by the viewport would let a wide photo be dragged up into its own black bar. Coil reports the intrinsic size once decoded; until then the viewport is the fallback bound. TDD: the HTML rewriting (linkifyImagesForZoom), the tap-target decision (zoomableImageTarget), and the zoom/pan math (imageZoomScale, imageZoomDoubleTapScale, clampImagePanOffset, fittedImageSize) were all written test-first against stubs, red on assertion mismatches, green on the real implementations -- 26 new cases in ImageZoomTest, including three over wrapReaderHtml itself to pin the ordering that keeps the YouTube thumbnail's link intact. Gesture wiring and the dialog itself are Compose UI, untestable here for the same reason the rest of ui/ is. Coverage is unchanged: ui/ is excluded from the Jacoco metric by design.
2026-09-15Support pinch-to-zoom for reader text size in the article viewPeter Stone
Pinching in ReaderScreen's article WebView now adjusts the same persisted reader font size the Settings slider controls, instead of doing nothing (WebView's own page zoom was never enabled, so a pinch previously had no effect at all). A ScaleGestureDetector is fed through setOnTouchListener, which always returns false so it never consumes the touch -- WebView's existing one-finger scrolling, link taps and long-press-link handling are unaffected, since ScaleGestureDetector only fires once a second pointer is down. During the gesture the WebView is scaled live via View.scaleX/ scaleY for immediate visual feedback; at gesture end the scale resets to 1x and the computed, clamped font size is persisted, which drives ReaderScreen's existing content-reload-on-fontSizePx-change path to re-render the actual HTML at the new size. TDD: pinchZoomFontSizePx (the pure scale-factor -> clamped-px calculation) was written test-first, red on a TODO() stub, green on the real implementation -- covered by SettingsTest. newReaderWebView grew from 6 to 8 parameters to carry the new gesture hooks, which pushed it over detekt's LongParameterList threshold; bundled all of its callbacks into a ReaderWebViewCallbacks data class rather than baselining past it, updating both call sites (ReaderScreen, LinkReaderScreen). ReaderContent gained no new parameter -- it now independently remembers its own Settings instance to persist the pinch-committed size, matching how it already remembers its own db and scope rather than threading them from ReaderScreen. LinkReaderScreen's WebView is untouched: it doesn't read the reader font size setting at all today, so wiring pinch-zoom there would be a separate, unrelated fix.
2026-08-22In-app YouTube playback, shared ConfirmDialog, hardened link handlingPeter Stone
- ReaderWebView/LinkReaderScreen/ReaderScreen: tapping a YouTube link inside an article now opens an in-app player (youtube-nocookie.com, JS scoped to just that dialog) instead of handing off to the YouTube app/browser. Falls back to external-open when a confident video ID can't be extracted (playlist/channel links). - A subframe (ad/embed iframe) auto-navigating is now blocked outright instead of being treated like a real link tap -- it fired the exact same callback as a user tap, with no interaction required. - wrapReaderHtml can now show a fallback hero image + date when the feed's own body HTML has no image of its own. - ConfirmDialog: the generic "confirm before doing something irreversible" dialog shape was independently rebuilt from scratch at FeedListScreen's unsubscribe and ArticleListScreen's mark-read-from- here confirmations. Both now share one composable; the latter also chunks its batch update (markReadBatchChunked) so a long-unread feed can't exceed SQLite's per-statement bound-variable limit.
2026-08-22Adopt shared saveArticleToStash in ReaderScreen and LinkReaderScreenPeter Stone
Replaces the 4 remaining copy-pasted StashClient-and-branch call sites (ReaderScreen x2, LinkReaderScreen x2) with the shared helper added in the previous commit.
2026-07-25Text-only bottom bar, re-extract action, fix YouTube thumbnails, verify ↵Peter Stone
Stash creds before saving - Bottom bar: Previous/Keep unread/Next are now plain text labels, no icons - Add "Re-download and re-extract" to the overflow menu (ReaderScreen and LinkReaderScreen) to force a fresh fetch + Readability pass on demand, overwriting whatever's cached - Fix a real compile bug in the previous commit's reader-mode-off handler (passed null into a non-null setExtractedContent) via a proper clearExtractedContent() DAO query - YouTube embeds: fixed the broken-image-glyph bug two ways -- (1) an 11-char video-ID regex was accidentally matching a playlist embed's literal "videoseries" placeholder path, building a thumbnail URL for a video that doesn't exist, and (2) Readability's link-density cleanup was discarding the replacement entirely (a bare <a><img></a> reads as an ad/share unit). Restructured as a <figure>/<figcaption> pair -- the same shape a normal captioned photo takes -- confirmed live this survives extraction intact - Stash settings: API key field is now a password field with a show/hide toggle, and "Verify and save" makes a real authenticated request before persisting anything, showing why it failed instead of silently saving bad credentials Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
2026-07-24Collapsible folders, long-press link menu, YouTube thumbnails, strip more ↵Peter Stone
Ars sequel chrome - Feed list: folders now collapse (start collapsed, with a per-folder unread count so collapsing doesn't hide "is there anything new") - Long-pressing a link inside an article shows an action menu (open in browser, share, save to Stash, save for later) instead of WebView's default text-selection popup -- confirmed live end to end, including the Stash/save-for-later paths creating a real standalone article - New shared ensureStandaloneArticle() helper (data/ArticleSaving.kt), factored out of ShareReceiverActivity, now also used by the long-press menu - YouTube iframe embeds become tap-to-open thumbnails (JS is off in the reader WebView, so the real player never worked anyway); tapping one goes straight to the YouTube app/browser instead of our own article-extraction pipeline - Fixed back navigation through a chain of links-within-links: each hop was stacking a new backstack entry, so back stepped through the chain one link at a time instead of returning straight to the source article. Confirmed live: A -> link -> B -> link -> C, one back-press now goes straight to A - Extended the <header> chrome-stripping from last round: also strip <footer>, an author bio card, and a "Most Read" trending-stories widget -- confirmed live one of the latter showed up as a giant unrelated photo plus an empty numbered list (its lazy-loaded items never populate in this hidden, off-screen extraction WebView) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
2026-07-23Strip <header> chrome before Readability parses; show full article title in ↵Peter Stone
reader content - Confirmed live (Ars Technica): a section-kicker badge, the dek/subhead, and an orphaned duplicate hero-image caption were all leaking into extracted content because they live in a <header> Readability doesn't exclude by tag, only deprioritizes in scoring. Strip <header> from the DOM ourselves before handing off to Readability -- a pre-processing decision on our side, not a change to Readability's own algorithm, per instruction not to touch it. - Render the article's full title as a heading at the top of the reader content itself (wrapReaderHtml), not just the top bar, which truncates to one line. This also replaces whatever heading Readability happened to keep in-body, now that <header> (and any <h1> inside it) is stripped pre-parse. - Bump extraction timeout 15s -> 25s: confirmed live, several heavy real-world pages were timing out before onPageFinished ever fired, well short of a real parsing failure. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
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-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.