Files
bookhoard/templates/audio_reader_templ.go
T
John O'Keefe 73ae1789c5 feat(reader): fullscreen mode; revert stacked volume; fix manga badges + ghost sidebar
Reader fullscreen: the topbar gains a fullscreen toggle (icon state + f
shortcut + help row; f is guarded so Ctrl/Cmd+F stays find-in-page).
Enter requests the Fullscreen API with fallbacks (desktop/Android get a
true full display; iOS Safari has no page-fullscreen API and falls back
to the soft visual mode only). Margins: the foliate paginator's closed
shadow root only yields through its observed 'margin' attribute, so
fullscreen fits a comfortable ratio (6% of the short side, floored at
48px) and floors it by the measured env insets — Android reports true
cutout insets in fullscreen, so portrait phones keep text clear of the
camera band; iOS letterboxes are already env-padded chrome. Fixed-layout
renderers skip it (fxl/webtoon pack pages edge-to-edge by design).
Exits the user makes outside the toggle (Esc, browser UI) sync via
fullscreenchange(+webkit) and restore the ordinary 48px margin; a
position-keeping re-render follows the ResizeObserver re-entry path.
Verified on-device: enter -> true fullscreen + icon swap + camera band
clear, exit -> chrome back, margins restored, position kept.

Volume: revert the stacked panel — the popup returns to the liked shape
(mute button left, horizontal dragger right, dialogue above the strip);
one-line flex restore of the pre-89665fb layout.

Manga badges: MediaBadgeKind makes the library type authoritative for
the comic/manga split — manga volumes ship as fixed_layout scans as
often as comic_archive (and manga_type is unreliable across sources:
unknown/NULL on real rows), so the old comic_archive+manga mapping left
the scan volumes unbadged. Anything in a manga library reads manga;
anything in a comics library reads comic. Verified: /dashboard lands 24
title="Manga" cover badges for the Manga library and the sections API
reports badge=manga for a fixed_layout item.

Ghost sidebar: the >=1024px media block applied body
padding-left:var(--sidebar-w) unconditionally, so pages that never
render the sidebar (logged-out pages, the audio player, the reader
itself) carried a phantom 16rem left margin that pushed content off
center on desktop widths. The body:has(.app-sidebar) rule below was
already the correct conditional source — it is now the only one.
Verified in the built stylesheet: exactly one such rule remains.

Infra: main.js ?v=3 stamp bump (reader.ts changed; hashed chunks churn
their URLs themselves).
2026-10-15 20:27:37 -04:00

184 lines
28 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// AudioReader is the reader surface for audiobooks (media_items with
// format_group = "audiobook"): cover, chapter, scrubber, skip/speed
// controls and the chapter list. Standalone page — the foliate document
// engine is deliberately not involved (handoff item 3 Batch A); the
// <audio> element streams the m4b through the existing cookie-authed
// Range-serving /uploads path.
import "fmt"
func AudioReader(user User, metadata ReaderMetadata) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html/><html lang=\"en\"><head><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("Listening · " + metadata.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/audio_reader.templ`, Line: 16, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " · Bookhoard</title><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><link href=\"/static/style.css\" rel=\"stylesheet\"><script type=\"module\" src=\"/static/main.js\"></script></head><body x-data=\"audioReader\" x-init=\"init()\" class=\"theme-{ user.Theme } h-screen overflow-hidden\" style=\"background-color: var(--bg-primary);\" data-media-item-id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(metadata.MediaItemID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/audio_reader.templ`, Line: 27, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" data-title=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(metadata.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/audio_reader.templ`, Line: 28, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" data-cover=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(metadata.CoverImagePath)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/audio_reader.templ`, Line: 29, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" data-file-url=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(metadata.FileURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/audio_reader.templ`, Line: 30, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" data-json=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf(`{"chapterEndpoint":"%s","progressEndpoint":"%s"}`,
"/api/media-items/"+metadata.MediaItemID+"/chapters",
"/api/media-items/"+metadata.MediaItemID+"/progress"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/audio_reader.templ`, Line: 33, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><div class=\"h-full flex flex-col items-center justify-center px-4 gap-6 select-none\"><!-- Header: back to the detail page (reader-style exit) and the\n\t\t\t chapter launcher. Floating drawer, never in-flow (A2b). --><div class=\"absolute top-0 inset-x-0 z-30 flex items-center justify-between px-4 py-3\"><button type=\"button\" @click=\"backToDetail()\" class=\"rounded-full p-2 hover:bg-surface-hover transition-colors\" style=\"color: var(--text-primary);\" aria-label=\"Back to book details\"><svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M19 12H5\"></path> <path d=\"m12 19-7-7 7-7\"></path></svg></button> <span class=\"inline-flex items-center gap-2\"><button type=\"button\" @click=\"bookmarksOpen = !bookmarksOpen\" class=\"rounded-full p-2 hover:bg-surface-hover transition-colors\" style=\"color: var(--text-primary); border: 1px solid var(--border); background-color: color-mix(in srgb, var(--bg-secondary) 80%, transparent);\" :aria-label=\"'Open bookmarks (' + bookmarks.length + ')'\" title=\"Bookmarks\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z\"></path></svg></button> <button type=\"button\" @click=\"chaptersOpen = !chaptersOpen\" class=\"text-sm font-semibold px-4 py-2 rounded-full hover:bg-surface-hover transition-colors inline-flex items-center gap-1.5\" style=\"color: var(--text-primary); border: 1px solid var(--border); background-color: color-mix(in srgb, var(--bg-secondary) 80%, transparent);\" :aria-label=\"'Open chapter list (currently chapter ' + (currentChapterIdx + 1) + ' of ' + (chapters.length || 1) + ')'\" title=\"Chapters\"><span>Chapter</span> <span class=\"tabular-nums\" x-text=\"(currentChapterIdx + 1) + '/' + (chapters.length || 1)\">1/1</span> <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"></path></svg></button></span></div><!-- Cover + title --><div class=\"flex flex-col items-center gap-3 text-center\"><div class=\"w-48 h-72 md:w-56 md:h-80 rounded-2xl overflow-hidden shadow-lg shrink-0 bg-cover bg-center\" style=\"background-color: var(--surface-hover);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if metadata.CoverImagePath != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(metadata.CoverImagePath)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/audio_reader.templ`, Line: 86, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(metadata.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/audio_reader.templ`, Line: 87, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"w-full h-full object-cover\" onerror=\"this.style.display='none'\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span class=\"text-5xl flex items-center justify-center h-full\" style=\"color: var(--text-secondary);\">🎧</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div><div class=\"max-w-md px-4\"><h1 class=\"font-semibold text-lg leading-snug line-clamp-2\" style=\"color: var(--text-primary);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/audio_reader.templ`, Line: 96, Col: 117}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</h1><p class=\"text-sm mt-0.5 truncate\" style=\"color: var(--text-secondary);\" x-text=\"chapterTitle()\">—</p></div></div><!-- Scrubber + transports --><div class=\"w-full max-w-xl flex flex-col gap-2\"><div class=\"flex items-center gap-2 text-xs tabular-nums\" style=\"color: var(--text-secondary);\"><span x-text=\"fmtTime(currentTime)\">0:00</span> <input id=\"audio-scrub\" type=\"range\" min=\"0\" step=\"0.1\" :max=\"chaptersDuration() || 1\" :value=\"scrubbing ? scrubValue : currentTime\" class=\"flex-1 accent-current\" @input=\"scrubbing = true; scrubValue = Number($event.target.value)\" @pointerup=\"seekTo(scrubValue); scrubbing = false\" @change=\"seekTo(scrubValue); scrubbing = false\" style=\"z-index: 20;\"> <span x-text=\"fmtTime(chaptersDuration() || 1)\">0:00</span></div><div class=\"flex items-center justify-center gap-3 md:gap-5\"><button type=\"button\" @click=\"stepChapter(-1)\" class=\"rounded-full p-2.5 hover:bg-surface-hover transition-colors\" aria-label=\"Previous chapter\" title=\"Previous chapter\" style=\"color: var(--text-primary);\"><svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M19 20 9 12l10-8v16z\"></path> <path d=\"M5 19V5\"></path></svg></button> <button type=\"button\" @click=\"skip(-1)\" class=\"relative rounded-full h-14 w-14 grid place-items-center hover:bg-surface-hover transition-colors\" :aria-label=\"'Skip back ' + skipSeconds + 's'\" :title=\"'Skip back ' + skipSeconds + 's'\" style=\"color: var(--text-primary);\"><svg width=\"52\" height=\"52\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\"></path> <path d=\"M3 3v5h5\"></path></svg> <span class=\"absolute inset-0 grid place-items-center text-[11px] font-semibold pointer-events-none\"><span x-text=\"skipSeconds\">10</span></span></button> <button id=\"audio-play\" type=\"button\" @click=\"togglePlay()\" class=\"rounded-full h-16 w-16 grid place-items-center\" style=\"background-color: var(--accent); color: var(--bg-primary);\" aria-label=\"Play or pause\"><span x-show=\"!playing\" class=\"grid place-items-center\"><svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\" style=\"margin-left: 2px;\"><path d=\"M8 5.2v13.6L19.2 12z\"></path></svg></span> <span x-show=\"playing\" class=\"grid place-items-center\" x-cloak><svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><rect x=\"6.2\" y=\"4.5\" width=\"4.2\" height=\"15\" rx=\"1.7\"></rect> <rect x=\"13.6\" y=\"4.5\" width=\"4.2\" height=\"15\" rx=\"1.7\"></rect></svg></span></button> <button type=\"button\" @click=\"skip(1)\" class=\"relative rounded-full h-14 w-14 grid place-items-center hover:bg-surface-hover transition-colors\" :aria-label=\"'Skip forward ' + skipSeconds + 's'\" :title=\"'Skip forward ' + skipSeconds + 's'\" style=\"color: var(--text-primary);\"><svg width=\"52\" height=\"52\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8\"></path> <path d=\"M21 3v5h-5\"></path></svg> <span class=\"absolute inset-0 grid place-items-center text-[11px] font-semibold pointer-events-none\"><span x-text=\"skipSeconds\">10</span></span></button> <button type=\"button\" @click=\"stepChapter(1)\" class=\"rounded-full p-2.5 hover:bg-surface-hover transition-colors\" aria-label=\"Next chapter\" title=\"Next chapter\" style=\"color: var(--text-primary);\"><svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M5 4l10 8-10 8V4z\"></path> <path d=\"M19 5v14\"></path></svg></button></div></div><!-- Settings strip: Skip ▾ · Speed ▾ · Bookmark · Volume ▾ --><div class=\"w-full max-w-xl flex items-center justify-center gap-2\"><div class=\"relative\" @click.outside=\"skipOpen = false\"><button type=\"button\" @click=\"skipOpen = !skipOpen\" class=\"text-xs px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors inline-flex items-center gap-1.5\" :aria-label=\"'Skip duration, currently ' + skipSeconds + ' seconds'\" style=\"color: var(--text-secondary);\"><span>Skip</span> <span class=\"font-semibold\" style=\"color: var(--text-primary);\" x-text=\"skipSeconds + 's'\">10s</span> <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"></path></svg></button><div x-show=\"skipOpen\" x-cloak class=\"absolute bottom-full mb-2 left-1/2 -translate-x-1/2 rounded-xl shadow-lg p-1.5 flex flex-col min-w-[7.5rem]\" style=\"background-color: var(--bg-secondary); border: 1px solid var(--border); z-index: 30;\"><template x-for=\"secs in [1, 5, 10, 15, 30]\" :key=\"'skip' + secs\"><button type=\"button\" @click=\"setSkip(secs); skipOpen = false\" class=\"w-full text-left px-3 py-1.5 rounded-md text-sm\" :class=\"Number(skipSeconds) === secs ? 'font-semibold' : ''\" :style=\"Number(skipSeconds) === secs ? 'background-color: var(--accent-muted); color: var(--text-primary);' : 'color: var(--text-secondary);'\" x-text=\"secs + ' seconds'\"></button></template></div></div><div class=\"relative\" @click.outside=\"speedOpen = false\"><button type=\"button\" @click=\"speedOpen = !speedOpen\" class=\"text-xs px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors inline-flex items-center gap-1.5\" :aria-label=\"'Playback speed, currently ' + speed + ' times'\" style=\"color: var(--text-secondary);\"><span>Speed</span> <span class=\"font-semibold\" style=\"color: var(--text-primary);\" x-text=\"speed + '×'\">1×</span> <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"></path></svg></button><div x-show=\"speedOpen\" x-cloak class=\"absolute bottom-full mb-2 left-1/2 -translate-x-1/2 rounded-xl shadow-lg p-1.5 flex flex-col min-w-[7rem]\" style=\"background-color: var(--bg-secondary); border: 1px solid var(--border); z-index: 30;\"><template x-for=\"rate in [0.75, 1, 1.25, 1.5, 2]\" :key=\"'speed' + rate\"><button type=\"button\" @click=\"setSpeed(rate); speedOpen = false\" class=\"w-full text-left px-3 py-1.5 rounded-md text-sm\" :class=\"Number(speed) === rate ? 'font-semibold' : ''\" :style=\"Number(speed) === rate ? 'background-color: var(--accent-muted); color: var(--text-primary);' : 'color: var(--text-secondary);'\" x-text=\"rate + '×'\"></button></template></div></div><button type=\"button\" @click=\"toggleBookmarkCapture()\" class=\"text-xs px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors inline-flex items-center gap-1.5\" :class=\"bookmarkCaptureOpen ? 'opacity-100' : 'opacity-70'\" aria-label=\"Add bookmark at current position\" style=\"color: var(--text-secondary);\"><svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z\"></path> <path d=\"M12 7v6M9 10h6\"></path></svg> <span>Bookmark</span></button><div class=\"relative\" @click.outside=\"volumeOpen = false\"><button type=\"button\" @click=\"volumeOpen = !volumeOpen\" class=\"rounded-lg p-2 hover:bg-surface-hover transition-colors\" :aria-label=\"'Volume ' + Math.round(Number(volume) * 100) + '%'\" style=\"color: var(--text-secondary);\"><span x-show=\"volumeIcon() === 'muted'\" x-cloak><svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M11 5 6 9H2v6h4l5 4z\"></path> <path d=\"m17 9 4 6M21 9l-4 6\"></path></svg></span> <span x-show=\"volumeIcon() === 'low'\"><svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M11 5 6 9H2v6h4l5 4z\"></path> <path d=\"M15.5 9.5a3.5 3.5 0 0 1 0 5\"></path></svg></span> <span x-show=\"volumeIcon() === 'mid'\"><svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M11 5 6 9H2v6h4l5 4z\"></path> <path d=\"M14.5 8a5 5 0 0 1 0 8\"></path> <path d=\"M17.5 5.5a9 9 0 0 1 0 13\"></path></svg></span> <span x-show=\"volumeIcon() === 'high'\"><svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M11 5 6 9H2v6h4l5 4z\"></path> <path d=\"M14.5 8a5 5 0 0 1 0 8\"></path> <path d=\"M17.5 5.5a9 9 0 0 1 0 13\"></path> <path d=\"M20.5 3a13 13 0 0 1 0 18\"></path></svg></span></button><div x-show=\"volumeOpen\" x-cloak class=\"absolute bottom-full mb-2 left-1/2 -translate-x-1/2 rounded-xl shadow-lg px-3 py-2.5 flex items-center gap-3\" style=\"background-color: var(--bg-secondary); border: 1px solid var(--border); z-index: 30;\"><button type=\"button\" @click=\"toggleMute()\" class=\"rounded-full p-1.5 hover:bg-surface-hover transition-colors\" :aria-label=\"muted ? 'Unmute' : 'Mute'\" style=\"color: var(--text-secondary);\"><span x-show=\"muted\" x-cloak class=\"block\" style=\"color: var(--text-primary);\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M11 5 6 9H2v6h4l5 4z\"></path> <path d=\"m17 9 4 6M21 9l-4 6\"></path></svg></span> <span x-show=\"!muted\" class=\"block\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M11 5 6 9H2v6h4l5 4z\"></path> <path d=\"M14.5 8a5 5 0 0 1 0 8\"></path> <path d=\"M17.5 5.5a9 9 0 0 1 0 13\"></path></svg></span></button> <input type=\"range\" min=\"0\" max=\"1\" step=\"0.05\" :value=\"muted ? 0 : volume\" @input=\"setVolume(Number($event.target.value))\" class=\"accent-current\" style=\"width: 7.5rem;\" aria-label=\"Volume\"></div></div></div><!-- Chapter drawer: slides top-to-bottom, floats above the page\n\t\t\t (backdrop overlay — the page never shifts) --><div x-show=\"chaptersOpen\" x-cloak class=\"fixed inset-0 z-40\" style=\"background-color: color-mix(in srgb, black 32%, transparent);\" @click=\"chaptersOpen = false\"></div><div x-show=\"chaptersOpen\" x-cloak x-transition:enter=\"transition ease-out duration-200\" x-transition:enter-start=\"-translate-y-4 opacity-0\" x-transition:enter-end=\"translate-y-0 opacity-100\" x-transition:leave=\"transition ease-in duration-150\" x-transition:leave-start=\"translate-y-0 opacity-100\" x-transition:leave-end=\"-translate-y-4 opacity-0\" class=\"fixed top-0 right-0 z-50 w-full sm:w-96 max-h-[70vh] overflow-y-auto rounded-b-2xl shadow-2xl p-3\" style=\"background-color: var(--bg-secondary); border: 1px solid var(--border); border-top-width: 0;\" @click.outside=\"chaptersOpen = false\"><div class=\"flex items-center justify-between mb-2 px-1\"><span class=\"text-sm font-semibold\" style=\"color: var(--text-primary);\">Chapters</span> <button type=\"button\" @click=\"chaptersOpen = false\" class=\"rounded-full p-1.5 hover:bg-surface-hover\" style=\"color: var(--text-secondary);\" aria-label=\"Close chapters\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M18 6 6 18\"></path><path d=\"m6 6 12 12\"></path></svg></button></div><template x-for=\"ch in chapters\" :key=\"ch.index\"><button type=\"button\" @click=\"jumpToChapter(ch.index)\" class=\"w-full text-left px-3 py-2.5 rounded-lg text-sm flex justify-between gap-3 hover:bg-surface-hover transition-colors\" :class=\"currentChapter() === ch.index ? 'font-semibold' : ''\" :style=\"currentChapter() === ch.index ? 'background-color: var(--accent-muted); color: var(--text-primary);' : 'color: var(--text-secondary);'\"><span class=\"truncate\" x-text=\"ch.title || ('Chapter ' + (ch.index + 1))\"></span> <span class=\"shrink-0 text-xs tabular-nums\" x-text=\"fmtTime(ch.start)\"></span></button></template><p x-show=\"!chapters.length\" class=\"text-sm text-center py-4\" style=\"color: var(--text-secondary);\">No chapters detected for this audiobook.</p></div><!-- Bookmarks drawer: same slide-down panel shape as chapters;\n\t\t\t note is the point (A2d) --><div x-show=\"bookmarksOpen\" x-cloak x-transition:enter=\"transition ease-out duration-200\" x-transition:enter-start=\"-translate-y-4 opacity-0\" x-transition:enter-end=\"translate-y-0 opacity-100\" x-transition:leave=\"transition ease-in duration-150\" x-transition:leave-start=\"translate-y-0 opacity-100\" x-transition:leave-end=\"-translate-y-4 opacity-0\" class=\"fixed top-0 right-0 z-50 w-full sm:w-96 max-h-[70vh] overflow-y-auto rounded-b-2xl shadow-2xl p-3\" style=\"background-color: var(--bg-secondary); border: 1px solid var(--border); border-top-width: 0;\"><div class=\"flex items-center justify-between mb-2 px-1\"><span class=\"text-sm font-semibold\" style=\"color: var(--text-primary);\">Bookmarks</span> <button type=\"button\" @click=\"bookmarksOpen = false\" class=\"rounded-full p-1.5 hover:bg-surface-hover\" style=\"color: var(--text-secondary);\" aria-label=\"Close bookmarks\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M18 6 6 18\"></path><path d=\"m6 6 12 12\"></path></svg></button></div><template x-for=\"b in bookmarks\" :key=\"String(b.id)\"><div class=\"px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors\"><div class=\"flex justify-between items-center gap-3\"><button type=\"button\" class=\"flex-1 text-left min-w-0\" @click=\"jumpToBookmark(b)\"><span class=\"text-sm font-medium truncate block\" style=\"color: var(--text-primary);\" x-text=\"b.title\"></span></button><div class=\"shrink-0 flex items-center gap-1\"><button type=\"button\" @click=\"startEditNote(b)\" class=\"rounded p-1 hover:bg-surface-hover\" style=\"color: var(--text-secondary);\" aria-label=\"Edit note\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z\"></path></svg></button> <button type=\"button\" @click=\"deleteBookmark(b)\" class=\"rounded p-1 hover:bg-surface-hover\" style=\"color: var(--text-secondary);\" aria-label=\"Delete bookmark\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3 6h18\"></path><path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6\"></path><path d=\"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"></path></svg></button></div></div><div class=\"note-rich text-xs mt-1\" x-show=\"b.notes && editingBookmarkId !== String(b.id)\" x-html=\"bookmarkNoteHtml(b)\"></div><div x-show=\"editingBookmarkId === String(b.id)\" x-cloak class=\"mt-1\"><textarea x-model=\"editingNote\" rows=\"3\" class=\"input text-sm resize-y\" placeholder=\"Note (markdown)\"></textarea><div class=\"flex justify-end gap-2 mt-1\"><button type=\"button\" @click=\"editingBookmarkId = ''\" class=\"btn btn-ghost text-xs px-2 py-1\">Cancel</button> <button type=\"button\" @click=\"saveNote()\" class=\"btn btn-primary text-xs px-2 py-1\">Save note</button></div></div></div></template><p x-show=\"!bookmarks.length\" class=\"text-sm text-center py-4\" style=\"color: var(--text-secondary);\">No bookmarks yet — tap the <span class=\"inline-flex align-middle\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\"><path d=\"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z\"></path></svg></span> button to save your position.</p></div><!-- Bookmark capture pill: floats under the header while playing\n\t\t\t position is live; optional note per the A2d spec --><div x-show=\"bookmarkCaptureOpen\" x-cloak x-transition class=\"fixed top-16 right-4 z-50 w-80 rounded-xl shadow-2xl p-3\" style=\"background-color: var(--bg-secondary); border: 1px solid var(--border);\"><p class=\"text-sm font-semibold mb-2\" style=\"color: var(--text-primary);\">Bookmark at <span class=\"tabular-nums\" x-text=\"fmtTime(currentTime)\">0:00</span></p><textarea x-model=\"bookmarkNote\" rows=\"3\" placeholder=\"Optional note — markdown welcome (e.g. **great quote**, list, [link](https://…))\" class=\"input mb-2 resize-y\"></textarea><div class=\"flex justify-end gap-2\"><button type=\"button\" @click=\"bookmarkCaptureOpen = false\" class=\"btn btn-ghost text-sm px-3 py-1.5\">Cancel</button> <button type=\"button\" @click=\"saveBookmark()\" class=\"btn btn-primary text-sm px-3 py-1.5\">Save</button></div></div><audio id=\"audio-element\" preload=\"auto\" class=\"hidden\"></audio></div></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate