feat(audiobooks): A2 — narrator metadata, audio-aware UX, player polish, bookmarks

A2a (metadata round): narrators TEXT[] + narrators_search (tags
pattern: accumulator + GIN pairs) + abridged bool on media_items;
scanner fills from the Libation/ABS sidecar (narrators verbatim,
abridged flag) with audio-payload promotion; editorial fields ride
the real columns while audio_metadata stays scan-derived (duration/
codec/chapters). Detail/edit/save paths carry the fields; the web
edit dialog grows an Audio section (Narrator(s) comma-input, Abridged
toggle, scanned duration read-only); search weights + narrator filter
(word_similarity over narrators_search) + narrator autocomplete added.

A2c (per-media-type wording): copy helpers keyed on format_group —
Listen Now, Mark Complete/Not Complete, Listening Progress, Your
Listening Stats, Time Listening, Time listened (pages tile swapped for
audio), Position H:MM:SS / total H:MM:SS on detail + progress pages
(duration parsed from audio_metadata). Dashboard: system collections
swap to Continue Listening / Recently Listened at DISPLAY time when
the audiobooks library is selected (stored names unchanged; restores
keep working); week card = Listening Time / Sessions / Books Touched;
cover-corner badges identify audiobook (headphones) / comic / manga
cards on mixed grids — books stay unbadged; hover action icon follows
the media type.

A2b (player): the scrubber binds value to the playhead with an explicit
scrub guard (the handle previously froze at its initial max); header
row = reader-style back arrow + Chapter n/total launcher; chapters
open in a floating top-down drawer (current chapter highlighted, page
never shifts); Skip/Speed rows share one label-left layout; honest
wall-clock session accounting (playing-only deltas, paused flush).

A2d (bookmarks): note-bearing time bookmarks ride the existing
media_bookmarks substrate (notes column + sync-aware service already
there) — audio locator convention audio:<seconds>, chapter_number
filled, auto-title '<chapter> — H:MM:SS', capture popover with optional
note, drawer with jump/edit-note/delete. Zero server changes needed.

Verified live on the Libation sample: narrators backfilled by rescan;
PUT edit persisted + overrides detected (narrators, abridged); force
rescan preserved the user edit; reset-to-scanned-of-defaults restored
truth; narrator filter/autocomplete scoped by library; dashboard
listsening wording + badges render; bookmark save with note deduped to
one row.
This commit is contained in:
John O'Keefe committed 2026-10-15 18:03:35 -04:00
1 parent 902cbe5915
commit 86f0af632e
38 files changed
+3069 -1552

No files matched your search

+266 -117
View File
@@ -33,134 +33,283 @@ templ AudioReader(user User, metadata ReaderMetadata) {
"/api/media-items/"+metadata.MediaItemID+"/progress") }
>
<div class="h-full flex flex-col items-center justify-center px-4 gap-6 select-none">
<!-- 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="material-symbols-rounded text-5xl flex items-center justify-center h-full" style="color: var(--text-secondary);">headphones</span>
}
</div>
<div>
<h1 class="font-semibold text-lg leading-snug" style="color: var(--text-primary);">{ metadata.Title }</h1>
<p class="text-sm mt-0.5" style="color: var(--text-secondary);" x-text="chapterTitle()">—</p>
</div>
</div>
<!-- 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>
<!-- 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"
:value="chaptersDuration() || 1"
class="flex-1 accent-current"
@input.debounce.300ms="seekTo($event.target.value)"
<!-- 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'"
/>
<span x-text="fmtTime(chaptersDuration() || 1)">0:00</span>
</div>
<div class="flex items-center justify-center gap-6">
<button
type="button"
@click="skip(-1)"
class="rounded-full p-3 hover:bg-surface-hover transition-colors"
:aria-label="'Skip back ' + skipSeconds + 's'"
style="color: var(--text-primary);"
>
<span class="text-lg font-semibold">-<span x-text="skipSeconds">10</span>s</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="text-3xl leading-none">▶</span>
<span x-show="playing" class="text-3xl leading-none" x-cloak>❚❚</span>
</button>
<button
type="button"
@click="skip(1)"
class="rounded-full p-3 hover:bg-surface-hover transition-colors"
:aria-label="'Skip forward ' + skipSeconds + 's'"
style="color: var(--text-primary);"
>
<span class="text-lg font-semibold">+<span x-text="skipSeconds">10</span>s</span>
</button>
</div>
} 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>
<!-- Chapter list + speed -->
<div class="w-full max-w-xl">
<div class="flex items-center justify-between">
<button
type="button"
@click="chaptersOpen = !chaptersOpen"
class="text-sm px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors"
style="color: var(--text-secondary);"
>
Chapters <span x-text="'(' + chapters.length + ')'"></span>
</button>
<div class="flex items-center gap-1.5">
<span class="text-xs" style="color: var(--text-secondary);">Skip</span>
<template x-for="secs in [1, 5, 10, 15, 30]" :key="'skip' + secs">
<button
type="button"
@click="setSkip(secs)"
class="text-xs px-2 py-1 rounded-md transition-colors"
:class="Number(skipSeconds) === secs ? 'opacity-100 underline' : 'opacity-50 hover:opacity-80'"
style="color: var(--text-secondary);"
x-text="secs + 's'"
></button>
</template>
</div>
</div>
<div class="flex items-center justify-between mt-1">
<span class="text-xs" style="color: var(--text-secondary);">Speed</span>
<div class="flex items-center gap-1.5">
<template x-for="rate in [0.75, 1, 1.25, 1.5, 2]" :key="rate">
<button
type="button"
@click="setSpeed(rate)"
class="text-xs px-2 py-1 rounded-md transition-colors"
:class="Number(speed) === rate ? 'opacity-100 underline' : 'opacity-50 hover:opacity-80'"
style="color: var(--text-secondary);"
x-text="rate + '×'"
></button>
</template>
</div>
</div>
<div x-show="chaptersOpen" x-cloak class="mt-2 max-h-64 overflow-y-auto rounded-xl border p-2" style="border-color: var(--border); background-color: var(--bg-secondary);">
<template x-for="ch in chapters" :key="ch.index">
<!-- 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-6">
<button
type="button"
@click="skip(-1)"
class="rounded-full px-4 py-2.5 hover:bg-surface-hover transition-colors"
:aria-label="'Skip back ' + skipSeconds + 's'"
style="color: var(--text-primary);"
>
<span class="text-base font-semibold">–<span x-text="skipSeconds">10</span>s</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="text-3xl leading-none">▶</span>
<span x-show="playing" class="text-3xl leading-none" x-cloak>❚❚</span>
</button>
<button
type="button"
@click="skip(1)"
class="rounded-full px-4 py-2.5 hover:bg-surface-hover transition-colors"
:aria-label="'Skip forward ' + skipSeconds + 's'"
style="color: var(--text-primary);"
>
<span class="text-base font-semibold">+<span x-text="skipSeconds">10</span>s</span>
</button>
<button
type="button"
@click="toggleBookmarkCapture()"
class="rounded-full p-2.5 ml-2 hover:bg-surface-hover transition-colors"
:class="bookmarkCaptureOpen ? 'opacity-100' : 'opacity-60'"
aria-label="Add bookmark at current position"
title="Add bookmark"
style="color: var(--text-primary);"
>
<svg width="20" height="20" 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>
</button>
</div>
</div>
<!-- Skip / Speed rows: one layout rule, labels left, chips right -->
<div class="w-full max-w-xl flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Skip</span>
<div class="flex items-center gap-1.5">
<template x-for="secs in [1, 5, 10, 15, 30]" :key="'skip' + secs">
<button
type="button"
@click="jumpToChapter(ch.index)"
class="w-full text-left px-3 py-2 rounded-lg text-sm flex justify-between gap-3 hover:bg-surface-hover transition-colors"
:class="chapters.length && currentChapter() === ch.index ? 'font-semibold' : ''"
@click="setSkip(secs)"
class="text-xs px-2.5 py-1.5 rounded-md transition-colors"
:class="Number(skipSeconds) === secs ? 'opacity-100 underline' : 'opacity-50 hover:opacity-80'"
style="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>
x-text="secs + 's'"
></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>
</div>
<div class="flex items-center justify-between">
<span class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Speed</span>
<div class="flex items-center gap-1.5">
<template x-for="rate in [0.75, 1, 1.25, 1.5, 2]" :key="rate">
<button
type="button"
@click="setSpeed(rate)"
class="text-xs px-2.5 py-1.5 rounded-md transition-colors"
:class="Number(speed) === rate ? 'opacity-100 underline' : 'opacity-50 hover:opacity-80'"
style="color: var(--text-secondary);"
x-text="rate + '×'"
></button>
</template>
</div>
</div>
</div>
<audio id="audio-element" preload="auto" class="hidden"></audio>
<!-- 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>
<p class="text-xs mt-1 line-clamp-2" x-show="b.notes && editingBookmarkId !== String(b.id)" style="color: var(--text-secondary);" x-text="b.notes"></p>
<div x-show="editingBookmarkId === String(b.id)" x-cloak class="mt-1">
<input type="text" x-model="editingNote" class="input text-sm" placeholder="Note" @keydown.enter.prevent="saveNote()"/>
<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>
<input
type="text"
x-model="bookmarkNote"
placeholder="Optional note (e.g. 'great quote')"
class="input mb-2"
/>
<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>
+7 -7
View File
@@ -118,7 +118,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, 7, "\"><div class=\"h-full flex flex-col items-center justify-center px-4 gap-6 select-none\"><!-- 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);\">")
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
}
@@ -130,7 +130,7 @@ func AudioReader(user User, metadata ReaderMetadata) templ.Component {
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: `audio_reader.templ`, Line: 41, Col: 37}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `audio_reader.templ`, Line: 86, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
@@ -143,7 +143,7 @@ func AudioReader(user User, metadata ReaderMetadata) templ.Component {
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: `audio_reader.templ`, Line: 42, Col: 28}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `audio_reader.templ`, Line: 87, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
@@ -154,25 +154,25 @@ func AudioReader(user User, metadata ReaderMetadata) templ.Component {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span class=\"material-symbols-rounded text-5xl flex items-center justify-center h-full\" style=\"color: var(--text-secondary);\">headphones</span>")
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><h1 class=\"font-semibold text-lg leading-snug\" style=\"color: var(--text-primary);\">")
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: `audio_reader.templ`, Line: 51, Col: 105}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `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\" 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\" :value=\"chaptersDuration() || 1\" class=\"flex-1 accent-current\" @input.debounce.300ms=\"seekTo($event.target.value)\"> <span x-text=\"fmtTime(chaptersDuration() || 1)\">0:00</span></div><div class=\"flex items-center justify-center gap-6\"><button type=\"button\" @click=\"skip(-1)\" class=\"rounded-full p-3 hover:bg-surface-hover transition-colors\" :aria-label=\"'Skip back ' + skipSeconds + 's'\" style=\"color: var(--text-primary);\"><span class=\"text-lg font-semibold\">-<span x-text=\"skipSeconds\">10</span>s</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=\"text-3xl leading-none\">▶</span> <span x-show=\"playing\" class=\"text-3xl leading-none\" x-cloak>❚❚</span></button> <button type=\"button\" @click=\"skip(1)\" class=\"rounded-full p-3 hover:bg-surface-hover transition-colors\" :aria-label=\"'Skip forward ' + skipSeconds + 's'\" style=\"color: var(--text-primary);\"><span class=\"text-lg font-semibold\">+<span x-text=\"skipSeconds\">10</span>s</span></button></div></div><!-- Chapter list + speed --><div class=\"w-full max-w-xl\"><div class=\"flex items-center justify-between\"><button type=\"button\" @click=\"chaptersOpen = !chaptersOpen\" class=\"text-sm px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors\" style=\"color: var(--text-secondary);\">Chapters <span x-text=\"'(' + chapters.length + ')'\"></span></button><div class=\"flex items-center gap-1.5\"><span class=\"text-xs\" style=\"color: var(--text-secondary);\">Skip</span><template x-for=\"secs in [1, 5, 10, 15, 30]\" :key=\"'skip' + secs\"><button type=\"button\" @click=\"setSkip(secs)\" class=\"text-xs px-2 py-1 rounded-md transition-colors\" :class=\"Number(skipSeconds) === secs ? 'opacity-100 underline' : 'opacity-50 hover:opacity-80'\" style=\"color: var(--text-secondary);\" x-text=\"secs + 's'\"></button></template></div></div><div class=\"flex items-center justify-between mt-1\"><span class=\"text-xs\" style=\"color: var(--text-secondary);\">Speed</span><div class=\"flex items-center gap-1.5\"><template x-for=\"rate in [0.75, 1, 1.25, 1.5, 2]\" :key=\"rate\"><button type=\"button\" @click=\"setSpeed(rate)\" class=\"text-xs px-2 py-1 rounded-md transition-colors\" :class=\"Number(speed) === rate ? 'opacity-100 underline' : 'opacity-50 hover:opacity-80'\" style=\"color: var(--text-secondary);\" x-text=\"rate + '×'\"></button></template></div></div><div x-show=\"chaptersOpen\" x-cloak class=\"mt-2 max-h-64 overflow-y-auto rounded-xl border p-2\" style=\"border-color: var(--border); background-color: var(--bg-secondary);\"><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 rounded-lg text-sm flex justify-between gap-3 hover:bg-surface-hover transition-colors\" :class=\"chapters.length && currentChapter() === ch.index ? 'font-semibold' : ''\" style=\"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></div><audio id=\"audio-element\" preload=\"auto\" class=\"hidden\"></audio></div></body></html>")
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-6\"><button type=\"button\" @click=\"skip(-1)\" class=\"rounded-full px-4 py-2.5 hover:bg-surface-hover transition-colors\" :aria-label=\"'Skip back ' + skipSeconds + 's'\" style=\"color: var(--text-primary);\"><span class=\"text-base font-semibold\">–<span x-text=\"skipSeconds\">10</span>s</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=\"text-3xl leading-none\">▶</span> <span x-show=\"playing\" class=\"text-3xl leading-none\" x-cloak>❚❚</span></button> <button type=\"button\" @click=\"skip(1)\" class=\"rounded-full px-4 py-2.5 hover:bg-surface-hover transition-colors\" :aria-label=\"'Skip forward ' + skipSeconds + 's'\" style=\"color: var(--text-primary);\"><span class=\"text-base font-semibold\">+<span x-text=\"skipSeconds\">10</span>s</span></button> <button type=\"button\" @click=\"toggleBookmarkCapture()\" class=\"rounded-full p-2.5 ml-2 hover:bg-surface-hover transition-colors\" :class=\"bookmarkCaptureOpen ? 'opacity-100' : 'opacity-60'\" aria-label=\"Add bookmark at current position\" title=\"Add bookmark\" style=\"color: var(--text-primary);\"><svg width=\"20\" height=\"20\" 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></button></div></div><!-- Skip / Speed rows: one layout rule, labels left, chips right --><div class=\"w-full max-w-xl flex flex-col gap-2\"><div class=\"flex items-center justify-between\"><span class=\"text-xs uppercase tracking-wide\" style=\"color: var(--text-secondary);\">Skip</span><div class=\"flex items-center gap-1.5\"><template x-for=\"secs in [1, 5, 10, 15, 30]\" :key=\"'skip' + secs\"><button type=\"button\" @click=\"setSkip(secs)\" class=\"text-xs px-2.5 py-1.5 rounded-md transition-colors\" :class=\"Number(skipSeconds) === secs ? 'opacity-100 underline' : 'opacity-50 hover:opacity-80'\" style=\"color: var(--text-secondary);\" x-text=\"secs + 's'\"></button></template></div></div><div class=\"flex items-center justify-between\"><span class=\"text-xs uppercase tracking-wide\" style=\"color: var(--text-secondary);\">Speed</span><div class=\"flex items-center gap-1.5\"><template x-for=\"rate in [0.75, 1, 1.25, 1.5, 2]\" :key=\"rate\"><button type=\"button\" @click=\"setSpeed(rate)\" class=\"text-xs px-2.5 py-1.5 rounded-md transition-colors\" :class=\"Number(speed) === rate ? 'opacity-100 underline' : 'opacity-50 hover:opacity-80'\" style=\"color: var(--text-secondary);\" x-text=\"rate + '×'\"></button></template></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-labLine truncated
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+53 -16
View File
@@ -68,18 +68,23 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string, tomb
by { book.Author.String }
</h2>
}
if book.IsAudio() && len(book.Narrators) > 0 {
<h3 class="text-sm md:text-base mb-5" style="color: var(--text-secondary)">
Narrated by { strings.Join(book.Narrators, ", ") }
</h3>
}
<!-- Action buttons -->
<div class="flex flex-wrap gap-2.5 mb-6">
<button
@click={ "window.location.href = '/readers/" + uuidToString(book.ID) + "'" }
class="btn btn-primary px-5 py-2.5"
>
@Icon("book-open", "h-4 w-4")
<span>Read Now</span>
@Icon(book.ActionIcon(), "h-4 w-4")
<span>{ book.ActionLabel() }</span>
</button>
if book.ReadingProgress != nil && book.ReadingProgress.Percentage.Valid && book.ReadingProgress.Percentage.Float64 >= 1.0 {
<button @click="toggleRead(false)" :disabled="readSaving" class="btn btn-secondary px-5 py-2.5">
<span x-show="!readSaving" class="inline-flex items-center gap-2">@Icon("check-circle", "h-4 w-4")<span>Mark Unread</span></span>
<span x-show="!readSaving" class="inline-flex items-center gap-2">@Icon("check-circle", "h-4 w-4")<span>{ book.MarkResetLabel() }</span></span>
<svg x-show="readSaving" class="animate-spin inline-block h-5 w-5" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
@@ -87,7 +92,7 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string, tomb
</button>
} else {
<button @click="toggleRead(true)" :disabled="readSaving" class="btn btn-secondary px-5 py-2.5">
<span x-show="!readSaving" class="inline-flex items-center gap-2">@Icon("check", "h-4 w-4")<span>Mark Read</span></span>
<span x-show="!readSaving" class="inline-flex items-center gap-2">@Icon("check", "h-4 w-4")<span>{ book.MarkCompleteLabel() }</span></span>
<svg x-show="readSaving" class="animate-spin inline-block h-5 w-5" viewBox="0 0 24 24" fill="none">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
@@ -262,7 +267,7 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string, tomb
<!-- Progress -->
if book.ReadingProgress != nil {
<div class="card p-6 mb-6">
<h3 class="font-semibold mb-4" style="color: var(--text-primary)">Reading Progress</h3>
<h3 class="font-semibold mb-4" style="color: var(--text-primary)">{ book.ProgressHeader() }</h3>
if book.ActiveConflict != nil {
<div class="mb-4 p-3 rounded-xl flex items-start gap-2" style="background-color: color-mix(in srgb, var(--status-warning) 14%, transparent); border: 1px solid var(--status-warning);">
@Icon("alert", "h-5 w-5 shrink-0")
@@ -278,10 +283,17 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string, tomb
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Progress</p>
<p class="text-2xl font-bold" style="color: var(--accent);">{ fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64*100) }%</p>
</div>
if book.IsAudio() {
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Position</p>
<p class="text-2xl font-bold tabular-nums" style="color: var(--accent);">{ book.AudioCurrentLabel() + " / " + book.AudioDurationLabel() }</p>
</div>
} else {
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Progress</p>
<p class="text-2xl font-bold" style="color: var(--accent);">{ fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64*100) }%</p>
</div>
}
if book.ReadingProgress.CurrentPage.Valid && book.ReadingProgress.TotalPages.Valid {
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Page</p>
@@ -290,7 +302,7 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string, tomb
}
if book.ReadingProgress.LastReadAt.Valid {
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Last Read</p>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">{ book.LastReadLabel() }</p>
<p class="text-sm" style="color: var(--text-primary)">{ FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone) }</p>
</div>
}
@@ -306,10 +318,10 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string, tomb
<!-- Reading stats (the viewer's own sessions on this book) -->
if book.BookStats != nil {
<div class="card p-6 mb-6">
<h3 class="font-semibold mb-4" style="color: var(--text-primary)">Your Reading Stats</h3>
<h3 class="font-semibold mb-4" style="color: var(--text-primary)">{ book.StatsHeader() }</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Time Reading</p>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">{ book.TimeStatLabel() }</p>
if book.BookStats.TotalMinutes >= 60 {
<p class="text-xl font-bold" style="color: var(--accent)">{ fmt.Sprintf("%dh %dmin", book.BookStats.TotalMinutes/60, book.BookStats.TotalMinutes%60) }</p>
} else {
@@ -320,10 +332,17 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string, tomb
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Sessions</p>
<p class="text-xl font-bold" style="color: var(--accent)">{ fmt.Sprintf("%d", book.BookStats.SessionCount) }</p>
</div>
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Pages Synced</p>
<p class="text-xl font-bold" style="color: var(--accent)">{ fmt.Sprintf("%d", book.BookStats.TotalPages) }</p>
</div>
if book.IsAudio() {
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Time listened</p>
<p class="text-xl font-bold tabular-nums" style="color: var(--accent)">{ book.TimeListenedLabel() }</p>
</div>
} else {
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Pages Synced</p>
<p class="text-xl font-bold" style="color: var(--accent)">{ fmt.Sprintf("%d", book.BookStats.TotalPages) }</p>
</div>
}
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Finished</p>
if book.BookStats.Finished {
@@ -363,6 +382,24 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string, tomb
<p class="capitalize" style="color: var(--text-primary)">{ book.Language.String }</p>
</div>
}
if book.IsAudio() && len(book.Narrators) > 0 {
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Narrator</p>
<p style="color: var(--text-primary)">{ strings.Join(book.Narrators, ", ") }</p>
</div>
}
if book.IsAudio() && book.AudioDuration() > 0 {
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Duration</p>
<p class="tabular-nums" style="color: var(--text-primary)">{ book.AudioDurationLabel() }</p>
</div>
}
if book.IsAudio() && book.Abridged {
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Abridged</p>
<p style="color: var(--text-primary)">Yes</p>
</div>
}
if book.Edition.Valid && book.Edition.String != "" {
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Edition</p>
+32
View File
@@ -3,6 +3,7 @@ package templates
import (
"bookhoard/internal/handlers"
"fmt"
"strings"
)
templ ProgressSyncModal(user User, book handlers.MediaDetail) {
@@ -501,6 +502,37 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
</div>
</div>
</div>
if book.IsAudio() {
<div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
<button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
style="color: var(--text-primary);"
@click="toggleSection('audio')">
<span class="font-semibold">Audio</span>
<span class="inline-flex">
<span x-show="openSections.audio">@Icon("chevron-down", "h-4 w-4")</span>
<span x-show="!openSections.audio" x-cloak>@Icon("chevron-right", "h-4 w-4")</span>
</span>
</button>
<div x-show="openSections.audio" x-transition class="p-4 space-y-3 border-t" style="border-color: var(--border);">
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Narrator(s)</label>
<input type="text" name="narrators" value={ strings.Join(book.Narrators, ", ") } class="input" placeholder="Comma-separated names"/>
</div>
<div>
<label class="flex items-center gap-2 text-sm" style="color: var(--text-primary);">
<input type="checkbox" name="abridged" checked={ book.Abridged } class="h-4 w-4 accent-current"/>
<span>Abridged edition</span>
</label>
</div>
if book.AudioDuration() > 0 {
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary);">Duration (scanned)</label>
<p class="input opacity-60 cursor-default tabular-nums" style="color: var(--text-primary);">{ book.AudioDurationLabel() }</p>
</div>
}
</div>
</div>
}
<div class="rounded-xl border overflow-hidden" style="border-color: var(--border);">
<button type="button" class="w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover"
style="color: var(--text-primary);"
+254 -178
View File
@@ -11,6 +11,7 @@ import templruntime "github.com/a-h/templ/runtime"
import (
"bookhoard/internal/handlers"
"fmt"
"strings"
)
func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
@@ -49,7 +50,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 25, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 26, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@@ -88,7 +89,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Source)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 57, Col: 23}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 58, Col: 23}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -101,7 +102,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(data.Timestamp, user.Timezone))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 60, Col: 59}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 61, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -119,7 +120,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", data.Data["percentage"].(float64)*100))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 66, Col: 73}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 67, Col: 73}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -147,7 +148,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f", data.Data["page"].(float64)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 74, Col: 66}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 75, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -160,7 +161,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f", data.Data["total_pages"].(float64)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 74, Col: 128}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 75, Col: 128}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -174,7 +175,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f", data.Data["page"].(float64)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 76, Col: 66}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 77, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -194,7 +195,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(data.Data["epubcfi"].(string))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 82, Col: 45}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 83, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -212,7 +213,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("resolveConflict('" + book.ActiveConflict.ID + "', '" + source + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 87, Col: 86}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 88, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil {
@@ -235,7 +236,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.LastSyncSource.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 98, Col: 105}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 99, Col: 105}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -248,7 +249,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64*100))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 102, Col: 74}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 103, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@@ -261,7 +262,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.LastSyncSource.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 105, Col: 50}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 106, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@@ -279,7 +280,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.CurrentPage.Int32)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 109, Col: 52}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 110, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -292,7 +293,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.TotalPages.Int32)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 109, Col: 97}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 110, Col: 97}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@@ -311,7 +312,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 114, Col: 95}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 115, Col: 95}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
@@ -371,7 +372,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail, tombstoneDays in
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 148, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 149, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
@@ -392,7 +393,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail, tombstoneDays in
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(book.HighlightsCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 163, Col: 90}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 164, Col: 90}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
@@ -405,7 +406,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail, tombstoneDays in
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(book.NotesCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 167, Col: 85}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 168, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
@@ -418,7 +419,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail, tombstoneDays in
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(len(book.DeletedAnnotations))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 171, Col: 106}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 172, Col: 106}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
@@ -441,7 +442,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail, tombstoneDays in
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(len(book.DeletedAnnotations))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 183, Col: 125}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 184, Col: 125}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
@@ -464,7 +465,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail, tombstoneDays in
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(tombstoneDays)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 189, Col: 51}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 190, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
@@ -492,7 +493,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail, tombstoneDays in
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(ann.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 196, Col: 61}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 197, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
if templ_7745c5c3_Err != nil {
@@ -505,7 +506,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail, tombstoneDays in
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(ann.AnnotationType)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 199, Col: 119}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 200, Col: 119}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
@@ -518,7 +519,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail, tombstoneDays in
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(ann.DeletedAt, user.Timezone))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 201, Col: 59}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 202, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
@@ -531,7 +532,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail, tombstoneDays in
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue("restoreDeletedAnnotation('" + ann.AnnotationType + "', '" + ann.ID + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 206, Col: 93}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 207, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil {
@@ -552,7 +553,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail, tombstoneDays in
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue("purgeDeletedAnnotation('" + ann.AnnotationType + "', '" + ann.ID + "')")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 213, Col: 91}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 214, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
if templ_7745c5c3_Err != nil {
@@ -578,7 +579,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail, tombstoneDays in
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(ann.DisplayText)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 223, Col: 94}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 224, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
@@ -602,7 +603,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail, tombstoneDays in
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(ann.SecondaryText)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 228, Col: 103}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 229, Col: 103}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
@@ -680,7 +681,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.CoverImagePath.String)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 281, Col: 40}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 282, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
if templ_7745c5c3_Err != nil {
@@ -693,7 +694,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 282, Col: 24}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 283, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
if templ_7745c5c3_Err != nil {
@@ -711,7 +712,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var34 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 288, Col: 24}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 289, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
if templ_7745c5c3_Err != nil {
@@ -745,7 +746,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var35 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 323, Col: 58}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 324, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
if templ_7745c5c3_Err != nil {
@@ -758,7 +759,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Author))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 327, Col: 74}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 328, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
if templ_7745c5c3_Err != nil {
@@ -771,7 +772,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var37 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(textToString(book.Description))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 332, Col: 41}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 333, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
if templ_7745c5c3_Err != nil {
@@ -784,7 +785,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var38 string
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(textToString(book.Summary))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 337, Col: 37}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 338, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
if templ_7745c5c3_Err != nil {
@@ -810,7 +811,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var39 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(tag)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 350, Col: 36}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 351, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
if templ_7745c5c3_Err != nil {
@@ -828,7 +829,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var40 string
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%.1f", book.CommunityRating.Float64))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 384, Col: 66}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 385, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40)
if templ_7745c5c3_Err != nil {
@@ -857,7 +858,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var41 string
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Publisher))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 402, Col: 80}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 403, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var41)
if templ_7745c5c3_Err != nil {
@@ -870,7 +871,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var42 string
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.ResolveAttributeValue(formatDateForInput(book.DatePublished))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 407, Col: 55}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 408, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var42)
if templ_7745c5c3_Err != nil {
@@ -883,7 +884,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var43 string
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Edition))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 412, Col: 76}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 413, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var43)
if templ_7745c5c3_Err != nil {
@@ -896,7 +897,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var44 string
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Language))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 416, Col: 78}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 417, Col: 78}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var44)
if templ_7745c5c3_Err != nil {
@@ -909,7 +910,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var45 string
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Genre))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 420, Col: 72}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 421, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var45)
if templ_7745c5c3_Err != nil {
@@ -922,7 +923,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var46 string
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.CopyrightYear.Int32))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 425, Col: 60}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 426, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var46)
if templ_7745c5c3_Err != nil {
@@ -951,7 +952,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var47 string
templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Series))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 443, Col: 74}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 444, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var47)
if templ_7745c5c3_Err != nil {
@@ -964,7 +965,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var48 string
templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.SeriesNumber.Int32))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 449, Col: 60}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 450, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var48)
if templ_7745c5c3_Err != nil {
@@ -977,7 +978,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var49 string
templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.SeriesCount.Int32))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 455, Col: 59}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 456, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var49)
if templ_7745c5c3_Err != nil {
@@ -990,7 +991,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var50 string
templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.Volume.Int32))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 461, Col: 54}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 462, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50)
if templ_7745c5c3_Err != nil {
@@ -1019,7 +1020,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var51 string
templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Isbn))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 480, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 481, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var51)
if templ_7745c5c3_Err != nil {
@@ -1032,7 +1033,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var52 string
templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Asin))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 484, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 485, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var52)
if templ_7745c5c3_Err != nil {
@@ -1045,7 +1046,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var53 string
templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.GoodreadsID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 488, Col: 85}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 489, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var53)
if templ_7745c5c3_Err != nil {
@@ -1058,7 +1059,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var54 string
templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.OpenlibraryID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 492, Col: 89}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 493, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var54)
if templ_7745c5c3_Err != nil {
@@ -1071,7 +1072,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var55 string
templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.GoogleBooksID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 496, Col: 90}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 497, Col: 90}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var55)
if templ_7745c5c3_Err != nil {
@@ -1084,13 +1085,88 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
var templ_7745c5c3_Var56 string
templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.WebUrl))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 500, Col: 74}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 501, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var56)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "\" class=\"input\"></div></div></div><div class=\"rounded-xl border overflow-hidden\" style=\"border-color: var(--border);\"><button type=\"button\" class=\"w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover\" style=\"color: var(--text-primary);\" @click=\"toggleSection('comic')\"><span class=\"font-semibold\">Comic/Manga</span> <span class=\"inline-flex\"><span x-show=\"openSections.comic\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "\" class=\"input\"></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if book.IsAudio() {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "<div class=\"rounded-xl border overflow-hidden\" style=\"border-color: var(--border);\"><button type=\"button\" class=\"w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover\" style=\"color: var(--text-primary);\" @click=\"toggleSection('audio')\"><span class=\"font-semibold\">Audio</span> <span class=\"inline-flex\"><span x-show=\"openSections.audio\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("chevron-down", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</span> <span x-show=\"!openSections.audio\" x-cloak>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Icon("chevron-right", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</span></span></button><div x-show=\"openSections.audio\" x-transition class=\"p-4 space-y-3 border-t\" style=\"border-color: var(--border);\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Narrator(s)</label> <input type=\"text\" name=\"narrators\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var57 string
templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.ResolveAttributeValue(strings.Join(book.Narrators, ", "))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 519, Col: 87}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var57)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "\" class=\"input\" placeholder=\"Comma-separated names\"></div><div><label class=\"flex items-center gap-2 text-sm\" style=\"color: var(--text-primary);\"><input type=\"checkbox\" name=\"abridged\" checked=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var58 string
templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Abridged)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 523, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var58)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "\" class=\"h-4 w-4 accent-current\"> <span>Abridged edition</span></label></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if book.AudioDuration() > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "<div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Duration (scanned)</label><p class=\"input opacity-60 cursor-default tabular-nums\" style=\"color: var(--text-primary);\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var59 string
templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(book.AudioDurationLabel())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 530, Col: 129}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "<div class=\"rounded-xl border overflow-hidden\" style=\"border-color: var(--border);\"><button type=\"button\" class=\"w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover\" style=\"color: var(--text-primary);\" @click=\"toggleSection('comic')\"><span class=\"font-semibold\">Comic/Manga</span> <span class=\"inline-flex\"><span x-show=\"openSections.comic\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -1098,7 +1174,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "</span> <span x-show=\"!openSections.comic\" x-cloak>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "</span> <span x-show=\"!openSections.comic\" x-cloak>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -1106,186 +1182,186 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "</span></span></button><div x-show=\"openSections.comic\" x-transition class=\"p-4 space-y-3 border-t\" style=\"border-color: var(--border);\"><div><label for=\"manga_type\" class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Manga Type</label> <select id=\"manga_type\" name=\"manga_type\" class=\"input\"><option value=\"unknown\" selected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var57 string
templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MangaType) == "unknown")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 518, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var57)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "\">Unknown</option> <option value=\"no\" selected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var58 string
templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MangaType) == "no")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 519, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var58)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 110, "\">No</option> <option value=\"yes\" selected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var59 string
templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MangaType) == "yes")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 520, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var59)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 111, "\">Yes</option> <option value=\"yes_and_right_to_left\" selected=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "</span></span></button><div x-show=\"openSections.comic\" x-transition class=\"p-4 space-y-3 border-t\" style=\"border-color: var(--border);\"><div><label for=\"manga_type\" class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Manga Type</label> <select id=\"manga_type\" name=\"manga_type\" class=\"input\"><option value=\"unknown\" selected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var60 string
templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MangaType) == "yes_and_right_to_left")
templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MangaType) == "unknown")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 521, Col: 113}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 550, Col: 85}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 112, "\">Yes (Right to Left)</option></select></div><div><label for=\"reading_direction\" class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Reading Direction</label> <select id=\"reading_direction\" name=\"reading_direction\" class=\"input\"><option value=\"auto\" selected=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\">Unknown</option> <option value=\"no\" selected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var61 string
templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ReadingDirection) == "auto")
templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MangaType) == "no")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 527, Col: 86}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 551, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var61)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 113, "\">Auto</option> <option value=\"ltr\" selected=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "\">No</option> <option value=\"yes\" selected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var62 string
templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ReadingDirection) == "ltr")
templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MangaType) == "yes")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 528, Col: 84}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 552, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var62)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 114, "\">Left to Right</option> <option value=\"rtl\" selected=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "\">Yes</option> <option value=\"yes_and_right_to_left\" selected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var63 string
templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ReadingDirection) == "rtl")
templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MangaType) == "yes_and_right_to_left")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 529, Col: 84}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 553, Col: 113}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 115, "\">Right to Left</option> <option value=\"vertical\" selected=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "\">Yes (Right to Left)</option></select></div><div><label for=\"reading_direction\" class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Reading Direction</label> <select id=\"reading_direction\" name=\"reading_direction\" class=\"input\"><option value=\"auto\" selected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var64 string
templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ReadingDirection) == "vertical")
templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ReadingDirection) == "auto")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 530, Col: 94}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 559, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 116, "\">Vertical</option></select></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Age Rating</label> <input type=\"text\" name=\"age_rating\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, "\">Auto</option> <option value=\"ltr\" selected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var65 string
templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.AgeRating))
templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ReadingDirection) == "ltr")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 535, Col: 81}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 560, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var65)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 117, "\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Story Arc</label> <input type=\"text\" name=\"story_arc\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, "\">Left to Right</option> <option value=\"rtl\" selected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var66 string
templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.StoryArc))
templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ReadingDirection) == "rtl")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 539, Col: 79}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 561, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var66)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 118, "\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Imprint</label> <input type=\"text\" name=\"imprint\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "\">Right to Left</option> <option value=\"vertical\" selected=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var67 string
templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Imprint))
templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ReadingDirection) == "vertical")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 543, Col: 76}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 562, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 119, "\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Scan Information</label> <input type=\"text\" name=\"scan_information\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "\">Vertical</option></select></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Age Rating</label> <input type=\"text\" name=\"age_rating\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var68 string
templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ScanInformation))
templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.AgeRating))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 547, Col: 93}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 567, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var68)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 120, "\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Metadata Notes</label> <textarea name=\"metadata_notes\" rows=\"2\" class=\"input\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Story Arc</label> <input type=\"text\" name=\"story_arc\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var69 string
templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.JoinStringErrs(textToString(book.MetadataNotes))
templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.StoryArc))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 552, Col: 43}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 571, Col: 79}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var69))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var69)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 121, "</textarea></div><div class=\"flex items-center gap-2\"><input type=\"checkbox\" name=\"is_black_and_white\" id=\"is_black_and_white\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Imprint</label> <input type=\"text\" name=\"imprint\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var70 string
templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Imprint))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 575, Col: 76}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var70)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Scan Information</label> <input type=\"text\" name=\"scan_information\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var71 string
templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ScanInformation))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 579, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var71)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Metadata Notes</label> <textarea name=\"metadata_notes\" rows=\"2\" class=\"input\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var72 string
templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.JoinStringErrs(textToString(book.MetadataNotes))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 584, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var72))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "</textarea></div><div class=\"flex items-center gap-2\"><input type=\"checkbox\" name=\"is_black_and_white\" id=\"is_black_and_white\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if book.IsBlackAndWhite.Bool {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 122, " checked")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, " checked")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 123, " class=\"rounded\"> <label for=\"is_black_and_white\" class=\"text-sm\" style=\"color: var(--text-secondary);\">Black & White</label></div></div></div><div class=\"rounded-xl border overflow-hidden\" style=\"border-color: var(--border);\"><button type=\"button\" class=\"w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover\" style=\"color: var(--text-primary);\" @click=\"toggleSection('technical')\"><span class=\"font-semibold\">Technical</span> <span class=\"inline-flex\"><span x-show=\"openSections.technical\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, " class=\"rounded\"> <label for=\"is_black_and_white\" class=\"text-sm\" style=\"color: var(--text-secondary);\">Black & White</label></div></div></div><div class=\"rounded-xl border overflow-hidden\" style=\"border-color: var(--border);\"><button type=\"button\" class=\"w-full px-4 py-3 flex justify-between items-center hover:bg-surface-hover\" style=\"color: var(--text-primary);\" @click=\"toggleSection('technical')\"><span class=\"font-semibold\">Technical</span> <span class=\"inline-flex\"><span x-show=\"openSections.technical\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -1293,7 +1369,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 124, "</span> <span x-show=\"!openSections.technical\" x-cloak>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "</span> <span x-show=\"!openSections.technical\" x-cloak>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -1301,72 +1377,72 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 125, "</span></span></button><div x-show=\"openSections.technical\" x-transition class=\"p-4 space-y-3 border-t\" style=\"border-color: var(--border);\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Page Count</label> <input type=\"number\" name=\"page_count\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var70 string
templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.PageCount.Int32))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 576, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var70)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Contributors (comma-separated)</label> <input type=\"text\" name=\"contributors\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var71 string
templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.ResolveAttributeValue(stringSliceToString(book.Contributors))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 581, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var71)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 127, "\" class=\"input\"></div><div class=\"grid grid-cols-2 gap-3\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Format</label> <input type=\"text\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var72 string
templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MimeType))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 586, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "\" readonly class=\"input opacity-60\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">File Size</label> <input type=\"text\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 134, "</span></span></button><div x-show=\"openSections.technical\" x-transition class=\"p-4 space-y-3 border-t\" style=\"border-color: var(--border);\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Page Count</label> <input type=\"number\" name=\"page_count\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var73 string
templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%.1f MB", float64(book.FileSize.Int64)/1024/1024))
templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.PageCount.Int32))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 591, Col: 97}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 608, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var73)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 129, "\" readonly class=\"input opacity-60\"></div></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">File Path</label> <input type=\"text\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 135, "\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Contributors (comma-separated)</label> <input type=\"text\" name=\"contributors\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var74 string
templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.FileLocation)
templ_7745c5c3_Var74, templ_7745c5c3_Err = templ.ResolveAttributeValue(stringSliceToString(book.Contributors))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 597, Col: 51}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 613, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var74)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 130, "\" readonly class=\"input opacity-60 font-mono text-xs\"></div></div></div></div></div><div class=\"flex items-center justify-between p-6 border-t flex-shrink-0\" style=\"border-color: var(--border);\"><div class=\"flex items-center gap-2\"><button @click=\"rescanBook()\" :disabled=\"rescanning\" class=\"btn btn-secondary\"><span x-show=\"!rescanning\" class=\"inline-flex items-center gap-2\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 136, "\" class=\"input\"></div><div class=\"grid grid-cols-2 gap-3\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">Format</label> <input type=\"text\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var75 string
templ_7745c5c3_Var75, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MimeType))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 618, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var75)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 137, "\" readonly class=\"input opacity-60\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">File Size</label> <input type=\"text\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var76 string
templ_7745c5c3_Var76, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%.1f MB", float64(book.FileSize.Int64)/1024/1024))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 623, Col: 97}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var76)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 138, "\" readonly class=\"input opacity-60\"></div></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary);\">File Path</label> <input type=\"text\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var77 string
templ_7745c5c3_Var77, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.FileLocation)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 629, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var77)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 139, "\" readonly class=\"input opacity-60 font-mono text-xs\"></div></div></div></div></div><div class=\"flex items-center justify-between p-6 border-t flex-shrink-0\" style=\"border-color: var(--border);\"><div class=\"flex items-center gap-2\"><button @click=\"rescanBook()\" :disabled=\"rescanning\" class=\"btn btn-secondary\"><span x-show=\"!rescanning\" class=\"inline-flex items-center gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -1374,7 +1450,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 131, "<span>Rescan</span></span> <svg x-show=\"rescanning\" class=\"animate-spin inline-block h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle> <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path></svg></button> <button @click=\"resetMetadata()\" :disabled=\"resetting\" class=\"btn btn-secondary\" title=\"Discard all manual edits and restore scanned metadata\"><span x-show=\"!resetting\" class=\"inline-flex items-center gap-2\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 140, "<span>Rescan</span></span> <svg x-show=\"rescanning\" class=\"animate-spin inline-block h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle> <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path></svg></button> <button @click=\"resetMetadata()\" :disabled=\"resetting\" class=\"btn btn-secondary\" title=\"Discard all manual edits and restore scanned metadata\"><span x-show=\"!resetting\" class=\"inline-flex items-center gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -1382,7 +1458,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 132, "<span>Reset to Scanned</span></span> <svg x-show=\"resetting\" class=\"animate-spin inline-block h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle> <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path></svg></button></div><div class=\"flex space-x-3\"><button @click=\"hideMetadataEditor()\" class=\"btn btn-secondary\">Cancel</button> <button @click=\"saveMetadata()\" class=\"btn btn-primary\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 141, "<span>Reset to Scanned</span></span> <svg x-show=\"resetting\" class=\"animate-spin inline-block h-5 w-5\" viewBox=\"0 0 24 24\" fill=\"none\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle> <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"></path></svg></button></div><div class=\"flex space-x-3\"><button @click=\"hideMetadataEditor()\" class=\"btn btn-secondary\">Cancel</button> <button @click=\"saveMetadata()\" class=\"btn btn-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -1390,7 +1466,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 133, "Save</button></div></div></div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 142, "Save</button></div></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
File diff suppressed because it is too large. Load diff
+12
View File
@@ -220,6 +220,18 @@ templ BookShelf(
/>
<datalist id="series-datalist"></datalist>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Narrator</label>
<input
type="text"
name="narrator_filter"
placeholder="Filter by narrator"
class="input"
list="narrator-datalist"
@input.debounce.300ms="if($el.value.length >= 2) fetchNarratorValues($el)"
/>
<datalist id="narrator-datalist"></datalist>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Language</label>
<input
+5 -5
View File
@@ -384,7 +384,7 @@ func BookShelf(
}
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</select></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Author</label> <input type=\"text\" name=\"author_filter\" placeholder=\"Filter by author\" class=\"input\" list=\"author-datalist\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchAuthorValues($el)\"> <datalist id=\"author-datalist\"></datalist></div><div class=\"relative\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Tags</label> <input type=\"text\" name=\"tags_filter\" placeholder=\"Filter by tags\" class=\"input\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchTagValues($el)\" @keydown=\"onTagFilterKeydown($event)\" @blur=\"hideTagDropdown()\"><div x-show=\"showTagDropdown\" x-transition x-cloak class=\"absolute z-50 mt-1 w-full rounded-lg shadow-lg max-h-48 overflow-y-auto\" style=\"background-color: var(--bg-primary); border: 1px solid var(--border);\"><template x-for=\"sug in tagSuggestions\" :key=\"sug.value\"><button type=\"button\" class=\"w-full text-left px-3 py-2 text-sm flex justify-between items-center\" :class=\"tagHighlightIndex === tagSuggestions.indexOf(sug) ? 'opacity-80' : ''\" :style=\"tagHighlightIndex === tagSuggestions.indexOf(sug) ? 'background-color: var(--bg-secondary); color: var(--text-primary);' : 'color: var(--text-primary);'\" @click=\"selectTagSuggestion(sug.value)\"><span x-text=\"sug.value\"></span> <span class=\"text-xs\" style=\"color: var(--text-secondary);\" x-text=\"sug.count + ' books'\"></span></button></template></div></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Series</label> <input type=\"text\" name=\"series_filter\" placeholder=\"Filter by series\" class=\"input\" list=\"series-datalist\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchSeriesValues($el)\"> <datalist id=\"series-datalist\"></datalist></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Language</label> <input type=\"text\" name=\"language_filter\" placeholder=\"Filter by language\" class=\"input\" list=\"language-datalist\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchLanguageValues($el)\"> <datalist id=\"language-datalist\"></datalist></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Year Range</label><div class=\"flex gap-2\"><input type=\"number\" name=\"year_min\" placeholder=\"From\" class=\"input\"> <input type=\"number\" name=\"year_max\" placeholder=\"To\" class=\"input\"></div></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Cover</label> <input type=\"button\" id=\"has-cover-tristate\" class=\"tristate-btn w-full transition-all duration-300 cursor-pointer flex items-center justify-center gap-2 px-4 py-2 rounded-lg border text-sm\" :class=\"{\n\t\t\t\t\t\t\t\t\t\t\t'state-null': hasCoverState === null,\n\t\t\t\t\t\t\t\t\t\t\t'state-true': hasCoverState === true,\n\t\t\t\t\t\t\t\t\t\t\t'state-false': hasCoverState === false\n\t\t\t\t\t\t\t\t\t\t}\" :value=\"hasCoverState === null ? '○ Any cover' : hasCoverState === true ? '✓ Has cover' : '✗ No cover'\" @click=\"cycleHasCover()\"><template x-if=\"hasCoverState !== null\"><input type=\"hidden\" name=\"has_cover\" :value=\"hasCoverState ? 'true' : 'false'\"></template></div></div><div class=\"sticky bottom-0 px-6 py-4 border-t\" style=\"background-color: var(--bg-secondary); border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);\"><button type=\"submit\" @click=\"filtersOpen = false\" class=\"btn btn-primary w-full\">Apply Filters</button></div></div></div></form><div id=\"books-grid\" class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</select></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Author</label> <input type=\"text\" name=\"author_filter\" placeholder=\"Filter by author\" class=\"input\" list=\"author-datalist\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchAuthorValues($el)\"> <datalist id=\"author-datalist\"></datalist></div><div class=\"relative\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Tags</label> <input type=\"text\" name=\"tags_filter\" placeholder=\"Filter by tags\" class=\"input\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchTagValues($el)\" @keydown=\"onTagFilterKeydown($event)\" @blur=\"hideTagDropdown()\"><div x-show=\"showTagDropdown\" x-transition x-cloak class=\"absolute z-50 mt-1 w-full rounded-lg shadow-lg max-h-48 overflow-y-auto\" style=\"background-color: var(--bg-primary); border: 1px solid var(--border);\"><template x-for=\"sug in tagSuggestions\" :key=\"sug.value\"><button type=\"button\" class=\"w-full text-left px-3 py-2 text-sm flex justify-between items-center\" :class=\"tagHighlightIndex === tagSuggestions.indexOf(sug) ? 'opacity-80' : ''\" :style=\"tagHighlightIndex === tagSuggestions.indexOf(sug) ? 'background-color: var(--bg-secondary); color: var(--text-primary);' : 'color: var(--text-primary);'\" @click=\"selectTagSuggestion(sug.value)\"><span x-text=\"sug.value\"></span> <span class=\"text-xs\" style=\"color: var(--text-secondary);\" x-text=\"sug.count + ' books'\"></span></button></template></div></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Series</label> <input type=\"text\" name=\"series_filter\" placeholder=\"Filter by series\" class=\"input\" list=\"series-datalist\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchSeriesValues($el)\"> <datalist id=\"series-datalist\"></datalist></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Narrator</label> <input type=\"text\" name=\"narrator_filter\" placeholder=\"Filter by narrator\" class=\"input\" list=\"narrator-datalist\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchNarratorValues($el)\"> <datalist id=\"narrator-datalist\"></datalist></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Language</label> <input type=\"text\" name=\"language_filter\" placeholder=\"Filter by language\" class=\"input\" list=\"language-datalist\" @input.debounce.300ms=\"if($el.value.length >= 2) fetchLanguageValues($el)\"> <datalist id=\"language-datalist\"></datalist></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Year Range</label><div class=\"flex gap-2\"><input type=\"number\" name=\"year_min\" placeholder=\"From\" class=\"input\"> <input type=\"number\" name=\"year_max\" placeholder=\"To\" class=\"input\"></div></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Cover</label> <input type=\"button\" id=\"has-cover-tristate\" class=\"tristate-btn w-full transition-all duration-300 cursor-pointer flex items-center justify-center gap-2 px-4 py-2 rounded-lg border text-sm\" :class=\"{\n\t\t\t\t\t\t\t\t\t\t\t'state-null': hasCoverState === null,\n\t\t\t\t\t\t\t\t\t\t\t'state-true': hasCoverState === true,\n\t\t\t\t\t\t\t\t\t\t\t'state-false': hasCoverState === false\n\t\t\t\t\t\t\t\t\t\t}\" :value=\"hasCoverState === null ? '○ Any cover' : hasCoverState === true ? '✓ Has cover' : '✗ No cover'\" @click=\"cycleHasCover()\"><template x-if=\"hasCoverState !== null\"><input type=\"hidden\" name=\"has_cover\" :value=\"hasCoverState ? 'true' : 'false'\"></template></div></div><div class=\"sticky bottom-0 px-6 py-4 border-t\" style=\"background-color: var(--bg-secondary); border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);\"><button type=\"submit\" @click=\"filtersOpen = false\" class=\"btn btn-primary w-full\">Apply Filters</button></div></div></div></form><div id=\"books-grid\" class=\"grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -400,7 +400,7 @@ func BookShelf(
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 275, Col: 59}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 287, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
@@ -423,7 +423,7 @@ func BookShelf(
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/api/media-items/search?library_id=%s&limit=%d&offset=%d", currentLibraryID, limit, offset-limit))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 284, Col: 126}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 296, Col: 126}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
if templ_7745c5c3_Err != nil {
@@ -454,7 +454,7 @@ func BookShelf(
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(offset/limit + 1)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 293, Col: 32}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 305, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
@@ -467,7 +467,7 @@ func BookShelf(
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/api/media-items/search?library_id=%s&limit=%d&offset=%d", currentLibraryID, limit, offset+limit))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 298, Col: 126}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 310, Col: 126}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil {
+57 -20
View File
@@ -3,9 +3,10 @@ package templates
import (
"bookhoard/internal/handlers"
"fmt"
"strings"
)
templ Dashboard(user User, sections []handlers.SectionData, allSections []handlers.SectionData, libData []LibraryData, currentLibraryID string, hiddenCollections []string, itemsPerSection int, week *WeekStatsData, errorMessage string) {
templ Dashboard(user User, sections []handlers.SectionData, allSections []handlers.SectionData, libData []LibraryData, currentLibraryID string, hiddenCollections []string, itemsPerSection int, week *WeekStatsData, activeLibType string, errorMessage string) {
<!DOCTYPE html>
<html lang="en">
<head>
@@ -25,7 +26,7 @@ templ Dashboard(user User, sections []handlers.SectionData, allSections []handle
switch. The widget re-fetches itself via htmx instead. -->
if week != nil {
<div class="w-full px-4 sm:px-6 lg:px-8 pt-6">
@DashboardWeekWidget(*week)
@DashboardWeekWidget(*week, activeLibType)
</div>
}
<main id="collections-container" class="w-full px-4 sm:px-6 lg:px-8 pt-5 pb-8">
@@ -130,6 +131,17 @@ templ BookCard(item handlers.BookInfo) {
class="w-full h-full object-cover"
/>
}
<!-- Media-type identifier (A2c): books stay unbadged — the
shelf default already reads as a book -->
if MediaBadgeIcon(item.FormatGroup) != "" {
<span
class="absolute top-2 right-2 z-10 flex items-center justify-center h-7 w-7 rounded-full shadow-sm"
style="background-color: color-mix(in srgb, var(--bg-primary) 78%, transparent); color: var(--text-primary); backdrop-filter: blur(3px);"
title={ strings.Title(item.FormatGroup) + " read" }
>
@Icon(MediaBadgeIcon(item.FormatGroup), "h-4 w-4")
</span>
}
</div>
<div class="book-card-meta">
<h3 class="font-semibold text-sm leading-snug line-clamp-2" style="color: var(--text-primary)" title={ item.Title }>
@@ -152,6 +164,15 @@ templ BookCard(item handlers.BookInfo) {
>
@Icon("book-open", "h-5 w-5")
</a>
} else if item.FormatGroup == "audiobook" {
<a
href={ "/readers/" + item.MediaItemID }
class="book-card-action-btn"
aria-label={ "Listen to " + item.Title }
title="Listen"
>
@Icon("headphones", "h-5 w-5")
</a>
} else {
<a
href={ "/readers/" + item.MediaItemID }
@@ -285,7 +306,7 @@ type BookStatsData struct {
// itself through htmx when the body sees a `week-refresh` event
// (dispatched by the library switcher), carrying the CURRENT
// #library-select value, so the widget tracks the switched library.
templ DashboardWeekWidget(week WeekStatsData) {
templ DashboardWeekWidget(week WeekStatsData, activeLibType string) {
<details
id="week-widget"
class="card group"
@@ -315,23 +336,39 @@ templ DashboardWeekWidget(week WeekStatsData) {
</summary>
<div class="px-4 md:px-6 pb-4 pt-3 border-t" style="border-color: color-mix(in srgb, var(--text-primary) 6%, transparent);">
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Reading Time</p>
<p class="text-2xl font-bold" style="color: var(--text-primary)">{ fmtReadingMinutes(week.WeekMinutes) }</p>
<p class="text-xs mt-0.5" style="color: var(--text-secondary)">{ weekVsPrevLabel(week.WeekMinutes, week.PrevMinutes) }</p>
</div>
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Pages</p>
<p class="text-2xl font-bold" style="color: var(--text-primary)">{ fmt.Sprintf("%d", week.WeekPages) }</p>
</div>
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Sessions</p>
<p class="text-2xl font-bold" style="color: var(--text-primary)">{ fmt.Sprintf("%d", week.WeekSessions) }</p>
</div>
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Books Touched</p>
<p class="text-2xl font-bold" style="color: var(--text-primary)">{ fmt.Sprintf("%d", week.WeekBooks) }</p>
</div>
if activeLibType == "audiobooks" {
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Listening Time</p>
<p class="text-2xl font-bold" style="color: var(--text-primary)">{ fmtReadingMinutes(week.WeekMinutes) }</p>
<p class="text-xs mt-0.5" style="color: var(--text-secondary)">{ weekVsPrevLabel(week.WeekMinutes, week.PrevMinutes) }</p>
</div>
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Sessions</p>
<p class="text-2xl font-bold" style="color: var(--text-primary)">{ fmt.Sprintf("%d", week.WeekSessions) }</p>
</div>
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Books Touched</p>
<p class="text-2xl font-bold" style="color: var(--text-primary)">{ fmt.Sprintf("%d", week.WeekBooks) }</p>
</div>
} else {
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Reading Time</p>
<p class="text-2xl font-bold" style="color: var(--text-primary)">{ fmtReadingMinutes(week.WeekMinutes) }</p>
<p class="text-xs mt-0.5" style="color: var(--text-secondary)">{ weekVsPrevLabel(week.WeekMinutes, week.PrevMinutes) }</p>
</div>
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Pages</p>
<p class="text-2xl font-bold" style="color: var(--text-primary)">{ fmt.Sprintf("%d", week.WeekPages) }</p>
</div>
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Sessions</p>
<p class="text-2xl font-bold" style="color: var(--text-primary)">{ fmt.Sprintf("%d", week.WeekSessions) }</p>
</div>
<div>
<p class="text-xs uppercase tracking-wide" style="color: var(--text-secondary)">Books Touched</p>
<p class="text-2xl font-bold" style="color: var(--text-primary)">{ fmt.Sprintf("%d", week.WeekBooks) }</p>
</div>
}
</div>
<p class="text-xs mt-3" style="color: var(--text-secondary)">
Synced from your reading devices
File diff suppressed because it is too large. Load diff
+10
View File
@@ -168,6 +168,16 @@ templ Icon(name string, extra string) {
<ellipse cx="12" cy="5" rx="8" ry="3"></ellipse>
<path d="M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5"></path>
<path d="M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6"></path>
case "headphones":
<path d="M4 14v-2a8 8 0 0 1 16 0v2"></path>
<rect x="3" y="14" width="4" height="7" rx="1.5"></rect>
<rect x="17" y="14" width="4" height="7" rx="1.5"></rect>
case "comic":
<path d="M4 4h16v12l-3 4H7l-3-4z"></path>
<path d="M4 9h16M9 9v11"></path>
case "manga":
<path d="M4 4h16v13a3 3 0 0 1-3 3H4z"></path>
<path d="M4 4v16M10 4h10v8"></path>
default:
<circle cx="12" cy="12" r="9"></circle>
}
+17 -2
View File
@@ -331,13 +331,28 @@ func Icon(name string, extra string) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "headphones":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<path d=\"M4 14v-2a8 8 0 0 1 16 0v2\"></path> <rect x=\"3\" y=\"14\" width=\"4\" height=\"7\" rx=\"1.5\"></rect> <rect x=\"17\" y=\"14\" width=\"4\" height=\"7\" rx=\"1.5\"></rect>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "comic":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<path d=\"M4 4h16v12l-3 4H7l-3-4z\"></path> <path d=\"M4 9h16M9 9v11\"></path>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "manga":
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<path d=\"M4 4h16v13a3 3 0 0 1-3 3H4z\"></path> <path d=\"M4 4v16M10 4h10v8\"></path>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<circle cx=\"12\" cy=\"12\" r=\"9\"></circle>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<circle cx=\"12\" cy=\"12\" r=\"9\"></circle>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</svg>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+3 -1
View File
@@ -72,7 +72,9 @@ templ Progress(user User, progressData []handlers.ProgressWithMedia, errorMessag
<div class="stat-card">
<span class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Current Position</span>
<p class="text-sm font-medium" style="color: var(--text-primary)">
if item.FormatGroup == "reflowable" {
if item.FormatGroup == "audiobook" && item.DurationSeconds > 0 {
{ fmtSecondsHMS(item.ProgressPercentage/100*item.DurationSeconds) + " / " + fmtSecondsHMS(item.DurationSeconds) }
} else if item.FormatGroup == "reflowable" {
if item.EstimatedPages > 0 {
{ fmt.Sprintf("Page %d of %d (est.)", item.CurrentPage, item.EstimatedPages) }
} else {
+51 -41
View File
@@ -159,35 +159,45 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if item.FormatGroup == "reflowable" {
if item.FormatGroup == "audiobook" && item.DurationSeconds > 0 {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmtSecondsHMS(item.ProgressPercentage/100*item.DurationSeconds) + " / " + fmtSecondsHMS(item.DurationSeconds))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 76, Col: 124}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if item.FormatGroup == "reflowable" {
if item.EstimatedPages > 0 {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Page %d of %d (est.)", item.CurrentPage, item.EstimatedPages))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 77, Col: 90}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f%%", item.ProgressPercentage))
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Page %d of %d (est.)", item.CurrentPage, item.EstimatedPages))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 79, Col: 62}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 79, Col: 90}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f%%", item.ProgressPercentage))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 81, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
} else {
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d / %d", item.CurrentPage, item.TotalPages))
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d / %d", item.CurrentPage, item.TotalPages))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 82, Col: 72}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 84, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -196,12 +206,12 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.LastUpdated)
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(item.LastUpdated)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 88, Col: 95}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 90, Col: 95}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -209,12 +219,12 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceIcon)
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceIcon)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 93, Col: 51}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 95, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -222,12 +232,12 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 94, Col: 98}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 96, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -248,12 +258,12 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 101, Col: 92}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 103, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -261,12 +271,12 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceType)
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceType)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 102, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 104, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -288,12 +298,12 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(item.EpubCFI)
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(item.EpubCFI)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 108, Col: 36}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 110, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+32
View File
@@ -62,6 +62,22 @@ func isUserVisible(userID string, visibility []UserVisibilityData) bool {
// TotalMediaCount sums the MediaCount across the given libraries,
// used to display the total next to the "All Libraries" option.
// MediaBadgeIcon selects the cover-corner badge glyph per media group
// (A2c): the per-type identifier on mixed grids. Books stay unbadged —
// the shelf's default reads as a book without a stamp.
func MediaBadgeIcon(formatGroup string) string {
switch formatGroup {
case "audiobook":
return "headphones"
case "comic":
return "comic"
case "manga":
return "manga"
default:
return ""
}
}
func TotalMediaCount(libs []LibraryData) int64 {
var total int64
for _, l := range libs {
@@ -350,3 +366,19 @@ func hxValsFolderDelete(folderPath string) string {
}
return "js:" + string(b)
}
// fmtSecondsHMS renders seconds as H:MM:SS (or M:SS under an hour) —
// the audio progress display shape shared with the web player.
func fmtSecondsHMS(seconds float64) string {
s := int(seconds + 0.5)
if s <= 0 {
return "0:00"
}
h := s / 3600
m := (s % 3600) / 60
sec := s % 60
if h > 0 {
return fmt.Sprintf("%d:%02d:%02d", h, m, sec)
}
return fmt.Sprintf("%d:%02d", m, sec)
}