From 2c714dcb60ef3cc8d28fa5a3fa2b3ec22d453aa5 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Thu, 15 Oct 2026 18:49:42 -0400 Subject: [PATCH] fix(audio player + dashboard): markdown bookmark notes, horizontal volume, transport glyphs, client-side badges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bookmarks: notes are markdown now — capture and drawer edit are textareas (raw source), display renders through the reader's markdown-it + DOMPurify pipeline (renderNoteHtml) as x-html; the edit textarea drops the enter-to-save handler so newlines survive. Volume: the panel dragger is horizontal (mute button beside it, 7.5rem slider) per user feedback — vertical orientation read as a scrubber clone at a glance. Play/pause: unicode glyphs replaced with SVG — the triangle is optically centered (2px right-bias) and the pause bars are longer and thicker rounded rects (4.2×15 at rx 1.7). Dashboard: the client-side card renderer (dashboard.ts — the path the JSON sections actually render through) now mirrors the templ BookCard: cover-corner badges for audiobook/comic/manga (books unbadged) and the hover action shows headphones with 'Listen to …' labels for audiobooks. BookInfo type + Go struct carry format_group end to end. Verified on-device: pause glyph centered/thicker, horizontal volume panel, markdown note ('**bold**' + list) rendered in the bookmarks drawer, badges on dashboard cards, sections JSON carrying format_group. --- templates/audio_reader.templ | 31 ++++++++++++++++--------- templates/audio_reader_templ.go | 2 +- web/src/audio.ts | 7 ++++++ web/src/dashboard.ts | 40 +++++++++++++++++++++++++++++---- web/src/types/api.d.ts | 1 + 5 files changed, 65 insertions(+), 16 deletions(-) diff --git a/templates/audio_reader.templ b/templates/audio_reader.templ index 17cf52a3..a6996cb8 100644 --- a/templates/audio_reader.templ +++ b/templates/audio_reader.templ @@ -155,8 +155,17 @@ templ AudioReader(user User, metadata ReaderMetadata) { style="background-color: var(--accent); color: var(--bg-primary);" aria-label="Play or pause" > - ▶ - ❚❚ + + + + + + -

+
- +
@@ -443,12 +452,12 @@ templ AudioReader(user User, metadata ReaderMetadata) { style="background-color: var(--bg-secondary); border: 1px solid var(--border);" >

Bookmark at 0:00

- + rows="3" + placeholder="Optional note — markdown welcome (e.g. **great quote**, list, [link](https://…))" + class="input mb-2 resize-y" + >
diff --git a/templates/audio_reader_templ.go b/templates/audio_reader_templ.go index 97433c51..c6918af0 100644 --- a/templates/audio_reader_templ.go +++ b/templates/audio_reader_templ.go @@ -172,7 +172,7 @@ func AudioReader(user User, metadata ReaderMetadata) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

—

0:00 0:00
Chapters

No chapters detected for this audiobook.

Bookmarks

No bookmarks yet — tap the button to save your position.

Bookmark at 0:00

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

—

0:00 0:00
Chapters

No chapters detected for this audiobook.

Bookmarks

No bookmarks yet — tap the button to save your position.

Bookmark at 0:00

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/web/src/audio.ts b/web/src/audio.ts index 608afa61..49cdb415 100644 --- a/web/src/audio.ts +++ b/web/src/audio.ts @@ -13,6 +13,7 @@ // tracks) — the mobile-web native feel from handoff item 6. import { Alpine } from "./alpine"; +import { renderNoteHtml } from "./markdown"; import { showToast } from "./toast"; type AudioChapter = { index: number; title: string; start: number; end: number }; @@ -375,6 +376,12 @@ Alpine.data("audioReader", () => ({ } }, + // Markdown notes: the drawer renders sanitized HTML from the same + // pipeline the reader's annotations use; editing stays raw markdown. + bookmarkNoteHtml(b: Record): string { + return renderNoteHtml(String(b.notes ?? "")); + }, + bookmarkSeconds(position: string): number { const n = parseInt(String(position).slice("audio:".length), 10); return Number.isFinite(n) ? n : 0; diff --git a/web/src/dashboard.ts b/web/src/dashboard.ts index b2ea6bd0..52830b51 100644 --- a/web/src/dashboard.ts +++ b/web/src/dashboard.ts @@ -286,29 +286,61 @@ const BOOK_OPEN_ICON = '' + '' + ""; +const HEADPHONES_ICON = + '"; +const COMIC_ICON = + '"; +const MANGA_ICON = + '"; +// Media-type identifier for mixed grids: '' for books (unbadged default) +function badgeIconFor(formatGroup: string): string { + if (formatGroup === "audiobook") return HEADPHONES_ICON; + if (formatGroup === "comic") return COMIC_ICON; + if (formatGroup === "manga") return MANGA_ICON; + return ""; +} + function renderBookCard(book: BookInfo): string { const coverUrl = book.cover_image_path || "/static/placeholder-book.svg"; const actionHref = book.has_conflict ? `/media/${book.media_item_id}` : `/readers/${book.media_item_id}`; + const isAudio = book.format_group === "audiobook"; const actionLabel = book.has_conflict ? `Resolve progress conflict for ${book.title}` - : `Read ${book.title}`; + : isAudio + ? `Listen to ${book.title}` + : `Read ${book.title}`; const actionTitle = book.has_conflict ? "Resolve progress conflict" - : "Read"; + : isAudio + ? "Listen" + : "Read"; + const actionIcon = isAudio ? HEADPHONES_ICON : BOOK_OPEN_ICON; + const badgeIcon = badgeIconFor(book.format_group || ""); return `
diff --git a/web/src/types/api.d.ts b/web/src/types/api.d.ts index ad84ff20..fbb308fa 100644 --- a/web/src/types/api.d.ts +++ b/web/src/types/api.d.ts @@ -102,6 +102,7 @@ interface BookInfo { author: string; cover_image_path: string; has_conflict: boolean; + format_group: string; } // Dashboard type definitions