Skip to content

The Nine Lives widget: your rank, your current book and your next Lives on the home screen - #111

Merged
StaticHumStudio merged 11 commits into
masterfrom
feat/220w2-home-widget
Nov 1, 2026
Merged

StaticHumStudio merged 11 commits into
masterfrom
feat/220w2-home-widget

Conversation

@StaticHumStudio

Copy link
Copy Markdown
Owner

What changes for users

  • The Nine Lives widget, a scaled down Home page. The rank card (rank and total time given) on top, the current book with its cover, chapter and a play and pause button in the middle, and the next Lives as covers across the bottom. Resizable from 2x2 up: smaller sizes drop the Lives first, then the rank card, so the current book with play always stays. Three Lives below 300 dp wide, four from 300 dp.
  • Play and pause work from the home screen, through the same handoff the Binder uses, so a cold start still brings up the playback notification. A Life tap opens that book's page. A book that can't play opens its page.
  • Paid, with a locked face. A free user sees the eye, a lock and "Unlock to bring the altar home" over empty sleeves, never a title, cover or rank. A tap opens the unlock screen.
  • Stays current cheaply. Redraws on play, pause, book or chapter change, a load ending, sync, mode switch, sign out, unlock change and cover picks, plus every 5 minutes only while playing. No redraws while paused.
  • Binder fix along the way. With one book's page open, "Open its page" from a widget for another book showed the old book again. Navigation now matches the book, not just the page type.

Closes #8

Tests

  • JVM: 1,996 pass (master df9f86c has 1,963). CI parity green. Glance (androidx.glance:glance-appwidget) is the one new dependency, approved in the design.

Evidence

/home/static/nine-lives-evidence/2026-10-31-220w/w2/SUMMARY.md on a Pixel emulator (API 36) and an API 30 emulator, against a mock Audiobookshelf:

  • Faces at 4x3, 4x2, 3x2 and 2x2 (07-size-*-v2.png), API 30 4x3 (09-api30-4x3-v2.png), locked (02-locked-crop.png), RTL (12-rtl-crop.png).
  • Warm play, warm pause and cold play from a killed process, with the service in the foreground each time. The rank card text matches Home exactly. Local, signed out and nothing played faces leak no titles. Relock with the app closed.
  • API 30: zero bitmap or crash errors at every size. The cover budget worst case is 598,016 of 614,400 bytes.
  • A failed load: the play button recovers.

Review

  • Claude Opus review 1: paywall, playback, shared code, Glance lifecycle and update loops clean. Fixed: content floated in the middle with Lives packed left (now rank on top, Lives spanning the bottom, larger Lives covers), and the play button could stay dimmed after a failed load.
  • Claude Opus review 2: clean.

Needs the S26U

The one row layout (Pixel won't go below 2 rows), One UI resize behavior, and battery over a day.

Draft copy pending Jeff

Nine Lives (picker label). Your rank, your current book, and your next lives (picker description). Unlock to bring the altar home (locked line). Reused from the Binder: Play, Pause, Open Nine Lives, Locked. Tap to see the unlock, and the empty state lines.

🤖 Generated with Claude Code

https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf

StaticHumStudio and others added 9 commits November 1, 2026 01:14
…s the build

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
…'s rank card

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
…your next lives

Play and pause go through the same invisible handoff screen as the Binder,
so a cold start holds until playback is in the foreground. A book that
cannot play right now opens its page instead, like a Binder cover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
…s, and every few minutes while playing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
…e is open

Navigation's single top matched the book page's route pattern, so with
book A's page on top a widget tap for book B kept showing book A. This hit
the Nine Lives widget's Lives and the Binder's Open its page alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
… and the resize floor matches real launcher cells

Pixel Launcher rounds a 2x2 to about 172 by 224 dp, under the old 180 dp
floor, so the smallest size could not be reached. The bitmap budget now
counts only the covers each size draws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
…onger leaves the dimmed play button

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
…ttom spanning the full width, the current book between

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XfNSbEfq8dG2ResrdGXbf
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-01T07:09:54.563095Z 397cc97 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8df9be63f8

ℹ️ 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".

Comment on lines 350 to 351
val shown = activeBook(shownBookId)
val plan = togglePlan(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject stale-library toggle taps before choosing a playback action

If the user switches libraries or modes and taps the widget before its debounced redraw, activeBook(shownBookId) returns null, but execution still reaches togglePlan. That function checks audible playback and a retained current book before checking shownBookId, so the stale button can pause or resume the new library's book instead of being ignored. The new widget now exposes this previously unused toggle path. Return IGNORE immediately when the shown book is outside the active library, as playFromBinderOwned already does.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3f9d7e5. togglePlan now returns IGNORE first when the shown book isn't in the active library, before any playback state is read, so a stale button never pauses or resumes the new library's book. The test failed first.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f9d7e5464

ℹ️ 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".

*/
internal fun toggleTap(launch: WidgetLaunch.TogglePlay): WidgetTap {
val act = CompletableDeferred<Unit>()
val plan = scope.async { toggleOwned(launch.bookId, act) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Decide pause actions before probing server reachability

When a streamed book is still playing or buffering after its server becomes unreachable, the new widget's pause tap calls toggleOwned, which eagerly awaits isPlayable(shown) before togglePlan can select PAUSE. That check can wait up to eight seconds for a server probe, leaving audio playing despite the pause tap even though pausing requires no network access. After validating entitlement and the active library, select the pause action before performing the playability check; reserve that check for actions that actually need to load media.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 397cc97. Playability is now asked lazily, only when the tap would load a book. A pause while audio is playing or buffering returns PAUSE without the server probe. The test uses a probe that hangs if called.

@StaticHumStudio
StaticHumStudio merged commit 8e3c411 into master Nov 1, 2026
1 check passed
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.

Home-screen widget: build the thing the listing used to claim

1 participant