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
+16
View File
@@ -408,6 +408,20 @@ func (h *KoboHandler) Markup(c *echo.Context) error {
pgMediaUUID := pgtype.UUID{Bytes: bookhoardUUID, Valid: true}
percentage := readingSync.PercentRead / 100.0
// Kobo only sends a percentage. For fixed-layout & comic formats the page
// index is the canonical locator, so derive it from the known page count.
var currentPage, totalPages *int
if mediaItem, mErr := h.db.GetMediaItem(c.Request().Context(), pgMediaUUID); mErr == nil {
if mediaItem.FormatGroup == string(wsync.FormatGroupFixedLayout) || mediaItem.FormatGroup == string(wsync.FormatGroupComicArchive) {
if mediaItem.PageCount.Valid && mediaItem.PageCount.Int32 > 0 {
total := int(mediaItem.PageCount.Int32)
page := wsync.PercentageToPage(percentage, total)
currentPage = &page
totalPages = &total
}
}
}
if h.progressSvc != nil {
_, err = h.progressSvc.SaveProgress(c.Request().Context(), wsync.SaveProgressRequest{
MediaItemID: pgMediaUUID,
@@ -415,6 +429,8 @@ func (h *KoboHandler) Markup(c *echo.Context) error {
Source: "kobo",
DeviceID: pgtype.UUID{Bytes: deviceID, Valid: true},
Percentage: &percentage,
CurrentPage: currentPage,
TotalPages: totalPages,
DeviceType: "kobo",
DeviceName: device.DeviceName,
Broadcast: true,