Repository navigation
The Binder: a CD binder home screen widget that flips through your books - #110
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
… app's own rules Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
…dget can carry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
…t up when you add it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
…d relock after a trial Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
… while the book loads Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
…ere you left it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
…ded corners Each sleeve is now the largest square its half of the widget can hold, and the two squares sit against the hinge. Covers are fitted whole instead of cropped and their corners are rounded to match the sleeves. The locked, empty and picker faces use the same square sleeves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
…cks the lock and library like Play A locked tap goes to Unlock without waiting for anything. A cold start that decides slowly closes the invisible handoff screen after 2 seconds and lets the play carry on in the background. "Open its page" now ignores a stale tap from another mode or account, and sends a locked tap to Unlock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
A process the tap just started still reads as offline until its first server check. With no book to restore, the widget decided before that check and sent every streamed book to its page. Now a streamed book that reads blocked gets one server check first, then plays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
…ages The hinge no longer turns with each page, so the fanned pages behind the front one stop drawing a staircase of hinges. Each page is the two sleeves on an inset page with a gilt edge, so what peeks out behind is a page edge, not a sliver of cover art. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
Every Binder layout is pinned left to right. StackView never mirrors its fan of pages, so a mirrored hinge grid put the hinge over the right cover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
Android 12 and later refuse to start the playback service in the foreground once the app has no screen up, and a slow server let that happen after the widget's handoff screen had closed (playing, but with no notification and open to being killed). The handoff screen now stays until the service has started, or the load ends, or 30 seconds pass. It is fully transparent and lets every touch through, so the home screen stays usable meanwhile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
Choosing a new cover (a search result or a photo) or going back to the original now redraws home screen widgets right away, instead of waiting for the next sync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
…ion too When the tapped book is the one the app had loaded, a cold start reloads it in the background after Play. The widget's handoff screen now waits for that reload and for the playback service to start, the same as for a new book. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e64b26b79e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| withTimeoutOrNull(deadline) { | ||
| coroutineScope { | ||
| missing.map { book -> async { permits.withPermit { fetchToDisk(book, px) } } }.awaitAll() |
There was a problem hiding this comment.
Make cover downloads cancellable at the warm-up deadline
When uncached covers come from a slow or unreachable server, the eight-second deadline does not stop the downloads: fetchToDisk() performs blocking OkHttp execute() and stream reads without connecting coroutine cancellation to the calls. The shared client allows 30-second connection and 60-second read timeouts, and structured concurrency waits for these blocking children even after withTimeoutOrNull expires. Since BinderFactory.onDataSetChanged() synchronously waits for warm() before publishing its spreads, this can leave the widget loading for a minute or longer instead of falling back to titles after eight seconds. Cancel the underlying calls on timeout or enforce a download-level deadline.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 9844089. Widget cover downloads now run as cancellable OkHttp calls (enqueue, with call.cancel() on coroutine cancellation) on a derived client with a 6 s callTimeout, under the 8 s warm deadline, so warm() returns on time and falls back to titles. The shared client is untouched. A cancelled or failed download leaves no file. Tests use a socket that never answers.
Cover downloads are cancellable and capped by a 6 second call timeout, so a stalled server can no longer keep the Binder widget loading past the 8 second fallback to titles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9844089ccd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| SleeveFill.Empty -> { | ||
| views.setInt(sleeve, "setBackgroundResource", R.drawable.widget_sleeve_empty) | ||
| views.setViewVisibility(cover, View.GONE) | ||
| views.setViewVisibility(title, View.GONE) | ||
| views.setContentDescription(sleeve, null) |
There was a problem hiding this comment.
Clear the click action when rendering an empty sleeve
When the library has an even number of books, the final spread has an empty right sleeve. The collection uses a single view type, so StackView can recycle a spread previously displaying a book into that final spread. This branch hides the artwork and title but leaves the previous sleeve's fill-in intent attached, allowing a tap on the visibly empty sleeve to play or open the previously displayed book. Explicitly clear the sleeve's click action when applying SleeveFill.Empty so recycled views cannot retain another book's action.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ad40d00. Every sleeve now sets its tap on every bind. An empty sleeve sends WidgetLaunch.Nothing, which the trampoline finishes on without launching anything, so a recycled view can't keep another book's action.
Every sleeve now sets its tap on every bind, and an empty one sends an explicit do nothing action, so a recycled row no longer plays or opens the previous book. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
What changes for users
Part of #8 (the large Nine Lives widget lands next and closes it).
Tests
Evidence
/home/static/nine-lives-evidence/2026-10-31-220w/SUMMARY.md, on a Pixel 6 emulator (API 36) with a mock Audiobookshelf:w1/06-swipe-strip-v3.png. Locked:w1/03-locked-crop-v2.png. Forced right to left:w1/17-rtl.png.w1/20-cover-hook.png).Review
Design deltas (accepted)
Needs the S26U
One UI fling feel, Widget settings from a long press, short wide cells, battery over a day, and a real trial expiry.
Draft copy pending Jeff
Binder. Flip through your books like an old CD binder. Unlock to fill the Binder. Unlock to bring the altar home. Signed out. Open Nine Lives to sign in. The folder went quiet. Open Nine Lives to find it. Set up your Binder. When you tap a cover. Play it. Open its page. Order. Done. Your picks wait here until you unlock. Play. Pause. Open Nine Lives. Locked. Tap to see the unlock. Benefit line: Home screen widgets, the Binder and Nine Lives.
🤖 Generated with Claude Code
https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf