Repository navigation
feat(readlist): continue ordered read lists like series - #1095
Conversation
Reading through a read list that spans several series fell back to series order as soon as the user left the read list page: the reader only used read-list navigation when the book was opened from that page, and Keep Reading / On Deck are series-only (Komga's On Deck is the first unread book of a series), so the dashboard suggested the series' next book instead of the read list's, sometimes next to the read list's own. KMReader now remembers the ordered read lists the user is reading and treats them like series: - ReaderPresentationManager.present resolves a missing read list context through ReadListReadingService.ownerContext, so a book owned by a read list being read continues in read-list order from any entry point (dashboard card, series page, book detail). An explicit context from the read list page always wins. Non-incognito sessions record the entry point, including seamless cross-book moves. - On Deck (row and detail page) puts a read list's next unread book first and drops the series suggestion for any book owned by a visible read list, so one reading thread shows up once. Keep Reading is left to Komga, which already lists every book in progress. Resolution runs on the full read list; the library scope only hides the result. - State lives in a new read_list_reading_states table (migration 00008) and syncs across devices through Komga's per-user client settings (1.20+), one key per read list, pushed before each pull and run with the reading-progress catch-up; older servers keep it local. Stop Reading (read list context menu and detail toolbar) clears it; a stop stays a tombstone until the server key is deleted. - ReadListReadingService publishes the derived snapshot, re-deriving it after its own writes and on local book-progress / read-list projection changes. On Deck observes it and reloads when what it merged is out of date, so a sync or another device's change needs no manual refresh. Alternatives dropped: passing a context per dashboard card only covered the dashboard, not series pages or book details; merging read lists into Keep Reading could show a book already finished on another device from stale local progress; having the service post dashboard reloads on change lost updates whenever another reader of the snapshot refreshed it first. Only ordered read lists take part; "next book" has no meaning in an unordered one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
everpcpc
left a comment
There was a problem hiding this comment.
Thanks — the design holds up well against the description: central context resolution, explicit-context-wins, observe-don't-push snapshot ownership, push-before-pull sync, and the new AGENTS.md rules all match the implementation. Spec check passes on the rest: On Deck suppression works on both the dashboard row and the detail page, tombstones survive pulls, incognito and unordered read lists are excluded.
Two blockers before merge (inline):
- Migration number collision —
00008is already taken onmain(00008_add_instance_api_key_id); this must be renumbered and reordered. Migrations are immutable once merged, so it has to happen now. - Cold-start snapshot never loads — offline launch, restart within the 30s catch-up debounce, or a hidden On Deck section all leave the snapshot empty, so read-list continuation silently falls back to series order from every entry point except the read list page, despite the state sitting in GRDB.
Smaller notes:
mergingReadListContinuationsis duplicated ~20 lines verbatim between the two dashboard views, and theonChangeobservers have already drifted (one extractedreloadIfReadListsChangedwith logging, the other inlined it).stopReadingis duplicated betweenReadListContextMenuandReadListDetailView, and they behave differently — the menu firesonMutationCompleted?()(a browse reload) though stopping isn't a read-list mutation, the detail view doesn't.- Offline stop is unconditional last-writer-wins: if another device resumed the read list while this one was offline, the tombstone's DELETE still removes the fresher server key. If that's intentional, worth one line in AGENTS.md; otherwise compare
readAtbefore deleting. UserClientSettingDto(ClientSettingsService) andAuthService.ClientSettingare the same{ value }DTO — could share one.- Theoretical: local recording is gated on
readList.ordered, butapplyRemoteReadListReadingStatesisn't — a state synced from another device could surface Stop Reading for an unordered read list. Very low probability.
Once the migration is renumbered and the cold-start load is fixed, this looks good to go. The manual validation matrix in the description is impressive.
|
One design suggestion on top of the review: please gate the whole feature behind a settings toggle, default OFF. Why a toggle at all. The feature changes the behavior of existing content rather than adding something purely new: it reorders On Deck, suppresses series suggestions for owned books, and changes next/previous navigation from every entry point. Today the only escape is Stop Reading per read list — which a surprised user doesn't know exists, or why they'd need it. A single switch ("Continue read lists like series", e.g. in Settings › Dashboard alongside Why default OFF.
What default OFF changes in the implementation. Gate everything, not just the visible effects: when off, don't record ( Recovering discoverability. Since default OFF hides the feature, surface it where the need arises: when the toggle is off, ordered read list pages could show a quiet hint row ("Enable Continue like a series in Settings › Dashboard to read this list across series"), plus a changelog callout. Contextual discovery beats hoping users find the setting. The two blockers from the review above still stand independently of this. |
|
A follow-up design suggestion that builds on the toggle comment above: surface read-list continuations as their own dashboard section instead of merging them into On Deck. This replaces the Why a dedicated section is the better shape:
Suggested card shape: read list name + next book's cover + progress (x/y); tapping opens the next book with an explicit read-list context (same as an On Deck card); context menu can carry Stop Reading. The trade-off to be explicit about: this deviates from the issue's original expectation ("A2 isn't suggested separately while the read list is in progress"). I'd argue that expectation priced in the merge design's costs — complexity, invisibility, unattributable suppression — without considering the alternative. If a user reads A2 ahead of the list, the cursor simply follows the open, exactly as the PR already implements. With this shape the whole feature becomes: default-OFF toggle gates everything; a dedicated section surfaces lists being read; On Deck is untouched. |
- Renumber the migration to 00009_add_read_list_reading_states, after main's 00008_add_instance_api_key_id, instead of registering a second 00008. - Load the read list snapshot with each instance in ContentView's per-instance startup task. It was only loaded by the network catch-up (offline-gated and debounced) or by On Deck, so an offline launch, a relaunch within the debounce, or a hidden On Deck left continuation and Stop Reading disabled although the state was in the local database. - Reconcile pulls first and keeps, per read list, whichever change happened last on any device, a read or a stop; a stop records its time on the tombstone. An offline stop no longer deletes a read another device made after it. - Only ordered read lists count as being read, including for Stop Reading, whatever another device synced. - Share the On Deck merge between the dashboard row and its detail page (OnDeckReadListMerge), Stop Reading between the context menu and the detail toolbar (ReadListStopReadingButton, no browse reload since stopping is not a read list mutation), and the client setting DTO between AuthService and ClientSettingsService. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Also addressed from the review summary:
🤖 Addressed by Claude Code |
Resolves the conflict with kmworks#1097, which moved dashboard row loading into DashboardSectionViewModel: the On Deck read list merge now runs in the view model's loadPage (after the fetch, re-checking the load id after the merge) and the row view's snapshot observer calls reloadIfReadListsOutdated, which supersedes through startReload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adopts the design review of this PR: the feature changes how existing content behaves, so it is now opt-in, and read lists being read surface in a section of their own instead of being merged into On Deck. - New setting "Continue Read Lists Like Series" in Settings > Dashboard, off by default. While off, nothing is recorded, synced, resolved, or shown: no local reads or writes and no client-settings requests. Opening a book from a read list's page keeps following that list, as before. Ordered read list pages show a hint row linking to the setting while it is off. - New "Read Lists in Progress" dashboard section, first by default: one card per read list being read, most recently read first, driven directly by ReadListReadingService's published continuations (no pagination or detail page). A card shows the next book's cover, the list's name, the next book's title, the book's progress while it is in progress, and how many of the list's books are read; tapping opens the next book in read-list order, and the context menu offers View Details and Stop Reading. Turning the setting on adds the section, turning it off removes it, and Reset only offers it while on. - On Deck is Komga's own again: the merge, the suppression of series suggestions, and OnDeckReadListMerge are removed, restoring the row, its detail page, and DashboardSectionViewModel to main. - The card shares Keep Reading's cover-tinted look: the tinted background and its cover loading move out of BookHorizontalCardView into CoverTintedCardBackground and coverArtwork(instanceId:bookId:into:), which both cards use (no change for Keep Reading). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in kmworks#1106 and the book card metadata refresh fix; no overlap with this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks! Adopted both suggestions in be2952a:
A few choices worth flagging:
Also merged current main. The PR description is updated, including the validation list. 🤖 Addressed by Claude Code |
Resolves the conflict with the two-column read list page (kmworks#1108): the continuation hint moves into the page's shared actions slot, so it renders as the last row of the new action card in both the wide and single-column layouts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Show the continuation book's download status icon in the card's bottom bar, per the horizontal-card contract; download-status projection changes now re-derive continuations instead of being skipped. - Make the Read Lists in Progress header navigate to the read lists browse page, like the pinned sections' headers. - Drop a reconcile round when the instance switched mid-flight, so a pull or push never applies the previous server's state to the new one.
|
Pushed two commits to this branch (maintainer edit) covering the small follow-ups from the final review, to save another round trip — hope that's OK:
Our main merges raced ( Two leftovers from the final review are filed as follow-ups rather than blockers: orphaned state rows / server keys when a list is deleted on the server or fully read (no functional impact), and translator comments for the new localization keys. |
Continuation syncs through the per-user client settings API (Komga 1.20.0+), so the app now targets 1.20.0+ overall instead of keeping a local-only fallback for older servers. Bumps the declared minimum in README, the App Store description, and the website docs.
|
One more direction change, pushed as |
everpcpc
left a comment
There was a problem hiding this comment.
Approving. Both blockers from the review are fixed and verified (migration renumbered to 00009, snapshot loads at startup regardless of network), the offline-stop reconcile is now last-change-wins per list, and the opt-in setting + dedicated section design is in. We pushed the remaining polish directly (download icon, navigable header, hint gating, sync instance guard, Komga 1.20.0 minimum). CI green on all platforms; local builds pass on iOS/macOS/tvOS. Follow-ups noted in comments (orphaned state cleanup) are non-blocking.
Closes #1094
Problem
Reading through an ordered read list that spans several series only follows the read list while you stay on the read list page. From the dashboard, a series page, or book details, the reader falls back to series next/previous, and nothing on the dashboard points to the read list's next book. See #1094 for the reproduction.
Approach
read_list_reading_statestable (migration00009): one row per read list the user reads through (last book opened, when). A non-incognito reader session with a read-list context records it on open and on seamless cross-book moves. Only ordered read lists take part; "next book" means nothing in an unordered one.ReaderPresentationManager.presentresolves a missing context throughReadListReadingService.ownerContext(forBookId:), so a book that belongs to a read list being read continues in read-list order from any entry point. An explicit context (opened from the read list page) always wins.BookHorizontalCardViewintoCoverTintedCardBackgroundandcoverArtwork(instanceId:bookId:into:), used by both cards. Keep Reading renders exactly as before./api/v1/client-settings/user, Komga 1.20.0+). One key per read list,kmreader.readlist.<lowercased id>: Komga's key pattern rejects uppercase, ids are case-insensitive base32, and the original id also travels in the value. It runs with the reading-progress catch-up (login, foreground, reconnect): it pulls first and keeps, per read list, whichever change happened last on any device (a read or a stop), then pushes the local changes that won. The app requires Komga 1.20.0+ (its minimum server version); sync failures are silent (debug log only).ReadListStopReadingButton), shown only for ordered read lists being read. A stop is a timestamped local tombstone until its server key is deleted: a pull doesn't undo it, and a read made on another device after it wins.ContentView's per-instance startup task, independent of the offline-gated, debounced network catch-up, so continuation works on offline launches and right after a relaunch.ReadListReadingServiceowns the derived snapshot. Refreshes run one at a time, after the service's own writes and on local book-progress / read-list projection changes; the section observes it directly. The service never posts dashboard reloads.AGENTS.mddocuments the reader, sync, and dashboard rules.Why this shape, not another
Validation
make buildpasses on iOS, macOS, and tvOS, up to date with currentmain./readlists/{id}/books/{bookId}/next|previous; so does opening Alpha 2 from its series page's Resume button.00009_add_read_list_reading_statesafter00008_add_instance_api_key_id; a fresh install runs the baseline through00009in order.Manual test plan
Cohort impact
Known limitations
🤖 Generated with Claude Code