Skip to content

Feature/extraction error log misc#155

Merged
NateEaton merged 13 commits into
mainfrom
feature/extraction-error-log-misc
May 19, 2026
Merged

Feature/extraction error log misc#155
NateEaton merged 13 commits into
mainfrom
feature/extraction-error-log-misc

Conversation

@NateEaton
Copy link
Copy Markdown
Owner

Summary

Pile of related work that grew from a single "extraction error log" focus into a broader set of bookmark-details, reader, and swipe-gesture improvements:

  • Bookmark details: extraction-error box + log viewer; improved thumbnail handling.
  • Reader top-bar: Pixel-Tablet title clipping fix, plus several rounds of stabilising the clearance so it doesn't reload the WebView during scroll.
  • Swipe actions: new vertical-intent fence that prevents accidental swipe commits when the user's gesture starts as a scroll.
  • Drawer: fix for a regression where the nav drawer couldn't be closed (scrim-tap or swipe-close) once swipe actions were enabled.
  • Highlights date fast-scroll: shelved (see notes below). Preserved at tag wip/highlights-fast-scroll.
  • Specs / docs: existing spec docs reorganised; new specs added for follow-up features (multi-select, compact-list fast-scroll prototype).

12 commits, ~2,240 line additions, ~26 deletions across code, tests, locale strings, user-guide markdown, and design specs.


Bookmark details

Commits: b536b52, b5b3350

  • Added an extraction-error box in the bookmark-details dialog that surfaces server-side extraction failures, with a tap-to-expand log viewer for the most recent extraction log lines.
  • Wired through BookmarkRepository / Bookmark model / ReadeckApi to carry the new fields.
  • Improved thumbnail display in the same dialog — better fitting and aspect handling for the bookmark's preview image.
  • New unit tests in BookmarkRepositoryImplTest and BookmarkDetailViewModelTest covering the new fields and dialog state.

Specs:

  • docs/specs/extraction-error-log-detail-panel-mini-spec.md (initial design)
  • docs/archive/extraction-error-log-detail-panel-mini-spec.md (post-ship archive copy)

Reader top-bar clearance

Commits: 93923270a1228b686c29d3543a0a

This area went through several iterations as edge cases surfaced during testing:

  1. 9392327 introduced onSizeChanged measurement of the rendered top bar to fix sub-pixel title clipping on Pixel Tablet.
  2. 0a1228b added a max-seen guard after testing showed the measurement was firing repeatedly during enterAlwaysScrollBehavior collapses, thrashing the WebView's CSS clearance spacer and causing scroll jumps (100%-read articles snapping to the bottom; fresh articles scrolling erratically).
  3. 686c29d dropped the measurement entirely once it became clear that the one-shot transition from "static fallback" to "measured value" on first composition was still causing a one-time WebView reload that would yank the user back to the top of the article if they were already scrolling. A static derivation with a 4dp safety cushion replaced it.
  4. 3543a0a bumped the cushion to 12dp after the Pixel Tablet emulator showed that 4dp wasn't enough to clear the M3 TopAppBar's internal padding on tablet form factors.

End state: topBarClearance is statically derived from TopAppBarExpandedHeight + WindowInsets.statusBars + 12.dp, stable across the screen's lifetime, so the WebView's article HTML is generated exactly once and never reloaded mid-scroll.

Spec: docs/archive/reader-top-bar-clearance-mini-spec.md.


Swipe-action gesture refinements

Commit: 5139d17

The original swipe-actions feature shipped on main with Compose's default touch-slop arbitration: whichever axis (horizontal or vertical) crosses platform touch slop first wins the pointer. That works for clean motion, but it lets a "scroll a bit, then move sideways within the same touch" gesture commit a swipe — because the late horizontal motion crosses slop before the vertical motion did.

This branch adds a per-pointer vertical-intent fence:

  • At the moment one axis first crosses touch slop, decide once: if dy >= dx, set verticalIntentVeto = true for the rest of that pointer's life.
  • The veto disables the draggable via its enabled flag, not by consuming events, so the parent LazyColumn still receives the vertical motion normally.
  • Locking the decision once (rather than re-checking each event) is intentional: if the veto could flip during an active drag, Modifier.draggable would observe enabled = false and call onDragStopped abnormally, sometimes committing a delete past the midpoint without going through the snackbar handshake.
  • The 45° boundary (dy >= dx rather than a 1.5× ratio) is also intentional: at exactly equal motion neither the inner draggable nor the outer scrollable can resolve dominance, and the gesture produces no motion at all. Biasing the equality case toward scroll resolves the deadlock and matches user intent — as much vertical as horizontal is too much vertical for a swipe.

Spec updates in docs/specs/swipe-actions-for-bookmark-cards-spec.md and arch-spec.md document the new behaviour.


Drawer regression fix

Commit: 2fa23dc

The Slice 4 drawer-gate change (already on main) set gesturesEnabled on ModalNavigationDrawer to a value that, when card swipe was enabled, correctly suppressed drawer-swipe-to-open — but also disabled M3's scrim-tap-to-close and swipe-to-close, leaving the drawer effectively unclosable except via menu item selection.

Fix: keep gestures live whenever the drawer is already open, by OR-ing drawerState.isOpen into the gate.

  • Drawer-swipe-to-open still suppressed when the drawer is closed on the bookmark list with swipe enabled.
  • Scrim-tap and swipe-close work whenever the drawer is open, on every screen.
  • Reader screen no longer receives drawer-swipe gestures (a second-iteration fix to a too-permissive intermediate version).

Highlights date fast-scroll — shelved

Commits: 6db1839 (implementation), e915446 (revert + spec preservation)

A Google-Photos-style date fast-scroll overlay for the highlights list was implemented and pushed to a snapshot build for a tester with thousands of highlights spanning years. The tester couldn't validate the feature, and there's no local dataset large enough to iterate on it.

Rather than ship unverified code, the feature was reverted. The plan is to first build the equivalent fast-scroll pattern for the main bookmark-list views (where there is test data), validate it there, then port back to highlights.

Preservation:

  • The original commit is tagged wip/highlights-fast-scroll (pushed to origin).
  • The spec doc docs/specs/highlights-date-fast-scroll-spec.md was kept in-tree, with a "Shelved" banner at the top pointing at the tag.

Documentation

Commits: b46f9cd, d46e6ac

  • Specs for landed features moved to docs/archive/.
  • New design specs added for follow-up work:
    • docs/specs/bookmark-compact-date-fast-scroll-prototype-spec.md — proposed first home for the fast-scroll pattern, where data is available.
    • docs/specs/list-multi-select-actions-spec.md — multi-select mode for the bookmark list (tablet/landscape batch UX).
  • .gitignore updates.

Test plan

  • Bookmark details: open a bookmark whose extraction failed on the server — extraction-error box visible; tap to expand log viewer.
  • Bookmark details thumbnails: open bookmarks with various aspect ratios — image fits cleanly in the dialog.
  • Reader top-bar clearance on phone: open an article on Pixel 9 — title's top edge is clearly below the bar; immediately scroll fast — no jump back to the top.
  • Reader top-bar clearance on tablet: open an article on Pixel Tablet emulator — title's top edge is clear of the bar.
  • Reader scroll behaviour: open a 100%-read article — tap top bar to scroll to top, then scroll down freely; no snap-back to the bottom.
  • Swipe actions vertical-intent fence: start a clear vertical scroll, then move sideways in the same touch — no swipe fires.
  • Swipe actions normal case: clean horizontal swipe with minor vertical wobble still commits past midpoint; partial swipes snap back.
  • Swipe at 45°: a perfectly diagonal gesture results in scroll, not swipe (and no deadlock).
  • Drawer-in-reader: open an article and swipe near the screen edge — drawer does not appear.
  • Drawer scrim-tap-to-close: open the drawer (hamburger), tap outside — drawer closes.
  • Drawer swipe-to-close: open the drawer, swipe right-to-left on the drawer surface — drawer closes.
  • Drawer swipe-to-open with swipe disabled: disable card swipe in settings, swipe from the left edge on the bookmark list — drawer opens.
  • Aggregate gradle: ./gradlew :app:assembleDebugAll :app:testDebugUnitTestAll :app:lintDebugAll all pass.

Notes for reviewers

  • The reader top-bar clearance area saw a lot of churn (four commits across this PR) as edge cases surfaced during real-device testing. The final state is the simplest of all the iterations and the trade-offs are documented inline.
  • The swipe vertical-intent fence is documented end-to-end in docs/specs/swipe-actions-for-bookmark-cards-arch-spec.md §3.1, including why the decision is locked once per gesture and why the boundary is at 45° rather than the more permissive 1.5× ratio.
  • The shelved highlights fast-scroll work is recoverable via git show wip/highlights-fast-scroll or by cherry-picking the tag. The spec doc explains both the original design and the plan to port the pattern from a future main-list fast-scroll prototype.

NateEaton and others added 12 commits May 17, 2026 20:19
When Readeck reports extraction errors for a bookmark, the details
panel now shows a collapsible error box (collapsed by default) above
the thumbnail. Tapping it reveals the error list, a help note, a
Learn more link to the Readeck extension docs, and a View log button
that fetches and displays the raw extraction log in a modal dialog
with copy-to-clipboard and a scroll progress indicator.

Fixes detection by mapping hasServerErrors through to the domain
model so bookmarks flagged via replaceServerErrorFlags (but with an
empty errors list) correctly trigger the error box.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace fixed 200dp crop with aspect-ratio-aware sizing. Uses
rememberAsyncImagePainter to reactively compute the natural height
at full card width, then applies one of three zones: crop up to
180dp minimum for wide/panoramic images, show full image at natural
height for normal images, or crop down to 50% screen height for
tall portrait images.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The reader HTML clearance was derived from TopAppBarExpandedHeight +
statusBars.top, which produced a spacer exactly equal to the bar height.
On Pixel 9 the title's line-box half-leading was enough to clear the
bar visually, but on Pixel Tablet sub-pixel rounding pushed the cap
height behind the bar. Measure the bar's actual rendered height via
onSizeChanged and add a 4 dp cushion so the title always clears.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Doc cleanup: add the mini-specs for the reader top-bar clearance fix
and the extraction error box + log viewer feature that shipped without
their accompanying spec files.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Slice 4 drawer-gate change set gesturesEnabled = false whenever
swipe actions were enabled on the bookmark list, which Material 3's
ModalNavigationDrawer also uses to gate scrim-tap-to-close and
swipe-to-close. The first attempt to keep gestures live with
|| !isOnBookmarkList overcorrected and re-enabled drawer-swipe-to-open
on every other screen, including the reader.

Restore the original bookmark-list-only swipe-to-open scope while
keeping gestures live whenever the drawer is open, so scrim-tap and
swipe-close work regardless of which screen the user is on.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The reader top bar uses enterAlwaysScrollBehavior, which collapses and
expands the bar as the user scrolls. The onSizeChanged callback added
in 9392327 fires on every collapse step, updating measuredTopBarHeightPx
each frame. That cascades into a recomputed CSS spacer for the WebView
and recomputed Compose padding, causing content reflow during scroll.
Read-progress restoration then yanks the scroll position around — most
visibly, a 100%-read article jumps back to the bottom on any scroll-down
attempt, and any article scrolls erratically.

Gate the measurement update so it only captures the bar's maximum
observed height, locking the spacer to the fully-expanded bar height.
The bar's collapse animation no longer feeds back into the WebView's
content height. Preserves the original tablet sub-pixel rounding fix
(measurement still wins over the static fallback) and the 4dp cushion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The implementation pushed in 6db1839 could not be validated locally —
we lack a highlights dataset large enough (thousands across years) to
iterate on the fast-scroll behavior, and the one tester with that
data couldn't confirm it worked. Rather than ship unverified code,
back out the feature.

The original commit is preserved at the tag wip/highlights-fast-scroll
for future revival. The spec doc is kept (with a shelved-status note
prepended) so the design intent and reference material remain
discoverable in-tree. The plan: build the same fast-scroll pattern
for the main bookmark list first, where we have test data, then
port the proven pattern back to highlights.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The max-seen guard added previously closed the continuous-thrash bug,
but one residual issue remained: between first composition (clearance
derived from the static fallback) and the post-layout onSizeChanged
callback (clearance derived from the measured height), readerTopClearance
CssPx changes value once. Because it keys the WebView's content remember
and LaunchedEffect, that one-step change regenerates the article HTML
and reloads the WebView. A user who starts scrolling immediately after
the article opens hits exactly the window where the reload yanks them
back to the top.

The original 9392327 commit introduced both a measurement and a 4dp
cushion to handle Pixel Tablet sub-pixel rounding. The 4dp cushion is
multiple orders of magnitude larger than any sub-pixel error — the
measurement is redundant. Drop the measurement, keep the cushion. The
clearance value is now stable from first composition forward, so the
WebView's article HTML is generated exactly once and never reloaded.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Compose's default touch-slop arbitration races the inner horizontal
draggable against the outer LazyColumn's vertical scrollable for the
first axis to cross slop. That works for clean motion but lets a
"scroll then sideways within the same touch" gesture commit a swipe
because the late horizontal motion crosses slop first.

Add a per-pointer intent fence that decides exactly once, at the
moment one axis first crosses platform touch slop, whether the gesture
is horizontal or vertical-dominant. If vertical-dominant (dy >= dx),
set verticalIntentVeto for the pointer's life so the draggable's
enabled flag stays false even if the user later moves cleanly sideways.

Locking once (rather than re-checking each event) is critical: if the
veto could flip mid-drag, Modifier.draggable would observe enabled=false
and call onDragStopped abnormally with whatever offset and velocity it
had, sometimes committing a delete past the midpoint without going
through the snackbar handshake.

The 45° boundary (dy >= dx) rather than a 1.5x ratio is also
deliberate: at exactly equal motion neither the inner draggable nor
the outer scrollable can resolve axis dominance on its own and the
gesture produces no motion at all. Biasing the equality case toward
scroll resolves the deadlock and matches user intent — as much
vertical as horizontal is too much vertical for a swipe.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The static derivation TopAppBarExpandedHeight + statusBars + 4dp was
sufficient on Pixel 9 but left the very top edge of article titles
clipped under the bar on the Pixel Tablet emulator. The M3 TopAppBar
apparently adds a small amount of internal padding on tablet form
factors beyond what TopAppBarExpandedHeight reports. 12dp comfortably
covers the gap.

The extra ~8dp at the top of articles on phones is negligible visually,
and the trade is worth it: keeping the value statically derived means
the WebView's HTML clearance spacer stays constant across the screen's
lifetime, so we don't reintroduce the mid-scroll content reload that
the measurement-based approach caused.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@NateEaton NateEaton self-assigned this May 19, 2026
@NateEaton NateEaton linked an issue May 19, 2026 that may be closed by this pull request
@NateEaton NateEaton added this to the v0.13.0 milestone May 19, 2026
The three coEvery/coAnswers blocks that mock performTransaction's three
generic instantiations cast invocation.args[0] to the matching
suspend lambda type. The casts are safe by construction (mockk wires
the right type into each generic call site) but Kotlin can't prove it
through the reflective args lookup, so the compiler emits Unchecked
cast warnings on each. Suppress locally with @Suppress("UNCHECKED_CAST")
and a one-line comment explaining the reasoning.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@NateEaton NateEaton merged commit a393b5f into main May 19, 2026
6 checks passed
@NateEaton NateEaton deleted the feature/extraction-error-log-misc branch May 19, 2026 02:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Details page Error block

1 participant