|
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.
|
|
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.
|
|
The data model and FilterRuleEngine already supported a per-feed scope
(FilterRuleEntity.feedId), but Settings' "Add rule" form never let a
user set it -- only a hardcoded migration had ever created one. Add a
FeedScopePicker so a rule can target "All feeds" (unchanged default)
or one specific feed, and show each existing rule's scope in the list.
Added the first real unit tests in this repo (FilterRuleEngineTest),
covering global vs. feed-scoped matching and mute-wins-over-highlight
priority -- the exact behavior this UI gap left unexercised outside
one migration-seeded row. First real jacoco coverage number: 2.31%.
While verifying, found the pre-commit hook's coverage step was
silently a no-op: it pointed at app/build/reports/jacoco/report.xml,
but the jacoco setup that landed here uses the standard Gradle
JacocoReport task, which writes to
reports/jacoco/jacocoTestReport/jacocoTestReport.xml instead. Fixed
the path so the gate actually reads real numbers now.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017GMEkHeqKz6FLkmizowBTK
|