audio_reader.templ was the only template emitting <!DOCTYPE html/> (trailing slash is a parse error), which put the audio player page in quirks mode — document.compatMode was BackCompat while every other page is CSS1Compat. Quirks layout rules are a standing hazard for the player's h-screen/flex page even where it happens to render. Drop the slash.
472 lines
23 KiB
Templ
472 lines
23 KiB
Templ
package templates
|
||
|
||
// 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"
|
||
|
||
templ AudioReader(user User, metadata ReaderMetadata) {
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<title>{ "Listening · " + metadata.Title } · 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={ metadata.MediaItemID }
|
||
data-title={ metadata.Title }
|
||
data-cover={ metadata.CoverImagePath }
|
||
data-file-url={ metadata.FileURL }
|
||
data-json={ fmt.Sprintf(`{"chapterEndpoint":"%s","progressEndpoint":"%s"}`,
|
||
"/api/media-items/"+metadata.MediaItemID+"/chapters",
|
||
"/api/media-items/"+metadata.MediaItemID+"/progress") }
|
||
>
|
||
<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
|
||
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 metadata.CoverImagePath != "" {
|
||
<img
|
||
src={ metadata.CoverImagePath }
|
||
alt={ metadata.Title }
|
||
class="w-full h-full object-cover"
|
||
onerror="this.style.display='none'"
|
||
/>
|
||
} else {
|
||
<span class="text-5xl flex items-center justify-center h-full" style="color: var(--text-secondary);">🎧</span>
|
||
}
|
||
</div>
|
||
<div class="max-w-md px-4">
|
||
<h1 class="font-semibold text-lg leading-snug line-clamp-2" style="color: var(--text-primary);">{ metadata.Title }</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
|
||
(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;
|
||
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
|
||
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>
|
||
}
|