fix(reader): stabilize chrome panels, bookmarks end-to-end, dead UI removal
Phase 0 of the reader redesign: - Panels no longer render under the top/bottom bars: sidebars get measured insets (same resize/safe-area mechanism as the viewport); panel max-height now derives from the bounded sidebar instead of a 100vh guess; right-side border targets the actual sidebar. - Bookmarks work end-to-end for the first time: REST CRUD under /api/media-items/:id/bookmarks (create/delete route through AnnotationService for dedup/LWW/tombstones), fix UpdateMediaBookmark referencing nonexistent updated_at column, frontend posts to the real API with per-format position (CFI vs page), live list with jump + delete instead of SSR-only snapshot. - Fix chapter matching in progress saves: boundaries were compared by a nonexistent tocItem property, so chapter was never persisted. - Remove dead UI: Navigator panel stub, empty dictionary popup shell, unwired Chrome Behavior select; purge 160 stale build artifacts. - Reader chrome now follows the user's app theme instead of hardcoded theme-tokyo-night.
This commit is contained in:
+63
-75
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func readerInitExpr(metadata ReaderMetadata, progress ReadingProgress) string {
|
||||
func readerInitExpr(metadata ReaderMetadata, progress ReadingProgress, bookmarks []Bookmark) string {
|
||||
config := map[string]interface{}{
|
||||
"mediaItemId": metadata.MediaItemID,
|
||||
"fileUrl": metadata.FileURL,
|
||||
@@ -27,6 +27,23 @@ func readerInitExpr(metadata ReaderMetadata, progress ReadingProgress) string {
|
||||
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))
|
||||
}
|
||||
@@ -47,36 +64,32 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
|
||||
</head>
|
||||
<body
|
||||
x-data="readerShell"
|
||||
x-init={ readerInitExpr(metadata, progress) }
|
||||
class="theme-tokyo-night h-screen overflow-hidden"
|
||||
x-init={ readerInitExpr(metadata, progress, bookmarks) }
|
||||
class={ "theme-" + user.Theme + " h-screen overflow-hidden" }
|
||||
>
|
||||
@ReaderChrome(user, metadata, progress)
|
||||
<!-- Dockable Panels Container -->
|
||||
<div id="reader-panels" class="fixed inset-0 pointer-events-none z-30">
|
||||
<!-- Left Sidebar (TOC) -->
|
||||
<div id="left-sidebar" class="absolute left-0 top-0 bottom-0 pointer-events-auto flex flex-col">
|
||||
<div id="toc-panel" x-show="tocOpen" x-transition class="panel-container pointer-events-auto" data-panel="toc">
|
||||
@ReaderTOCPanel(metadata)
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right Sidebar (Settings, Navigator, Bookmarks) -->
|
||||
<div id="right-sidebar" class="absolute right-0 top-0 bottom-0 pointer-events-auto flex flex-col">
|
||||
<div id="settings-panel" x-show="settingsOpen" x-transition class="panel-container pointer-events-auto" data-panel="settings">
|
||||
@ReaderSettingsPanel()
|
||||
</div>
|
||||
<div id="navigator-panel" x-show="navigatorOpen" x-transition class="panel-container pointer-events-auto" data-panel="navigator">
|
||||
@ReaderNavigatorPanel()
|
||||
</div>
|
||||
<div id="bookmarks-panel" x-show="bookmarksOpen" x-transition class="panel-container pointer-events-auto" data-panel="bookmarks">
|
||||
@ReaderBookmarksPanel(bookmarks)
|
||||
</div>
|
||||
<!-- Dockable Panels Container -->
|
||||
<div id="reader-panels" class="fixed inset-0 pointer-events-none z-30">
|
||||
<!-- Left Sidebar (TOC) -->
|
||||
<div id="left-sidebar" class="absolute left-0 pointer-events-auto flex flex-col" style="top: 56px; bottom: 56px;">
|
||||
<div id="toc-panel" x-show="tocOpen" x-transition class="panel-container pointer-events-auto" data-panel="toc">
|
||||
@ReaderTOCPanel(metadata)
|
||||
</div>
|
||||
</div>
|
||||
<div id="reader-viewport" class="absolute inset-x-0" style="top: 56px; bottom: 56px;">
|
||||
<foliate-view id="reader-view" class="block w-full h-full"></foliate-view>
|
||||
<!-- Right Sidebar (Settings, Bookmarks) -->
|
||||
<div id="right-sidebar" class="absolute right-0 pointer-events-auto flex flex-col" style="top: 56px; bottom: 56px;">
|
||||
<div id="settings-panel" x-show="settingsOpen" x-transition class="panel-container pointer-events-auto" data-panel="settings">
|
||||
@ReaderSettingsPanel()
|
||||
</div>
|
||||
<div id="bookmarks-panel" x-show="bookmarksOpen" x-transition class="panel-container pointer-events-auto" data-panel="bookmarks">
|
||||
@ReaderBookmarksPanel()
|
||||
</div>
|
||||
</div>
|
||||
@DictionaryPopup()
|
||||
</body>
|
||||
</div>
|
||||
<div id="reader-viewport" class="absolute inset-x-0" style="top: 56px; bottom: 56px;">
|
||||
<foliate-view id="reader-view" class="block w-full h-full"></foliate-view>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
@@ -186,8 +199,8 @@ templ ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center gap-1">
|
||||
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents">📖</button>
|
||||
<button @click="addBookmark()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmark">🏷️</button>
|
||||
<button @click="toggleBookmarks()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Notes">📝</button>
|
||||
<button @click="addBookmark()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmark this position">🏷️</button>
|
||||
<button @click="toggleBookmarks()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmarks">📝</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -211,14 +224,6 @@ templ ReaderSettingsPanel() {
|
||||
<!-- Display settings -->
|
||||
<div class="mb-6">
|
||||
<h3 class="font-semibold mb-2">Display</h3>
|
||||
<label class="block mb-2">
|
||||
Chrome Behavior
|
||||
<select name="chrome_behavior" class="w-full mt-1 px-3 py-2 rounded border">
|
||||
<option value="auto-hide">Auto Hide</option>
|
||||
<option value="always-visible">Always Visible</option>
|
||||
<option value="hide-on-scroll">Hide on Scroll</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="block mb-2">
|
||||
Progress Mode
|
||||
<select name="progress_mode" x-model="progressMode" @change="applyProgressMode()" class="w-full mt-1 px-3 py-2 rounded border">
|
||||
@@ -356,26 +361,7 @@ templ ReaderTOCPanel(_ ReaderMetadata) {
|
||||
</div>
|
||||
}
|
||||
|
||||
templ ReaderNavigatorPanel() {
|
||||
<div
|
||||
class="dockable-panel"
|
||||
data-panel="navigator"
|
||||
data-side="right"
|
||||
x-ref="navigatorPanel"
|
||||
>
|
||||
<div class="panel-header flex items-center justify-between p-3 cursor-pointer" @click="toggleWindowShade($refs.navigatorPanel)">
|
||||
<h3 class="panel-title font-semibold">🗺️ Navigator</h3>
|
||||
<div class="panel-controls flex items-center gap-2">
|
||||
<button class="window-shade-toggle">─</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-content p-2 overflow-hidden">
|
||||
<div id="navigator-viewport" class="relative w-full h-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ ReaderBookmarksPanel(bookmarks []Bookmark) {
|
||||
templ ReaderBookmarksPanel() {
|
||||
<div
|
||||
class="dockable-panel"
|
||||
data-panel="bookmarks"
|
||||
@@ -389,32 +375,34 @@ templ ReaderBookmarksPanel(bookmarks []Bookmark) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-content p-4 overflow-y-auto">
|
||||
if len(bookmarks) > 0 {
|
||||
<div id="bookmarks-list" class="space-y-2">
|
||||
for _, bookmark := range bookmarks {
|
||||
<div id="bookmarks-list" class="space-y-2">
|
||||
<template x-for="bookmark in bookmarkItems" :key="bookmark.id">
|
||||
<div class="flex items-center gap-1 group">
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="goToBookmarkTarget($el.dataset.cfi)"
|
||||
data-cfi={ bookmark.CfiPosition }
|
||||
class="block py-2 hover:bg-gray-700 rounded px-2"
|
||||
@click.prevent="goToBookmark(bookmark)"
|
||||
class="flex-1 min-w-0 block py-2 hover:bg-gray-700 rounded px-2"
|
||||
>
|
||||
<span class="font-medium">{ bookmark.Title }</span>
|
||||
<span class="text-xs ml-2" style="color: var(--text-secondary)">
|
||||
{ bookmark.Position }
|
||||
</span>
|
||||
<span class="font-medium block truncate" x-text="bookmark.title"></span>
|
||||
<span class="text-xs block truncate" style="color: var(--text-secondary)" x-text="bookmark.positionLabel"></span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<p class="text-sm" style="color: var(--text-secondary)">No bookmarks yet</p>
|
||||
}
|
||||
<button
|
||||
@click="deleteBookmark(bookmark.id)"
|
||||
class="p-2 rounded hover:bg-red-900/60 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
title="Delete bookmark"
|
||||
aria-label="Delete bookmark"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="bookmarkItems.length === 0">
|
||||
<p class="text-sm" style="color: var(--text-secondary)">No bookmarks yet</p>
|
||||
</template>
|
||||
</div>
|
||||
<button @click="addBookmark()" class="w-full py-2 mt-4 bg-blue-600 text-white rounded hover:bg-blue-700">
|
||||
+ Add Bookmark
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ DictionaryPopup() {
|
||||
<div id="dictionary-popup" class="hidden fixed bg-white text-black p-4 rounded-lg shadow-xl max-w-md z-50"></div>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user