feat(sync): use page index as canonical locator for fixed-layout & comic formats

Fixed-layout EPUBs, PDFs, DjVu, and comic archives (cbz/cbr/cb7/cbt)
are page-based: each page is a fixed image, so a page index is an exact,
universal locator regardless of screen size or device. Sync previously
treated these like reflowable content (CFI-first restore, percentage
fallback, character-offset math), which was both wrong and lossy. This
makes the page index the canonical position for fixed-layout and comic
formats while leaving the reflowable path byte-for-byte unchanged.

Backend:
- progress.go SaveProgress: branch on mediaItem.FormatGroup. For
  fixed_layout/comic_archive derive percentage from current_page/
  total_pages and skip the CFI/character-offset back-fills (meaningless
  for image content). The reflowable derivation block is preserved
  verbatim under an else.
- kobo.go: Kobo only sends a percentage, so for fixed-layout/comic
  formats derive CurrentPage via PercentageToPage(percentage, pageCount)
  using the media item's known page count, so Kobo->web lands on the
  exact page.

Reader:
- reader.templ readerInitExpr: pass savedPage/savedTotalPages to the
  reader config for fixed_layout/comic_archive formats.
- reader.ts: when isFixedLayout and savedPage is present, restore via
  view.init({ lastLocation: savedPage - 1 }) (a bare number navigates
  foliate directly to the section index). Reflowable falls through to
  the existing CFI->percentage path, unchanged.
This commit is contained in:
2026-06-07 00:03:46 -04:00
parent b1d2ccc87c
commit 423a1c0bf7
5 changed files with 82 additions and 29 deletions
+10 -4
View File
@@ -412,6 +412,8 @@ document.addEventListener("alpine:init", () => {
mangaType: string;
savedPercentage?: number;
savedCfi?: string;
savedPage?: number;
savedTotalPages?: number;
}) {
this.mediaItemId = config.mediaItemId;
this.settings = await loadSettings();
@@ -517,14 +519,18 @@ document.addEventListener("alpine:init", () => {
document.addEventListener("keydown", (ev: KeyboardEvent) =>
this.handleKeydown(ev),
);
if (config.savedCfi) {
await this.view.init({ lastLocation: config.savedCfi });
if (this.isFixedLayout && config.savedPage != null && config.savedPage > 0) {
// Fixed-layout & comics: a page index is the exact, universal locator.
// A bare number navigates directly to the section index in foliate.
await this.view.init({ lastLocation: config.savedPage - 1 })
} else if (config.savedCfi) {
await this.view.init({ lastLocation: config.savedCfi })
} else if (config.savedPercentage && config.savedPercentage > 0) {
await this.view.init({
lastLocation: { fraction: config.savedPercentage },
});
})
} else {
await this.view.init({});
await this.view.init({})
}
this.initTime = Date.now();
this.fetchReadingSpeed();