feat(annotations + player): bookmark rows in the banner, stacked volume panel, badge parity for comics/manga
Annotations banner: audio/text bookmarks join notes and highlights — third row kind (bookmark glyph indicator, H:MM:SS / Page N / Chapter N friendly labels), edit-note/delete actions branch by kind, and the banner's export now covers all annotations. The archived ledger (ListDeletedAnnotationsForBook) already tombstoned bookmarks equally and Restore/Purge take annotation_type=bookmark — verified live: delete → restore brings the row back with its note intact. Volume: the dragger sits BELOW the mute icon in the panel (stacked flyout shape) with the horizontal slider from the last round. Badges: the real fix for comics/manga — format_group only knows 'comic_archive' (manga_type is unreliable across sources), so the comic/manga split is resolved from the LIBRARY type server-side. handlers.MediaBadgeKind + BookInfo.Badge at every construction site (dashboard SSR + sections API, collections, series, and the bookshelf search grid — which was also missing FormatGroup entirely, so no card ever badged there). Both renderers (templ + dashboard.ts) switch on the resolved badge. Verified: All-Books grid renders 2 Comic + 3 Manga badges, dashboard the audiobook one. Infra: header.templ main.js src gains ?v=<stamp> — browsers keyed an intermediate bundle by URL and served it across rebuilds (the stale 'Add note' the user saw); bump the stamp when JS stale-shadows.
This commit is contained in:
1 parent
2c714dcb60
commit
89665fb2bc
19 files changed
+273
-92
No files matched your search
@@ -13,8 +13,8 @@ import { renderNoteHtml, stripNote } from "./markdown";
|
||||
interface AnnotationRow {
|
||||
/** stable key for x-for */
|
||||
key: string;
|
||||
/** note | highlight */
|
||||
kind: "note" | "highlight";
|
||||
/** note | highlight | bookmark */
|
||||
kind: "note" | "highlight" | "bookmark";
|
||||
/** human location label (Section N / Page N / NN% / source words) */
|
||||
label: string;
|
||||
/** compact preview line: stripped markdown (note) or capped selection */
|
||||
@@ -83,9 +83,32 @@ function pdfEnvelopePage(cfi: string): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Bookmark position labels: audio:<s> renders H:MM:SS; page:N /
|
||||
* comic:page:N renders Page N; chapter:N renders Chapter N (0-based in
|
||||
* storage); else the bookmark title's own words. */
|
||||
function bookmarkLabel(position: string | null | undefined, title: string): string {
|
||||
const raw = (position ?? "").trim();
|
||||
if (raw.startsWith("audio:")) {
|
||||
const sec = parseInt(raw.slice(6), 10);
|
||||
if (Number.isFinite(sec) && sec >= 0) {
|
||||
const h = Math.floor(sec / 3600);
|
||||
const m = Math.floor((sec % 3600) / 60);
|
||||
const sec2 = sec % 60;
|
||||
const two = (n: number) => String(n).padStart(2, "0");
|
||||
return h > 0 ? `${h}:${two(m)}:${two(sec2)}` : `${m}:${two(sec2)}`;
|
||||
}
|
||||
}
|
||||
const pageM = /^(?:comic:)?page:(\d+)$/.exec(raw);
|
||||
if (pageM) return `Page ${pageM[1]}`;
|
||||
const chapM = /^chapter:(\d+)$/.exec(raw);
|
||||
if (chapM) return `Chapter ${Number(chapM[1]) + 1}`;
|
||||
return title || "Bookmark";
|
||||
}
|
||||
|
||||
function buildAnnotationRows(
|
||||
notes: any[],
|
||||
highlights: any[],
|
||||
bookmarks: any[] = [],
|
||||
): { rows: AnnotationRow[]; sources: AnnotationSource[] } {
|
||||
const rows: AnnotationRow[] = [];
|
||||
const sources: AnnotationSource[] = [];
|
||||
@@ -161,6 +184,32 @@ function buildAnnotationRows(
|
||||
sources.push({ row, source: note, highlightText: selection });
|
||||
}
|
||||
|
||||
for (const b of bookmarks) {
|
||||
if (b.deleted) continue;
|
||||
const notesrc = String(b.notes ?? "");
|
||||
const title = String(b.title ?? "");
|
||||
const label = bookmarkLabel(b.position, title);
|
||||
const loc = String(b.position ?? "");
|
||||
const row: AnnotationRow = {
|
||||
key: `bookmark-${b.id}`,
|
||||
kind: "bookmark",
|
||||
label,
|
||||
preview: title || label,
|
||||
richSelection: "",
|
||||
richNote: renderNoteHtml(notesrc),
|
||||
loc,
|
||||
color: "var(--accent)",
|
||||
hasNote: notesrc.trim().length > 0,
|
||||
expanded: false,
|
||||
};
|
||||
rows.push(row);
|
||||
sources.push({
|
||||
row,
|
||||
source: notesrc,
|
||||
highlightText: title || label,
|
||||
});
|
||||
}
|
||||
|
||||
return { rows, sources };
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user