fix(reader): read position from the API at open; never write a restored position
The reader page embedded a snapshot of reading state (position, bookmarks) server-side at render time. Browsers may reuse that HTML (heuristic caching, bfcache), so opening a book could restore a stale position — and worse, the restore's relocate auto-saved it back, overwriting a newer device push minutes later. A KOReader sync followed by opening the web reader would silently revert the row to the old web position; the row's source and the rendered page disagreed. The web reader is intrinsically tied to the server, so it has no business preserving reading state client-side: - The rendered page now carries only immutable book metadata. The reader fetches progress fresh (cache: no-store) from the existing progress API at open and restores with the same priority as before (page for fixed-layout, CFI, percentage, fresh start); a failed fetch opens at the start and writes nothing. Initial bookmarks likewise come from their endpoint instead of the embed; annotations already did. - Progress saves are gated on deliberate navigation only (page turns, keys, slider, search/TOC/bookmark/back-stack jumps, tap zones — each marks the session as user-moved). Restores and section-load relocations never write, so displaying a position can no longer clobber a newer one. A bfcache-resurrected page resets the flag and cannot write its frozen position either. This replaces the old five-second post-init suppression, which a stale page bypassed. - The server-rendered initial progress badges render a neutral placeholder until the first relocate fills them (sub-second). No API, schema, or sync-engine changes. Normal reading saves exactly as before — the first save now simply waits for the first real page turn.
This commit is contained in:
+11
-54
@@ -5,7 +5,11 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func readerInitExpr(metadata ReaderMetadata, progress ReadingProgress, bookmarks []Bookmark) string {
|
||||
// The init config carries only immutable book metadata. Reading state
|
||||
// (position, bookmarks, annotations) is never embedded: the reader fetches
|
||||
// it from the APIs at open time, so the page can never carry — nor write
|
||||
// back — a stale snapshot of it.
|
||||
func readerInitExpr(metadata ReaderMetadata) string {
|
||||
config := map[string]interface{}{
|
||||
"mediaItemId": metadata.MediaItemID,
|
||||
"fileUrl": metadata.FileURL,
|
||||
@@ -13,42 +17,11 @@ func readerInitExpr(metadata ReaderMetadata, progress ReadingProgress, bookmarks
|
||||
"readingDirection": metadata.ReadingDirection,
|
||||
"mangaType": metadata.MangaType,
|
||||
}
|
||||
if progress.Percentage > 0 {
|
||||
config["savedPercentage"] = progress.Percentage / 100
|
||||
}
|
||||
if progress.EpubCfi != "" {
|
||||
config["savedCfi"] = progress.EpubCfi
|
||||
}
|
||||
// Fixed-layout & comic formats: the page index is the canonical, exact
|
||||
// locator (pages are fixed images). Pass it so the reader restores by page.
|
||||
if (metadata.FormatGroup == "fixed_layout" || metadata.FormatGroup == "comic_archive") && progress.CurrentPage > 0 {
|
||||
config["savedPage"] = progress.CurrentPage
|
||||
if progress.TotalPages > 0 {
|
||||
config["savedTotalPages"] = progress.TotalPages
|
||||
}
|
||||
}
|
||||
if len(bookmarks) > 0 {
|
||||
items := make([]map[string]interface{}, 0, len(bookmarks))
|
||||
for _, b := range bookmarks {
|
||||
var page any
|
||||
if b.PageNumber != nil {
|
||||
page = *b.PageNumber
|
||||
}
|
||||
items = append(items, map[string]interface{}{
|
||||
"id": b.ID,
|
||||
"title": b.Title,
|
||||
"positionLabel": b.Position,
|
||||
"cfi": b.CfiPosition,
|
||||
"page": page,
|
||||
})
|
||||
}
|
||||
config["bookmarks"] = items
|
||||
}
|
||||
jsonBytes, _ := json.Marshal(config)
|
||||
return fmt.Sprintf("initReader(%s)", string(jsonBytes))
|
||||
}
|
||||
|
||||
templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookmarks []Bookmark) {
|
||||
templ Reader(user User, metadata ReaderMetadata) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -64,7 +37,7 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
|
||||
</head>
|
||||
<body
|
||||
x-data="readerShell"
|
||||
x-init={ readerInitExpr(metadata, progress, bookmarks) }
|
||||
x-init={ readerInitExpr(metadata) }
|
||||
class={ "theme-" + user.Theme + " h-screen overflow-hidden" }
|
||||
>
|
||||
<!-- Reading surface: edge-to-edge. Chrome overlays translucently;
|
||||
@@ -96,7 +69,7 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ReaderChrome(metadata, progress)
|
||||
@ReaderChrome(metadata)
|
||||
|
||||
<!-- Drawer scrim -->
|
||||
<div
|
||||
@@ -311,7 +284,7 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
|
||||
</html>
|
||||
}
|
||||
|
||||
templ ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) {
|
||||
templ ReaderChrome(metadata ReaderMetadata) {
|
||||
<div id="reader-chrome" class="transition-opacity duration-300" :class="chromeVisible ? 'opacity-100' : 'chrome-hidden opacity-0 pointer-events-none'">
|
||||
<!-- Top bar -->
|
||||
<div id="reader-topbar" class="fixed top-0 left-0 right-0 border-b z-40 pt-[env(safe-area-inset-top)] reader-glass">
|
||||
@@ -365,17 +338,7 @@ templ ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) {
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="w-px h-6 reader-sep"></div>
|
||||
<div id="progress-display" @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[4rem] max-w-[5rem] sm:max-w-none text-center cursor-pointer truncate whitespace-nowrap overflow-hidden">
|
||||
<span class="hidden sm:inline" x-text="progressLabel"></span><span x-text="progressMain">
|
||||
if progress.FormatGroup == "reflowable" {
|
||||
if metadata.EstimatedPages > 0 {
|
||||
{ fmt.Sprintf("%.0f%% · Page %d/%d", progress.Percentage, progress.CurrentPage, metadata.EstimatedPages) }
|
||||
} else {
|
||||
{ fmt.Sprintf("%.0f%%", progress.Percentage) }
|
||||
}
|
||||
} else {
|
||||
{ fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages) }
|
||||
}
|
||||
</span>
|
||||
<span class="hidden sm:inline" x-text="progressLabel"></span><span x-text="progressMain">—</span>
|
||||
</div>
|
||||
<div class="w-px h-6 reader-sep"></div>
|
||||
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents (t)">📖</button>
|
||||
@@ -467,13 +430,7 @@ templ ReaderChrome(metadata ReaderMetadata, progress ReadingProgress) {
|
||||
<!-- Progress + TOC -->
|
||||
<div class="flex items-center gap-1">
|
||||
<div id="progress-display-fx" @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[3.5rem] text-center cursor-pointer truncate whitespace-nowrap overflow-hidden">
|
||||
<span x-text="progressMain">
|
||||
if progress.FormatGroup == "reflowable" {
|
||||
{ fmt.Sprintf("%.0f%%", progress.Percentage) }
|
||||
} else {
|
||||
{ fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages) }
|
||||
}
|
||||
</span>
|
||||
<span x-text="progressMain">—</span>
|
||||
</div>
|
||||
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents (t)">📖</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user