Skip to content

Extract Home and Shelf loading from DialogManager into pure loaders - #72

Closed
CaptainCamin wants to merge 213 commits into
Billiam:mainfrom
CaptainCamin:claude/advisor-opus-9cff0c
Closed

CaptainCamin wants to merge 213 commits into
Billiam:mainfrom
CaptainCamin:claude/advisor-opus-9cff0c

Conversation

@CaptainCamin

Copy link
Copy Markdown

What changed

  • hardcover/lib/shelf_loader.lua: the whole-shelf paging loop (page size, retry on a cancelled request, 429 backoff, de-duplication, page cap) plus plan(), which decides what the screen does with the outcome.
  • hardcover/lib/home_loader.lua: the Home refresh sequence (counts, reading, lists, goals), which saves what comes back and reports only what changed.
  • DialogManager:showHome / showShelf now build the dialog and handle each result; they went from 2,132 to 2,036 lines.
  • New harnesses: spec/shelf_loader_harness.lua (18 checks) and spec/home_loader_harness.lua (10 checks).

Why

DialogManager mixed UI building with fetching, caching and retry rules, so that logic could only be tested through a UI harness. The loaders take the API, network check and liveness test as arguments, so they run under stock Lua like the other pure modules. This is the first step of splitting up DialogManager.

Notes for review

  • Behaviour should be unchanged; the existing offline_shelf, close_refresh, no_blocking_dialog, menu_wiring and startup_weight harnesses pass.
  • spec/charts_harness.lua fails when run on Lua 5.5 (math.atan2 is gone); it fails the same way on main. Other harnesses need unpack shimmed on 5.5. Not run on LuaJIT or in the emulator.
  • Goals, Lists and Vibes follow the same pattern and could be extracted next.

🤖 Generated with Claude Code

dev and others added 30 commits October 1, 2026 22:06
Feature code survived intact; git history, CI config, and the spec/
harnesses did not. Re-initialised the repository so work can continue.
Both shelf views crashed on device and there was no way to see why. The first
attempt loaded the real vendored ListMenu/CoverMenu under stubs, but every
KOReader method those widgets touch that the harness did not stub surfaced as a
nil call indistinguishable from a plugin bug -- two passes went into chasing
those before it was clear the approach could not separate the two.

Rewrote it on zlibrary.koplugin's approach instead: stub the KOReader boundary,
capture what the plugin hands the real widget constructors, assert on that. The
plugin's own modules are never stubbed, so this tests code we actually wrote.

spec/support.lua holds the shared scaffolding and the capturing Menu.
spec/shelf_dialog_harness.lua drives the real ShelfDialog across both statuses,
twelve missing-optional-field variants, six shelf sizes and the paging states.

Also restores spec/run_all.sh and CI, both of which were lost with the earlier
working tree.
The earlier working-tree loss took most of spec/ with it, leaving only the
config and shelf harnesses. These four put the coverage back for the two
features that had none.

sync_queue_harness (36) -- the queue is the whole of offline sync. Losing a
page or replaying a send silently corrupts the user's reading history, and
none of it is visible on the device until it is wrong.

cache_harness (20) -- the half the queue cannot reach: what the UI shows while
offline. Wrong hydration means the user opens a book they have been reading and
the plugin reports a page they never turned to.

auth_harness (28) -- centred on the one hazard that cannot be recovered from:
Hardcover's refresh tokens are single-use, and replaying a spent one revokes the
whole chain. Pinned that exactly one refresh is ever presented, that an unknown
outcome is never retried, and that sign-out survives a restart.

oauth_client_harness (18) -- OAuth reports failures as a 4xx plus a JSON body
carrying the real reason. Discarding the body turns 'the user has not approved
yet' into an indistinguishable failure, so every sign-in looks broken.

Also adds spec/json.lua. A stub that raised on every body would have made the
error-code paths untestable, which is precisely where the subtle bugs are.

Three harness bugs worth recording, all found by these tests:

- readSetting must return the live table by reference, as real LuaSettings
  does. Returning a copy silently discarded every write, so the queue appeared
  to lose entries.
- HARDCOVER.STATUS.READ does not exist (it is FINISHED), and referencing a
  missing constant yields nil, which quietly neutered five assertions. Both
  harnesses now verify the constants they use at load time.
- uihttps delivers a response body through the sink callback, not the return
  value; returning it as a third value left the sink empty and made every 200
  look malformed.

151 checks total, from 49.
Both the Want to Read and Currently Reading lists crashed on device. Cause:

hardcover/vendor/listmenu.lua decides how to draw a row with

    self.is_directory = not (self.entry.is_file or self.entry.file)

Shelf items set neither field, so every book was rendered through the
directory branch instead of the book branch -- the one that draws a cover and
handles a file entry. search_dialog.lua sets

    file = "hardcover-" .. book.book_id

and the shelf dialog simply omitted it, which is why the search list worked
and both shelf lists did not.

Fixed by setting the same synthetic marker in ShelfDialog:createListItem. The
marker is never opened; it exists so the menu classifies the row as a book.

Pinned with two assertions in the shelf harness, which fail against the old
code. Neither book_detail_dialog nor journal_dialog uses the vendored list, so
neither was affected.

Found by comparing against zlibrary.koplugin, whose menu.lua sets show_cover on
an extended Menu and documents the two related crash modes it hit (a no_title
crash in mergeTitleBarIntoLayout, and freed widgets left in item.state). Neither
applies here -- this plugin never sets no_title and never embeds item.state --
but reading their approach is what pointed at the branch selection.
The offline sync, shelf lists and the crash fix shipped in this version but were
never written down, so the release notes described only the OAuth work.
Symptom: tapping a dialog item in the plugin menu did nothing, and the screen
only updated after a power cycle.

Cause was not a repaint. Every dialog-opening menu item ran its show from inside
an AutoWifi callback, and AutoWifi has paths that never invoke that callback:

  - airplane mode: wifiPrompt returned immediately
  - withWifi returned silently when a connection was already pending
  - withWifi returned silently when the device cannot restore wifi
  - withWifi's restore branch called back only from a connectivity check that
    may never fire

On those paths the callback never ran, so the dialog was never constructed. On
e-ink a screen that never changes looks exactly like a stale framebuffer, which
is why this read as a missing-refresh bug, and why a power cycle appeared to fix
it -- the cycle rebuilt the UI and revealed the screen that had been requested
all along.

Two changes:

- HardcoverMenu:withWifiThen runs the action first and layers the wifi prompt on
  top, so the screen opens whether or not wifi comes up. A failed fetch now
  reports 'Could not load your list from Hardcover' instead of nothing at all.
  This is the shape zlibrary.koplugin uses: it shows every dialog directly in the
  menu callback and handles connectivity separately.

- AutoWifi:withWifi and wifiPrompt now reach their callback on every path, so
  the remaining callers (link book, and the progress paths in main.lua) cannot
  hang either.

Pinned by spec/menu_wiring_harness.lua, which fails against the old code on four
separate callback paths plus a source check that no menu item defers its dialog.
The About box and the sign-in flow both blocked on an HTTPS request before
calling UIManager:show, so on a device that could not reach the host nothing was
ever put on screen. On e-ink a screen that never changes is indistinguishable
from one that failed to refresh, which is why this was twice mistaken for a
repaint problem.

About was the clearer case: it called Github:newestRelease() first, and that
request had no timeout at all -- unlike hardcover_api and oauth_client, which
both set one. With no route to api.github.com it blocked until TCP gave up, and
the About box was downstream of it.

- github.lua: a 5s timeout, pcall around the request and the decode, and a
  newestReleaseAsync() so callers need not block.
- About: builds the text, shows the box, then fills in the release note if
  GitHub answers. Its appearance no longer depends on the network.
- signIn: shows a 'Contacting Hardcover' indicator before beginDeviceFlow, and
  closes it before showing either the code dialog or an error.

Also fixes a double-show: main.lua called UIManager:show(dialog) and then
dialog:onShowSignIn(), which shows it again. UIManager has no duplicate guard,
so one screen got two window-stack entries.

spec/no_blocking_menu_harness.lua pins the rule. Verified it fails against the
pre-fix code by reverting the About entry and the timeout in a scratch copy --
2 of its 7 checks go red -- because a guard that cannot fail proves nothing.

The earlier commit's wifi changes are unrelated to this and stay: those
callbacks genuinely never fired on four paths, which was a real bug, just not
the one being reported.
The dialog had never been rendered against real KOReader widgets, and it did
not open at all. Four separate faults, each of which a stub harness cannot
see because the widget never runs:

* button_row.height is nil. HorizontalGroup sizes itself behind getSize() and
  has no .height field, so the ScrollableContainer height arithmetic raised
  "attempt to perform arithmetic on field 'height' (a nil value)".

* ScrollableContainer was given width/height, which it ignores: it takes its
  size from an explicit dimen, and reads dimen.w/h in initState. The first
  paint died with "attempt to index field 'dimen' (a nil value)".

* button_row was appended to the scroll's content *and* placed outside it, so
  the same widget had two parents and the Close button rendered twice.

* Shelf.detailRows emits the description as a metadata row, and the dialog
  also gives it a dedicated wrapping box, so the whole paragraph printed
  twice. Filtered at the display layer so detailRows stays complete.

Emulator harness, extended so a change can be checked before it is ported:

* spec/emu/tree.lua walks the painted widget tree and returns visible text
  with geometry. Screenshot-independent, and the reason the duplicated
  description was catchable at all -- two identical paragraphs read as a
  rendering artefact in a PNG but not as a count.

* Each shot also writes a .txt of everything drawn. Reading a PNG needs a
  vision model, which may be rate-limited or absent; the text dump is always
  there, and two runs that render differently diff as text.

* Scenarios for shelf, search, book detail and journal. They assert on
  structure and content, not just "it rendered": the file-marker invariant on
  every list row, no text drawn twice, nothing outside the panel, paging that
  actually advances.

* boot.lua gains key dispatch and a widget-tree dump. Both had traps worth
  recording in the README: UIManager:sendEvent returns nothing even when the
  press worked, and key bindings are device keycodes (RPgFwd), so "NextPage"
  matches nothing and is dropped silently.

Fixtures use the plugin's real settings and dialogs and replace only the
network. A scenario that depends on an unstubbed call shows up as an empty
screen rather than a false pass.

spec/emu/.home and .out are gitignored; package_release.sh already excludes
spec/ entirely, so none of this ships.

Both suites pass: 4 emulator scenarios, 36 stock-Lua checks.
boot.lua defined emu:key twice: an UIManager-dispatch variant and a
topmost-widget variant that swallows whether the key was consumed. The
later definition won, so every scenario silently got the lenient one --
a key matching no binding passed instead of failing, which is the same
silence that makes a broken widget look like a no-op.

No scenario called emu:key (only the README), so nothing depended on the
overriding behaviour. The lenient variant is now emu:keyLenient and both
survive; emu:press remains the one that asserts consumption. Also gave
the surviving emu:key a comment saying what it does *not* check, since
UIManager:sendEvent returns nothing about handling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The plugin had no shared way to say "loading", "that failed" or "try
again", so every screen improvised. That is how a failed rating save came
to be reported by calling a misspelled field (hardcover_menu.lua:716,
dialog_magager), which raises on nil instead of reporting anything.

Adapted from zlibrary.koplugin's ui.lua and dialog_manager.lua. Three
things carried over deliberately rather than reinvented:

* No force_one_line on the loading message. To fit one line InfoMessage
  shrinks its font and re-runs init(); a plugin that patches
  InfoMessage.init (appearance.koplugin does) reassigns the font every
  call, so the loop never converges and KOReader dies with a stack
  overflow. Wrapping to a second line needs no re-run. Asserted absent,
  because it is invisible until it kills a device and adding it is the
  natural thing to do when a message does not fit.

* The retry frame is impersonal -- "Could not complete X" -- so the
  operation name is an object and the verb has nothing to disagree with.
  Naming it as the subject broke ten of z-library's fourteen locales
  ("Kommentare ist fehlgeschlagen").

* The error path carries icon = "notice-warning". Without it every error
  rendered as an ordinary notice while the code claimed otherwise.

The retry text includes the underlying error and never claims the failure
was temporary: for a walled server or a wrong host it is not, and
"temporary" invites a retry that cannot work.

Not yet wired into any screen -- Phase 2 converts the blocking
dialog_manager paths to show-then-fetch and uses this.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
spec/no_blocking_menu_harness.lua already pinned two menu paths (About,
sign-in). Both pass. The dialog paths were never covered, and all four
of them block on the network before showing anything:

    showShelf            API at 1, show at 2
    showBookDetail       API at 1, show at 2
    updateSearchResults  API call, no show at all
    journalEntryForm     API at 1, show at 2

With no route to the API that is up to six seconds of nothing after the
tap (socketutil:set_timeout(6, 12) at hardcover_api.lua:176), and on
e-ink the panel keeps whatever was last painted until the user
power-cycles. That reads as a crashed device.

The harness records UIManager:show and the API call on one shared
timeline and asserts the show came first, so a regression that moves a
fetch back above a show fails here rather than on a device.

Committed RED on purpose: this asserts behaviour the plugin does not yet
have, so run_all.sh reports FAILURES until the dialog_manager paths are
converted. The four diagnostics above are the bug, stated numerically.

Two notes on what this harness deliberately does not do:

* It never calls a dialog's init. Building a widget tree under stubs
  means every unstubbed KOReader method surfaces as a nil call that looks
  exactly like a plugin bug. The constructor spec is captured instead,
  which is what answers the ordering question. A null-object fallback
  covers the callers that immediately poke a field init would have built
  (dialog_manager.lua:238 scrolls _input_widget unguarded).

* The cost of that choice is real: a dialog missing a field it should
  have set is invisible here. spec/emu/run.sh is what covers that, by
  rendering through the real frontend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three stacked bugs, all silent until a scenario actually tapped something:

* ui/gesturedetector does not exist; the module is device/gesturedetector.
* There is no Gesture class to instantiate. gesturedetector builds a plain
  table -- { ges = "tap", pos = Geom:new{...}, time = ... } -- and ges_events
  match on the *string* via `gs.ges ~= self.ges`. Constructing anything else
  yields a gesture every widget ignores.
* ui/time is fixed-point. Time:s(x) builds a time from a count of seconds, so
  Time:s() is arithmetic on nil; Time.now() reads the clock.

It also referenced an undefined global `Event` instead of self.Event.

The old body swallowed all of this in a pcall and returned nothing, so a broken
tap looked exactly like a tap nothing responded to. tap() now returns whether a
widget consumed the gesture, and tapExpecting() asserts on it -- a tap outside
every registered range is dropped silently, which is otherwise
indistinguishable from a widget ignoring touches.

Verified: a tap at a row selects it and fires the selection callback; taps on
empty space and the title bar correctly return nil.

Also removed a duplicated emu:key definition. In Lua the later one wins, so the
two variants had been shadowing each other.
Four dialog entry points fetched before showing anything:

    showShelf             Api:getShelf          -> shown 30 lines later
    showBookDetail        Api:getBookDetail     -> shown after
    updateSearchResults   Api:findBooks         -> no show, error was a no-op
    journalEntryForm      Api:findDefaultEdition-> dialog built after the lookup

With no route to the API that is up to six seconds of nothing after the
tap (socketutil:set_timeout(6, 12)), and on e-ink the panel keeps
whatever was last painted until the user power-cycles -- which reads as
a crashed device, not as a network problem.

Each now builds its dialog and shows it first, then fills it in from the
callback. The async wrappers on HardcoverApi deliver through
UIManager:nextTick so a caller may touch widgets directly; every write
is guarded on isWidgetShown because the user can close the dialog while
the request is in flight, and updating a freed widget crashes.

Also in this change:

* BookDetailDialog grows a loading body and setDetail. setDetail re-runs
  init, which is safe here because every widget in the body is a pure
  function of self.detail -- noted in the code, because the same trick
  would destroy FocusManager focus if used for live text.

* ShelfDialog grows setEntries, setEmptyState and updatePager. An empty
  shelf previously rendered as a title bar over a blank list, which
  reads as a bug rather than as an answer. The empty-state row still
  carries a `file` marker: without one the vendored ListMenu draws it
  through the folder branch that already crashed this plugin once.

* updateSearchResults no longer swallows failures. It closed the dialog
  only when Api.enabled was false and otherwise left stale rows that
  looked like results. It keeps the old rows on failure too -- clearing
  them turns a transient error into "no matches", a different wrong
  answer to the question asked.

* Two silent write failures in hardcover_menu.lua. A failed rating save
  called self.dialog_magager (misspelled), so it raised on nil instead of
  reporting -- and raised after the spinner was gone, so the screen just
  did not change. A failed page write hit an empty else branch.

journalEntryForm's edition lookup moved after the show rather than being
dropped: the dialog is usable without it, and setEdition fills it in.

Verified: spec/run_all.sh OK (the red harness from fc11656 is now 4/4),
and all four emulator scenarios still render through real KOReader.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous commit converted the dialog_manager entry points. These
four lived in callers and blocked the same way:

    Change edition        Api:findEditions in the menu callback
    Link book             findBookOptions -- two round trips, before any
                          dialog existed
    Suggest a book        cacheRandomBooks on a cold cache
    Journal -> edition    Api:findEditions inline in select_edition_callback

New: DialogManager:buildLoadingSearchDialog. Takes a fetch function
instead of a list, shows an empty dialog first, then fills it. "Change
edition", the link-book dialog and the journal's edition picker all route
through it, so the three copies of fetch-then-show collapse into one.

buildBookListDialog grew the same optional fetch, because its rows hand
off to the file searcher rather than to linkBook -- reusing the search
dialog's own select callback would have been wrong.

SearchDialog grows setEmptyState. Its "No items" was indistinguishable
from a list that failed to load, and both the edition picker and the
suggestion list can genuinely come back empty.

Suggest a book still prefers its cache -- a warm list means no request
at all. Only a cold cache goes to the network, and then the dialog is
already up. The empty case no longer raises a Notification and returns
without showing anything; it is an empty state inside a real screen.

Kept buildSearchDialog: spec/emu/scenarios/search.lua drives the search
screen through it, so it is a test entry point as much as a production
one. Verified rather than assumed -- grepped before deciding.

Also resolved the journal re-entrancy TODO at :207 by explaining it in
place. buildLoadingSearchDialog assigns self.search_dialog while a
journal dialog is up, which is safe because the journal dialog is a
local rather than a field, and the two are different widget classes.

Harness now 6/6, suite OK, all four emulator scenarios still render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every shelf row rendered as:

    The Lathe of Heaven
    hardcover-201
    Want to Read

The marker is the synthetic `file` value that stops ListMenu drawing an
API-sourced row as a folder, and it was reaching the screen as the book's
name. vendored/listmenu.lua:277 sets bookinfo = self.entry, then :502
renders title = bookinfo.title or filename_without_suffix, and
filename_without_suffix is parsed back out of entry.file (:131). Shelf
rows set `text` but no `title`, so every one of them fell through to the
filename branch.

A stub harness cannot see this: the item table is correct in the broken
case too, with a `file` and a `mandatory` exactly as it should be. It
surfaced only by reading spec/emu/.out/shelf_page1.txt.

Row shaping moves into hardcover/lib/ui/list_row.lua, shared by both
list dialogs, which had drifted into two different implementations. It is
pure -- no widgets -- so it runs under stock Lua. Three row fields are
load-bearing and each now has a test:

  * `file`  -- without it the row renders as a folder. Crashed both shelf
               views on device once already.
  * `title` -- without it the marker becomes the visible name. The bug
               this fixes.
  * cover keys -- absent when there is no cover, never empty. An empty
               cover_url builds an image widget that can never load.

Four defects fixed in the move:

  * `if book.contributions.author` indexed a FUNCTION, so the primary
    author was always dropped and only contributors survived.
  * `series` was assigned three times; the last write won, so a series
    position was discarded whenever the book also had a language.
  * The language branch read `self.series` -- the dialog's field, never
    set -- to decide whether to prefix, so it always took the wrong
    branch.
  * Language was carried in the `series` field, conflating two things and
    making ListMenu render a language as a series name.

Two things the existing tests caught that are worth recording:

* Shelf rows arrive with `authors` already joined into a STRING by
  Shelf.normalizeEntry, while search rows carry author.name plus a
  contributions list. Handling only the structured shape silently dropped
  the shelf's whole author column. Both shapes are handled now.

* The shelf harness asserted the author against `text`, which was only
  true because the old code concatenated authors into the text string.
  The author lives in `authors` now, which is what ListMenu and SearchMenu
  actually paint from; concatenating it into `text` printed the title
  twice on the SearchMenu path.

The emulator scenario now asserts the marker is absent from the painted
text, and that guard was verified to fail when `title` is removed -- a
check that cannot fail is not a check.

Suite OK, emulator OK, and the shelf rows now read title, author,
status and pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two tooling gaps, both of which had been hiding real risk.

run_all.sh was testing on whatever `lua` was on PATH. On this machine
that is 5.5; KOReader is LuaJIT (5.1 semantics) and CI pins 5.1. Code
that runs green on 5.5 can still be a syntax error on the device, so the
suite was proving nothing about the target. It now prefers luajit, then
lua5.1, and prints a warning when neither is present rather than
quietly testing on the wrong one.

It also reported a bare "FAILURES" with no [FAIL] line anywhere when a
harness exited non-zero without printing one -- which is how the
shelf_dialog breakage after the status_dialogs change presented, and it
cost a loop over harnesses one at a time to diagnose. Output is now
captured per harness and the summary names every failure. Verified by
adding a deliberate failing assertion and confirming it appears in the
list.

package_release.sh gets artifact verification, which it had none of. It
checked that zip was TOLD to include _meta.lua and main.lua; it never
checked that the built bytes were right. Now it extracts the archive and
confirms: the top-level directory exists (without it KOReader installs
nothing), no credential file, no dev directories, the shipped tree
matches the stage, the stage matches the working tree, every .lua
parses, and every internal require resolves inside the archive.

Three real bugs in that script, each found by running it rather than
reading it:

* It ran `zip -r` from OUT_DIR expecting the plugin directory to be
  there. Pointed anywhere else -- a staging area, /tmp -- zip found no
  input and exited 12 with NO output. Now stages an explicit copy of
  what ships, which also makes the dev-directory check meaningful
  instead of relying on a -x list that drifts.
* ROOT was never assigned; the staging code referenced it and died on
  `set -u`.
* The require check stripped a "hardcover/" prefix from paths that
  already included it, pointing every lookup a directory too deep. It
  reported all 38 modules as missing.

The token guard covered only hc_pat_. The plugin ships OAuth, so a real
access or refresh token starts hc_at_ or hc_rt_ and would have shipped
past it. All three prefixes are checked now, and the reported matches
are shown so a human can tell a real token from a spec placeholder.
Verified by pasting a token-shaped string into a source file and
confirming the build refuses and names it; the credential-file guard
was verified the same way.

Also corrected a comment claiming this script is shared with
.github/workflows/release.yml. There is no release workflow --
workflows/ holds test.yml alone -- so this script is currently the only
definition of what ships. The comment now says that, and says a future
release workflow must call this script rather than zip anything itself.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There were three ways to show an error in this plugin, all building an
InfoMessage by hand:

  DialogManager:showError         2 second timeout
  BookDetailDialog:showError      no timeout at all
  StatusDialogs.error             5 second timeout, warning icon

All three now delegate to StatusDialogs.error. The divergence was not
cosmetic: BookDetailDialog's version had no timeout, so a failure shown
from there had no defined lifetime, and the three different timeouts
meant no two screens behaved alike when reporting the same kind of fault.

Also pinned the shelf rating formatting, which the row extraction could
plausibly have broken: a whole rating reads "4*" not "4.0*", and a
fractional one keeps its decimal. Both are now asserted, since that
formatting is user-visible and previously untested.

Left the success Notification in hardcover.lua alone -- that is a
different thing from an error and is not StatusDialogs' business.

Suite OK under LuaJIT, emulator OK, marker leak still absent from the
painted shelf text, and the release zip re-verified after the change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, repaint before sign-in request

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
…rite once per enqueue

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
…test order-proof

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
…n unknown outcome

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
…dialogs instead of leaving them on the stack

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
…freeze the UI; grey out Sync now when nothing is pending

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
Claude and others added 28 commits October 2, 2026 11:08
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
Similar to [book] carousel on the details screen (1.3.1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
…ersion 1.3.3

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
Similar strip: load after the series, retry when cut off, say when it fails (1.3.3)
…ve author; version 1.3.4

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
Strips ignore touches, long Similar heading, title above author (1.3.4)
Swipeable strips and an On device search button (1.4.0)
… an error; version 1.4.1

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
…tly Reading shows offline reading

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
…ed with their tag

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
…nd workflow tag handling fixes

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
1.4.1: offline manual page/rating, one Hardcover button, offline Home reading, beta updates, similar-strip placeholder
…ed in place; version 1.4.2

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
…copy, Home tile, screen, setting); version 1.5.0

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
…ns, Based on...) behind a Vibes tile; asks for read:vibes

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
… a status keeps the page's question, the username lookup retries

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS
1.5.0: For you, Vibes, pending changes, offline fixes, one Hardcover button, beta updates, account name
* Add e-ink chart widgets (columns, bars, donut, KPI tiles) and their pure geometry

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS

* Stats data layer: pure stats computation, paged finished-books query, saved copy

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS

* Stats screen: charts of your reading, period chooser, saved copy, Home tile

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS

* Book details: community rating breakdown and genre, mood and content-warning labels

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS

* Stats: ask for the genre counts only on the first page

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS

* 1.6.0: Stats, and what readers say on a book's details

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS

---------

Co-authored-by: Claude <noreply@anthropic.com>
… without an edition (#38)

* Move the ratings breakdown from the book details to the Reviews screen

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS

* Book details: release dates, audiobook length, credits by role, review, list and edition counts

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS

* Book details: ask for the community and detail fields in the book's own request too, not only the edition's

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS

* 1.6.1: details rows, ratings breakdown on Reviews, fix for books without an edition

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XoUz2Ej6GE9ammm1V8yFYS

---------

Co-authored-by: Claude <noreply@anthropic.com>
showHome and showShelf mixed building the dialog with the fetch, cache and
retry logic, which could only be tested through a UI harness. The paging
loop (retries, 429 backoff, de-duplication, page cap) is now ShelfLoader and
the Home refresh sequence is HomeLoader. Both take the API, network check and
liveness test as arguments, so they run under stock Lua and have their own
harnesses. DialogManager now builds the dialog and says what to do with each
result. Behaviour is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@CaptainCamin

Copy link
Copy Markdown
Author

Opened here by mistake (wrong target repo); closing.

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.

2 participants