fix(ui): mobile layout overflows across admin, reader, and book pages

Every fix is a minimal class-level change; desktop layouts are untouched
because each fixed row already fit on one line there.

- progress: CFI string is one unbreakable token — it inflated the page
  layout width to 398-501px (whole-page zoom-out on phones) and rendered
  debug-looking text edge to edge. min-w-0 + break-all lets it wrap.
- audio reader: settings strip min-content (~470px) exceeded the 390px
  viewport, clipping Skip/Sleep and — because the page ICB inflated —
  pushing the chapters drawer (w-full of the viewport) past the screen
  edge so its timestamps clipped too. flex-wrap fixes both symptoms.
- book detail edit-metadata modal footer: Rescan/Reset/Write-to-file row
  overflowed the modal; clipped buttons were unreachable. flex-wrap.
- bookshelf save-filter modal footer: three buttons overflowed both
  screen edges. flex-wrap.
- admin libraries: rows overprinted (badge chips shrink-0 vs actions
  shrink-0, no wrap) — 'manga⟳Scan▸1 folders⌄Manage'. flex-wrap stacks
  actions under the name on phones.
- admin podcasts: maintenance table forced a 892px page (44% zoom).
  overflow-x-auto on the card scrolls the table inside it instead.
- stats: 4-pill range row forced 421px; page permanently ~7% zoomed out.
  flex-wrap.
- podcast feed detail: label+select pairs now travel in their own flex
  pairs so flex-wrap can never split 'Order' from its select; ids and
  htmx change-triggers preserved.
- cosmetics: conflicts stat labels tracking-wide→normal (UNRESOLVED
  clipped), archived file path line-clamp-3, rules-page Smart badge
  wraps cleanly, dashboard View All shrink-0 nowrap (was wrapping to
  two lines).
This commit is contained in:
John O'Keefe committed 2026-11-02 09:30:55 -05:00
1 parent e3ca3426ba
commit 88adf30aa9
24 files changed
+109 -103

No files matched your search

+1 -1
View File
@@ -66,7 +66,7 @@ templ AdminArchived(user User, items []ArchivedItem, retentionDays int) {
if item.Author != "" { if item.Author != "" {
<p style="color: var(--text-secondary)">{ item.Author }</p> <p style="color: var(--text-secondary)">{ item.Author }</p>
} }
<p class="font-mono text-xs break-all" style="color: var(--text-secondary)">{ item.FilePath }</p> <p class="font-mono text-xs break-all line-clamp-3" style="color: var(--text-secondary)">{ item.FilePath }</p>
</td> </td>
<td class="px-4 py-3" style="color: var(--text-primary)">{ item.LibraryName }</td> <td class="px-4 py-3" style="color: var(--text-primary)">{ item.LibraryName }</td>
<td class="px-4 py-3" style="color: var(--text-secondary)">{ item.Status }</td> <td class="px-4 py-3" style="color: var(--text-secondary)">{ item.Status }</td>
+2 -2
View File
@@ -164,14 +164,14 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<p class=\"font-mono text-xs break-all\" style=\"color: var(--text-secondary)\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<p class=\"font-mono text-xs break-all line-clamp-3\" style=\"color: var(--text-secondary)\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.FilePath) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.FilePath)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 69, Col: 103} return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 69, Col: 116}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
+1 -1
View File
@@ -200,7 +200,7 @@ templ LibraryList(user User, libraries []LibraryData, users []User) {
<div class="space-y-4"> <div class="space-y-4">
for _, library := range libraries { for _, library := range libraries {
<div class="card overflow-hidden"> <div class="card overflow-hidden">
<div class="flex items-center justify-between gap-4 p-5"> <div class="flex flex-wrap items-center justify-between gap-4 p-5">
<div class="flex items-center gap-3 min-w-0 flex-1"> <div class="flex items-center gap-3 min-w-0 flex-1">
<span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);"> <span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("library", "h-5 w-5") @Icon("library", "h-5 w-5")
+1 -1
View File
@@ -288,7 +288,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, library := range libraries { for _, library := range libraries {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<div class=\"card overflow-hidden\"><div class=\"flex items-center justify-between gap-4 p-5\"><div class=\"flex items-center gap-3 min-w-0 flex-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<div class=\"card overflow-hidden\"><div class=\"flex flex-wrap items-center justify-between gap-4 p-5\"><div class=\"flex items-center gap-3 min-w-0 flex-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+1 -1
View File
@@ -48,7 +48,7 @@ templ AdminPodcasts(user User, feeds []AdminPodcastFeedData, totalFeeds int, tot
if len(feeds) == 0 { if len(feeds) == 0 {
<div class="card p-6 text-center text-sm" style="color: var(--text-secondary)">No podcast feeds exist yet.</div> <div class="card p-6 text-center text-sm" style="color: var(--text-secondary)">No podcast feeds exist yet.</div>
} else { } else {
<div class="oc-table card"> <div class="oc-table card overflow-x-auto">
<table class="w-full text-sm"> <table class="w-full text-sm">
<thead> <thead>
<tr class="text-left text-xs uppercase tracking-wide" style="color: var(--text-secondary); border-bottom: 1px solid var(--border);"> <tr class="text-left text-xs uppercase tracking-wide" style="color: var(--text-secondary); border-bottom: 1px solid var(--border);">
+1 -1
View File
@@ -155,7 +155,7 @@ func AdminPodcasts(user User, feeds []AdminPodcastFeedData, totalFeeds int, tota
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"oc-table card\"><table class=\"w-full text-sm\"><thead><tr class=\"text-left text-xs uppercase tracking-wide\" style=\"color: var(--text-secondary); border-bottom: 1px solid var(--border);\"><th class=\"px-4 py-3\">Feed</th><th class=\"px-4 py-3\">Library</th><th class=\"px-4 py-3\">Subscribers</th><th class=\"px-4 py-3 text-right\">Downloaded</th><th class=\"px-4 py-3 text-right\">Disk</th><th class=\"px-4 py-3\">Last fetch</th><th class=\"px-4 py-3 text-right\">Actions</th></tr></thead> <tbody>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"oc-table card overflow-x-auto\"><table class=\"w-full text-sm\"><thead><tr class=\"text-left text-xs uppercase tracking-wide\" style=\"color: var(--text-secondary); border-bottom: 1px solid var(--border);\"><th class=\"px-4 py-3\">Feed</th><th class=\"px-4 py-3\">Library</th><th class=\"px-4 py-3\">Subscribers</th><th class=\"px-4 py-3 text-right\">Downloaded</th><th class=\"px-4 py-3 text-right\">Disk</th><th class=\"px-4 py-3\">Last fetch</th><th class=\"px-4 py-3 text-right\">Actions</th></tr></thead> <tbody>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+1 -1
View File
@@ -514,7 +514,7 @@ templ AudioReader(user User, metadata ReaderMetadata) {
</div> </div>
<!-- Settings strip: Skip ▾ · Speed ▾ · Bookmark · Volume ▾ --> <!-- Settings strip: Skip ▾ · Speed ▾ · Bookmark · Volume ▾ -->
<div class="w-full max-w-xl flex items-center justify-center gap-2"> <div class="w-full max-w-xl flex flex-wrap items-center justify-center gap-2">
<div class="relative" @click.outside="skipOpen = false"> <div class="relative" @click.outside="skipOpen = false">
<button <button
type="button" type="button"
+1 -1
View File
@@ -615,7 +615,7 @@ func AudioReader(user User, metadata ReaderMetadata) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</div></div><!-- Settings strip: Skip ▾ · Speed ▾ · Bookmark · Volume ▾ --><div class=\"w-full max-w-xl flex items-center justify-center gap-2\"><div class=\"relative\" @click.outside=\"skipOpen = false\"><button type=\"button\" @click=\"skipOpen = !skipOpen\" class=\"text-xs px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors inline-flex items-center gap-1.5\" :aria-label=\"'Skip duration, currently ' + skipSeconds + ' seconds'\" style=\"color: var(--text-secondary);\"><span>Skip</span> <span class=\"font-semibold\" style=\"color: var(--text-primary);\" x-text=\"skipSeconds + 's'\">10s</span> <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"></path></svg></button><div x-show=\"skipOpen\" x-cloak class=\"absolute bottom-full mb-2 left-1/2 -translate-x-1/2 rounded-xl shadow-lg p-1.5 flex flex-col min-w-[7.5rem]\" style=\"background-color: var(--bg-secondary); border: 1px solid var(--border); z-index: 30;\"><template x-for=\"secs in skipChoices\" :key=\"'skip' + secs\"><button type=\"button\" @click=\"pickSkip(secs)\" class=\"w-full text-left px-3 py-1.5 rounded-md text-sm\" :class=\"isSkipActive(secs) ? 'font-semibold' : ''\" :style=\"isSkipActive(secs) ? 'background-color: var(--accent-muted); color: var(--text-primary);' : 'color: var(--text-secondary);'\" x-text=\"secs + ' seconds'\"></button></template></div></div><div class=\"relative\" @click.outside=\"speedOpen = false\"><button type=\"button\" @click=\"speedOpen = !speedOpen\" class=\"text-xs px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors inline-flex items-center gap-1.5\" :aria-label=\"'Playback speed, currently ' + speed + ' times'\" style=\"color: var(--text-secondary);\"><span>Speed</span> <span class=\"font-semibold\" style=\"color: var(--text-primary);\" x-text=\"speed + '×'\">1×</span> <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"></path></svg></button><div x-show=\"speedOpen\" x-cloak class=\"absolute bottom-full mb-2 left-1/2 -translate-x-1/2 rounded-xl shadow-lg p-1.5 flex flex-col min-w-[7rem]\" style=\"background-color: var(--bg-secondary); border: 1px solid var(--border); z-index: 30;\"><template x-for=\"rate in speeds\" :key=\"'speed' + rate\"><button type=\"button\" @click=\"pickSpeed(rate)\" class=\"w-full text-left px-3 py-1.5 rounded-md text-sm\" :class=\"isSpeedActive(rate) ? 'font-semibold' : ''\" :style=\"isSpeedActive(rate) ? 'background-color: var(--accent-muted); color: var(--text-primary);' : 'color: var(--text-secondary);'\" x-text=\"rate + '×' + (isDefaultSpeed(rate) ? ' · default' : '')\"></button></template><!-- §8: pin the current rate as the user default —\n\t\t\t\t\t\t books without their own speed pick it up --><button type=\"button\" x-show=\"!isDefaultSpeed(speed)\" @click=\"pickDefaultSpeed(speed)\" class=\"w-full text-left px-3 py-1.5 mt-1 rounded-md text-xs border-t\" style=\"border-color: var(--border); color: var(--text-secondary);\" x-text=\"'Make ' + speed + '× my default'\"></button></div></div><button type=\"button\" @click=\"toggleBookmarkCapture()\" class=\"text-xs px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors inline-flex items-center gap-1.5\" :class=\"bookmarkCaptureOpen ? 'opacity-100' : 'opacity-70'\" aria-label=\"Add bookmark at current position\" style=\"color: var(--text-secondary);\"><svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z\"></path> <path d=\"M12 7v6M9 10h6\"></path></svg> <span>Bookmark</span></button><div class=\"relative\" @click.outside=\"sleepOpen = false\"><button type=\"button\" @click=\"sleepOpen = !sleepOpen\" class=\"text-xs px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors inline-flex items-center gap-1.5\" :aria-label=\"'Sleep timer, currently ' + sleepLabelStr\" style=\"color: var(--text-secondary);\"><svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"></path></svg> <span class=\"font-semibold tabular-nums\" style=\"color: var(--text-primary);\" x-text=\"sleepLabelStr === 'Off' ? 'Sleep' : sleepLabelStr\">Sleep</span> <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"></path></svg></button><div xLine truncated templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</div></div><!-- Settings strip: Skip ▾ · Speed ▾ · Bookmark · Volume ▾ --><div class=\"w-full max-w-xl flex flex-wrap items-center justify-center gap-2\"><div class=\"relative\" @click.outside=\"skipOpen = false\"><button type=\"button\" @click=\"skipOpen = !skipOpen\" class=\"text-xs px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors inline-flex items-center gap-1.5\" :aria-label=\"'Skip duration, currently ' + skipSeconds + ' seconds'\" style=\"color: var(--text-secondary);\"><span>Skip</span> <span class=\"font-semibold\" style=\"color: var(--text-primary);\" x-text=\"skipSeconds + 's'\">10s</span> <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"></path></svg></button><div x-show=\"skipOpen\" x-cloak class=\"absolute bottom-full mb-2 left-1/2 -translate-x-1/2 rounded-xl shadow-lg p-1.5 flex flex-col min-w-[7.5rem]\" style=\"background-color: var(--bg-secondary); border: 1px solid var(--border); z-index: 30;\"><template x-for=\"secs in skipChoices\" :key=\"'skip' + secs\"><button type=\"button\" @click=\"pickSkip(secs)\" class=\"w-full text-left px-3 py-1.5 rounded-md text-sm\" :class=\"isSkipActive(secs) ? 'font-semibold' : ''\" :style=\"isSkipActive(secs) ? 'background-color: var(--accent-muted); color: var(--text-primary);' : 'color: var(--text-secondary);'\" x-text=\"secs + ' seconds'\"></button></template></div></div><div class=\"relative\" @click.outside=\"speedOpen = false\"><button type=\"button\" @click=\"speedOpen = !speedOpen\" class=\"text-xs px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors inline-flex items-center gap-1.5\" :aria-label=\"'Playback speed, currently ' + speed + ' times'\" style=\"color: var(--text-secondary);\"><span>Speed</span> <span class=\"font-semibold\" style=\"color: var(--text-primary);\" x-text=\"speed + '×'\">1×</span> <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"></path></svg></button><div x-show=\"speedOpen\" x-cloak class=\"absolute bottom-full mb-2 left-1/2 -translate-x-1/2 rounded-xl shadow-lg p-1.5 flex flex-col min-w-[7rem]\" style=\"background-color: var(--bg-secondary); border: 1px solid var(--border); z-index: 30;\"><template x-for=\"rate in speeds\" :key=\"'speed' + rate\"><button type=\"button\" @click=\"pickSpeed(rate)\" class=\"w-full text-left px-3 py-1.5 rounded-md text-sm\" :class=\"isSpeedActive(rate) ? 'font-semibold' : ''\" :style=\"isSpeedActive(rate) ? 'background-color: var(--accent-muted); color: var(--text-primary);' : 'color: var(--text-secondary);'\" x-text=\"rate + '×' + (isDefaultSpeed(rate) ? ' · default' : '')\"></button></template><!-- §8: pin the current rate as the user default —\n\t\t\t\t\t\t books without their own speed pick it up --><button type=\"button\" x-show=\"!isDefaultSpeed(speed)\" @click=\"pickDefaultSpeed(speed)\" class=\"w-full text-left px-3 py-1.5 mt-1 rounded-md text-xs border-t\" style=\"border-color: var(--border); color: var(--text-secondary);\" x-text=\"'Make ' + speed + '× my default'\"></button></div></div><button type=\"button\" @click=\"toggleBookmarkCapture()\" class=\"text-xs px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors inline-flex items-center gap-1.5\" :class=\"bookmarkCaptureOpen ? 'opacity-100' : 'opacity-70'\" aria-label=\"Add bookmark at current position\" style=\"color: var(--text-secondary);\"><svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z\"></path> <path d=\"M12 7v6M9 10h6\"></path></svg> <span>Bookmark</span></button><div class=\"relative\" @click.outside=\"sleepOpen = false\"><button type=\"button\" @click=\"sleepOpen = !sleepOpen\" class=\"text-xs px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors inline-flex items-center gap-1.5\" :aria-label=\"'Sleep timer, currently ' + sleepLabelStr\" style=\"color: var(--text-secondary);\"><svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\"></path></svg> <span class=\"font-semibold tabular-nums\" style=\"color: var(--text-primary);\" x-text=\"sleepLabelStr === 'Off' ? 'Sleep' : sleepLabelStr\">Sleep</span> <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m6 9 6 6 6-6\"></path></svg></butLine truncated
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+2 -2
View File
@@ -659,8 +659,8 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
</div> </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 flex-wrap items-center justify-between gap-x-6 gap-y-3 p-6 border-t flex-shrink-0" style="border-color: var(--border);">
<div class="flex items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<button <button
@click="rescanBook()" @click="rescanBook()"
:disabled="rescanning" :disabled="rescanning"
+1 -1
View File
@@ -1477,7 +1477,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 156, "\" 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, 156, "\" readonly class=\"input opacity-60 font-mono text-xs\"></div></div></div></div></div><div class=\"flex flex-wrap items-center justify-between gap-x-6 gap-y-3 p-6 border-t flex-shrink-0\" style=\"border-color: var(--border);\"><div class=\"flex flex-wrap 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+1 -1
View File
@@ -284,7 +284,7 @@ templ BookShelf(
<div id="filter-save-error" style="display: none; color: var(--status-danger); padding: 0.75rem; border-radius: 0.5rem; margin-top: 0.75rem;"></div> <div id="filter-save-error" style="display: none; color: var(--status-danger); padding: 0.75rem; border-radius: 0.5rem; margin-top: 0.75rem;"></div>
<input type="hidden" name="resource_type" value="media-items"/> <input type="hidden" name="resource_type" value="media-items"/>
</div> </div>
<div class="flex justify-end gap-2"> <div class="flex flex-wrap justify-end gap-2">
<button type="button" @click="showSaveModal = false" class="btn btn-secondary">Cancel</button> <button type="button" @click="showSaveModal = false" class="btn btn-secondary">Cancel</button>
<button type="submit" class="btn btn-secondary">Save Filter</button> <button type="submit" class="btn btn-secondary">Save Filter</button>
<!-- Item 3.5: the bridge — the current tree becomes a live <!-- Item 3.5: the bridge — the current tree becomes a live
+1 -1
View File
@@ -562,7 +562,7 @@ func BookShelf(
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</button></div><form id=\"save-filter-form\" hx-post=\"/api/saved-filters\" hx-target=\"#saved-filters-list\" hx-swap=\"beforeend\" hx-include=\"#filter-form\" @htmx:afterRequest=\"if(event.detail.xhr.status < 400) { afterFilterSave() }\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Filter Name</label> <input type=\"text\" id=\"save-filter-name\" name=\"filter_name\" placeholder=\"My Custom Filter\" class=\"input\" required><div id=\"filter-save-error\" style=\"display: none; color: var(--status-danger); padding: 0.75rem; border-radius: 0.5rem; margin-top: 0.75rem;\"></div><input type=\"hidden\" name=\"resource_type\" value=\"media-items\"></div><div class=\"flex justify-end gap-2\"><button type=\"button\" @click=\"showSaveModal = false\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" class=\"btn btn-secondary\">Save Filter</button><!-- Item 3.5: the bridge — the current tree becomes a live\n\t\t\t\t\t\t\t smart collection (the qf engine drives it everywhere:\n\t\t\t\t\t\t\t shelf, collections, play chains). --><button type=\"button\" @click=\"saveFilterAsCollection()\" class=\"btn btn-primary\">Save as Collection</button></div><input type=\"hidden\" name=\"limit\" value=\"50\"> <input type=\"hidden\" name=\"offset\" value=\"0\"></form></div></div></body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</button></div><form id=\"save-filter-form\" hx-post=\"/api/saved-filters\" hx-target=\"#saved-filters-list\" hx-swap=\"beforeend\" hx-include=\"#filter-form\" @htmx:afterRequest=\"if(event.detail.xhr.status < 400) { afterFilterSave() }\"><div class=\"mb-4\"><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Filter Name</label> <input type=\"text\" id=\"save-filter-name\" name=\"filter_name\" placeholder=\"My Custom Filter\" class=\"input\" required><div id=\"filter-save-error\" style=\"display: none; color: var(--status-danger); padding: 0.75rem; border-radius: 0.5rem; margin-top: 0.75rem;\"></div><input type=\"hidden\" name=\"resource_type\" value=\"media-items\"></div><div class=\"flex flex-wrap justify-end gap-2\"><button type=\"button\" @click=\"showSaveModal = false\" class=\"btn btn-secondary\">Cancel</button> <button type=\"submit\" class=\"btn btn-secondary\">Save Filter</button><!-- Item 3.5: the bridge — the current tree becomes a live\n\t\t\t\t\t\t\t smart collection (the qf engine drives it everywhere:\n\t\t\t\t\t\t\t shelf, collections, play chains). --><button type=\"button\" @click=\"saveFilterAsCollection()\" class=\"btn btn-primary\">Save as Collection</button></div><input type=\"hidden\" name=\"limit\" value=\"50\"> <input type=\"hidden\" name=\"offset\" value=\"0\"></form></div></div></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+1 -1
View File
@@ -34,7 +34,7 @@ templ CollectionRules(user User, collection CollectionData, fieldsJSON string, t
<span class="grid place-items-center h-12 w-12 rounded-2xl shrink-0 text-2xl" style={ "background-color: var(--accent-muted); color: " + collection.Color }>{ collection.Icon }</span> <span class="grid place-items-center h-12 w-12 rounded-2xl shrink-0 text-2xl" style={ "background-color: var(--accent-muted); color: " + collection.Color }>{ collection.Icon }</span>
<div> <div>
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">{ collection.Name }</h1> <h1 class="text-3xl font-bold" style="color: var(--text-primary)">{ collection.Name }</h1>
<p class="flex items-center gap-2" style="color: var(--text-secondary)"> <p class="flex flex-wrap items-center gap-2" style="color: var(--text-secondary)">
<span class="badge text-xs" style="background-color: var(--accent-muted); color: var(--accent);">⚡ Smart</span> <span class="badge text-xs" style="background-color: var(--accent-muted); color: var(--accent);">⚡ Smart</span>
if scope == "episodes" { if scope == "episodes" {
Episodes scope — membership recomputes live from the tree below. Episodes scope — membership recomputes live from the tree below.
+1 -1
View File
@@ -152,7 +152,7 @@ func CollectionRules(user User, collection CollectionData, fieldsJSON string, tr
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</h1><p class=\"flex items-center gap-2\" style=\"color: var(--text-secondary)\"><span class=\"badge text-xs\" style=\"background-color: var(--accent-muted); color: var(--accent);\">⚡ Smart</span> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</h1><p class=\"flex flex-wrap items-center gap-2\" style=\"color: var(--text-secondary)\"><span class=\"badge text-xs\" style=\"background-color: var(--accent-muted); color: var(--accent);\">⚡ Smart</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+3 -3
View File
@@ -60,15 +60,15 @@ templ ConflictsListContent(user User, conflicts []handlers.ConflictDetailRespons
templ ConflictStatsBar(total int, unresolved int) { templ ConflictStatsBar(total int, unresolved int) {
<div class="stat-card text-center py-3"> <div class="stat-card text-center py-3">
<div class="text-2xl font-bold" style="color: var(--text-primary);">{ total }</div> <div class="text-2xl font-bold" style="color: var(--text-primary);">{ total }</div>
<div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Total</div> <div class="text-xs uppercase tracking-normal" style="color: var(--text-secondary);">Total</div>
</div> </div>
<div class="stat-card text-center py-3"> <div class="stat-card text-center py-3">
<div class="text-2xl font-bold" style="color: var(--status-warning);">{ unresolved }</div> <div class="text-2xl font-bold" style="color: var(--status-warning);">{ unresolved }</div>
<div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Unresolved</div> <div class="text-xs uppercase tracking-normal" style="color: var(--text-secondary);">Unresolved</div>
</div> </div>
<div class="stat-card text-center py-3"> <div class="stat-card text-center py-3">
<div class="text-2xl font-bold" style="color: var(--status-success);">{ total - unresolved }</div> <div class="text-2xl font-bold" style="color: var(--status-success);">{ total - unresolved }</div>
<div class="text-xs uppercase tracking-wide" style="color: var(--text-secondary);">Resolved</div> <div class="text-xs uppercase tracking-normal" style="color: var(--text-secondary);">Resolved</div>
</div> </div>
} }
+3 -3
View File
@@ -219,7 +219,7 @@ func ConflictStatsBar(total int, unresolved int) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div><div class=\"text-xs uppercase tracking-wide\" style=\"color: var(--text-secondary);\">Total</div></div><div class=\"stat-card text-center py-3\"><div class=\"text-2xl font-bold\" style=\"color: var(--status-warning);\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div><div class=\"text-xs uppercase tracking-normal\" style=\"color: var(--text-secondary);\">Total</div></div><div class=\"stat-card text-center py-3\"><div class=\"text-2xl font-bold\" style=\"color: var(--status-warning);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -232,7 +232,7 @@ func ConflictStatsBar(total int, unresolved int) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div><div class=\"text-xs uppercase tracking-wide\" style=\"color: var(--text-secondary);\">Unresolved</div></div><div class=\"stat-card text-center py-3\"><div class=\"text-2xl font-bold\" style=\"color: var(--status-success);\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div><div class=\"text-xs uppercase tracking-normal\" style=\"color: var(--text-secondary);\">Unresolved</div></div><div class=\"stat-card text-center py-3\"><div class=\"text-2xl font-bold\" style=\"color: var(--status-success);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -245,7 +245,7 @@ func ConflictStatsBar(total int, unresolved int) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div><div class=\"text-xs uppercase tracking-wide\" style=\"color: var(--text-secondary);\">Resolved</div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div><div class=\"text-xs uppercase tracking-normal\" style=\"color: var(--text-secondary);\">Resolved</div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+1 -1
View File
@@ -70,7 +70,7 @@ templ CollectionCarousel(section handlers.SectionData) {
if section.ViewAllURL != "" { if section.ViewAllURL != "" {
<a <a
href={ section.ViewAllURL } href={ section.ViewAllURL }
class="inline-flex items-center gap-1 text-sm font-medium hover:underline transition-colors" class="inline-flex shrink-0 items-center gap-1 whitespace-nowrap text-sm font-medium hover:underline transition-colors"
style="color: var(--accent);" style="color: var(--accent);"
> >
View All View All
+1 -1
View File
@@ -256,7 +256,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" class=\"inline-flex items-center gap-1 text-sm font-medium hover:underline transition-colors\" style=\"color: var(--accent);\">View All") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" class=\"inline-flex shrink-0 items-center gap-1 whitespace-nowrap text-sm font-medium hover:underline transition-colors\" style=\"color: var(--accent);\">View All")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
+36 -30
View File
@@ -120,21 +120,25 @@ templ PodcastDetailPage(user User, data PodcastDetailData) {
<span>Refresh</span> <span>Refresh</span>
</button> </button>
<!-- Order override (D5): per-user; refreshes the episode list --> <!-- Order override (D5): per-user; refreshes the episode list.
<label class="text-sm ml-2" for="podcast-order-select" style="color: var(--text-secondary)">Order</label> Label+select travel as a pair so flex-wrap can never
<select separate them on narrow screens. -->
id="podcast-order-select" <div class="flex items-center gap-1.5">
name="order" <label class="text-sm" for="podcast-order-select" style="color: var(--text-secondary)">Order</label>
class="input w-auto py-1.5 cursor-pointer" <select
hx-put={ "/api/podcasts/feeds/" + data.Feed.ID + "/order" } id="podcast-order-select"
hx-target="#episode-list" name="order"
hx-swap="outerHTML" class="input w-auto py-1.5 cursor-pointer"
> hx-put={ "/api/podcasts/feeds/" + data.Feed.ID + "/order" }
<option value="native" selected?={ data.Order == "native" }>Feed order</option> hx-target="#episode-list"
<option value="reverse" selected?={ data.Order == "reverse" }>Feed order (reversed)</option> hx-swap="outerHTML"
<option value="oldest" selected?={ data.Order == "oldest" }>Oldest first (pub date)</option> >
<option value="newest" selected?={ data.Order == "newest" }>Newest first (pub date)</option> <option value="native" selected?={ data.Order == "native" }>Feed order</option>
</select> <option value="reverse" selected?={ data.Order == "reverse" }>Feed order (reversed)</option>
<option value="oldest" selected?={ data.Order == "oldest" }>Oldest first (pub date)</option>
<option value="newest" selected?={ data.Order == "newest" }>Newest first (pub date)</option>
</select>
</div>
<!-- Serial suggestion (D5, §0 item 3 rename): one-click, <!-- Serial suggestion (D5, §0 item 3 rename): one-click,
never silent. "reverse" sorts by FEED POSITION DESC — never silent. "reverse" sorts by FEED POSITION DESC —
@@ -152,21 +156,23 @@ templ PodcastDetailPage(user User, data PodcastDetailData) {
</button> </button>
} }
<!-- Auto-download policy (D6) --> <!-- Auto-download policy (D6) — label+controls as one pair -->
<label class="text-sm ml-2" for="podcast-autodl-select" style="color: var(--text-secondary)">Auto-download</label> <div class="flex items-center gap-1.5">
@PodcastAutoDownloadSelect(data.Feed) <label class="text-sm" for="podcast-autodl-select" style="color: var(--text-secondary)">Auto-download</label>
if data.Feed.AutoDownload == "newest" { @PodcastAutoDownloadSelect(data.Feed)
<input if data.Feed.AutoDownload == "newest" {
type="number" <input
id="podcast-autodl-count" type="number"
name="auto_download_count" id="podcast-autodl-count"
min="1" name="auto_download_count"
max="100" min="1"
value={ podcastInt(data.Feed.AutoDownloadCount) } max="100"
class="input w-20 py-1.5" value={ podcastInt(data.Feed.AutoDownloadCount) }
aria-label="Newest N episodes" class="input w-20 py-1.5"
/> aria-label="Newest N episodes"
} />
}
</div>
<!-- Unsubscribe (issues #4/#7): the sole subscriber who is <!-- Unsubscribe (issues #4/#7): the sole subscriber who is
an admin gets the three-choice modal (unsubscribe / an admin gets the three-choice modal (unsubscribe /
+42 -42
View File
@@ -359,14 +359,14 @@ func PodcastDetailPage(user User, data PodcastDetailData) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<span>Refresh</span></button><!-- Order override (D5): per-user; refreshes the episode list --><label class=\"text-sm ml-2\" for=\"podcast-order-select\" style=\"color: var(--text-secondary)\">Order</label> <select id=\"podcast-order-select\" name=\"order\" class=\"input w-auto py-1.5 cursor-pointer\" hx-put=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<span>Refresh</span></button><!-- Order override (D5): per-user; refreshes the episode list.\n\t\t\t\t\t Label+select travel as a pair so flex-wrap can never\n\t\t\t\t\t separate them on narrow screens. --><div class=\"flex items-center gap-1.5\"><label class=\"text-sm\" for=\"podcast-order-select\" style=\"color: var(--text-secondary)\">Order</label> <select id=\"podcast-order-select\" name=\"order\" class=\"input w-auto py-1.5 cursor-pointer\" hx-put=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var18 string var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + data.Feed.ID + "/order") templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + data.Feed.ID + "/order")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 129, Col: 63} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 132, Col: 64}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -412,7 +412,7 @@ func PodcastDetailPage(user User, data PodcastDetailData) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, ">Newest first (pub date)</option></select><!-- Serial suggestion (D5, §0 item 3 rename): one-click,\n\t\t\t\t\t never silent. \"reverse\" sorts by FEED POSITION DESC —\n\t\t\t\t\t no pub dates involved, so messy dates can't break it. -->") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, ">Newest first (pub date)</option></select></div><!-- Serial suggestion (D5, §0 item 3 rename): one-click,\n\t\t\t\t\t never silent. \"reverse\" sorts by FEED POSITION DESC —\n\t\t\t\t\t no pub dates involved, so messy dates can't break it. -->")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -424,7 +424,7 @@ func PodcastDetailPage(user User, data PodcastDetailData) templ.Component {
var templ_7745c5c3_Var19 string var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + data.Feed.ID + "/order") templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + data.Feed.ID + "/order")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 145, Col: 64} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 149, Col: 64}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -435,7 +435,7 @@ func PodcastDetailPage(user User, data PodcastDetailData) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<!-- Auto-download policy (D6) --><label class=\"text-sm ml-2\" for=\"podcast-autodl-select\" style=\"color: var(--text-secondary)\">Auto-download</label>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<!-- Auto-download policy (D6) — label+controls as one pair --><div class=\"flex items-center gap-1.5\"><label class=\"text-sm\" for=\"podcast-autodl-select\" style=\"color: var(--text-secondary)\">Auto-download</label>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -451,7 +451,7 @@ func PodcastDetailPage(user User, data PodcastDetailData) templ.Component {
var templ_7745c5c3_Var20 string var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(podcastInt(data.Feed.AutoDownloadCount)) templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(podcastInt(data.Feed.AutoDownloadCount))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 165, Col: 54} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 170, Col: 55}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -462,7 +462,7 @@ func PodcastDetailPage(user User, data PodcastDetailData) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<!-- Unsubscribe (issues #4/#7): the sole subscriber who is\n\t\t\t\t\t an admin gets the three-choice modal (unsubscribe /\n\t\t\t\t\t remove feed / cancel); everyone else gets a plain\n\t\t\t\t\t confirm — file retention is admin knowledge only. -->") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</div><!-- Unsubscribe (issues #4/#7): the sole subscriber who is\n\t\t\t\t\t an admin gets the three-choice modal (unsubscribe /\n\t\t\t\t\t remove feed / cancel); everyone else gets a plain\n\t\t\t\t\t confirm — file retention is admin knowledge only. -->")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -479,7 +479,7 @@ func PodcastDetailPage(user User, data PodcastDetailData) templ.Component {
var templ_7745c5c3_Var21 string var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + data.Feed.ID + "/subscription") templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + data.Feed.ID + "/subscription")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 182, Col: 74} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 188, Col: 74}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -497,7 +497,7 @@ func PodcastDetailPage(user User, data PodcastDetailData) templ.Component {
var templ_7745c5c3_Var22 string var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + data.Feed.ID + "/subscription") templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + data.Feed.ID + "/subscription")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 191, Col: 74} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 197, Col: 74}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -516,7 +516,7 @@ func PodcastDetailPage(user User, data PodcastDetailData) templ.Component {
var templ_7745c5c3_Var23 string var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + data.Feed.ID) templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + data.Feed.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 203, Col: 56} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 209, Col: 56}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -555,7 +555,7 @@ func PodcastDetailPage(user User, data PodcastDetailData) templ.Component {
var templ_7745c5c3_Var24 string var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(podcastInt(data.GapCount) + " episode(s) not downloaded in the middle of this feed") templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(podcastInt(data.GapCount) + " episode(s) not downloaded in the middle of this feed")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 218, Col: 160} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 224, Col: 160}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -578,7 +578,7 @@ func PodcastDetailPage(user User, data PodcastDetailData) templ.Component {
var templ_7745c5c3_Var25 string var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + data.Feed.ID + "/episodes/delete-files") templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + data.Feed.ID + "/episodes/delete-files")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 258, Col: 82} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 264, Col: 82}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -651,7 +651,7 @@ func PodcastEpisodeList(data PodcastDetailData) templ.Component {
var templ_7745c5c3_Var27 string var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue("/podcasts/" + data.Feed.ID + "/episodes") templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue("/podcasts/" + data.Feed.ID + "/episodes")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 294, Col: 52} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 300, Col: 52}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -721,7 +721,7 @@ func PodcastEpisodeListPolling(data PodcastDetailData) templ.Component {
var templ_7745c5c3_Var29 string var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue("/podcasts/" + data.Feed.ID + "/episodes") templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue("/podcasts/" + data.Feed.ID + "/episodes")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 319, Col: 52} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 325, Col: 52}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -773,7 +773,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var31 string var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolAttr(ep.Downloaded)) templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(boolAttr(ep.Downloaded))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 336, Col: 43} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 342, Col: 43}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -786,7 +786,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var32 string var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(podcastPct(ep.ListenPct)) templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(podcastPct(ep.ListenPct))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 337, Col: 44} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 343, Col: 44}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -799,7 +799,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var33 string var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(strings.ToLower(ep.Title + " " + ep.Description)) templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(strings.ToLower(ep.Title + " " + ep.Description))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 338, Col: 69} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 344, Col: 69}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -818,7 +818,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var34 string var templ_7745c5c3_Var34 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(ep.ID) templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(ep.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 351, Col: 18} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 357, Col: 18}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -831,7 +831,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var35 string var templ_7745c5c3_Var35 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue("Select " + ep.Title + " for file deletion") templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue("Select " + ep.Title + " for file deletion")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 353, Col: 61} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 359, Col: 61}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -855,7 +855,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var36 string var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(podcastInt(ep.Position)) templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(podcastInt(ep.Position))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 361, Col: 28} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 367, Col: 28}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -868,7 +868,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var37 templ.SafeURL var templ_7745c5c3_Var37 templ.SafeURL
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinURLErrs(ep.PlayURL) templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinURLErrs(ep.PlayURL)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 365, Col: 24} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 371, Col: 24}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -881,7 +881,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var38 string var templ_7745c5c3_Var38 string
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(ep.Title) templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(ep.Title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 365, Col: 129} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 371, Col: 129}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -899,7 +899,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var39 string var templ_7745c5c3_Var39 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(ep.StateBadge()) templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(ep.StateBadge())
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 367, Col: 165} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 373, Col: 165}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -917,7 +917,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var40 string var templ_7745c5c3_Var40 string
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(ep.StateBadge()) templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(ep.StateBadge())
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 369, Col: 134} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 375, Col: 134}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -935,7 +935,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var41 string var templ_7745c5c3_Var41 string
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(ep.StateBadge()) templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(ep.StateBadge())
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 371, Col: 133} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 377, Col: 133}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -958,7 +958,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var42 string var templ_7745c5c3_Var42 string
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(ep.DownloadError) templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(ep.DownloadError)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 375, Col: 97} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 381, Col: 97}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -982,7 +982,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var43 string var templ_7745c5c3_Var43 string
templ_7745c5c3_Var43, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(progressWidth(ep.ProgressPct) + " background-color: var(--accent);") templ_7745c5c3_Var43, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(progressWidth(ep.ProgressPct) + " background-color: var(--accent);")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 381, Col: 130} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 387, Col: 130}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -995,7 +995,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var44 string var templ_7745c5c3_Var44 string
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(FormatEpisodeSize(ep.ProgressReceived) + " / " + FormatEpisodeSize(ep.ProgressTotal)) templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(FormatEpisodeSize(ep.ProgressReceived) + " / " + FormatEpisodeSize(ep.ProgressTotal))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 384, Col: 93} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 390, Col: 93}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -1013,7 +1013,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var45 string var templ_7745c5c3_Var45 string
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(FormatEpisodeSize(ep.ProgressReceived)) templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(FormatEpisodeSize(ep.ProgressReceived))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 390, Col: 122} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 396, Col: 122}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -1036,7 +1036,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var46 string var templ_7745c5c3_Var46 string
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(ep.Date) templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(ep.Date)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 395, Col: 13} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 401, Col: 13}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -1050,7 +1050,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var47 string var templ_7745c5c3_Var47 string
templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(" · " + ep.Duration) templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(" · " + ep.Duration)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 397, Col: 27} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 403, Col: 27}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -1065,7 +1065,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var48 string var templ_7745c5c3_Var48 string
templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(" · " + ep.Size) templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(" · " + ep.Size)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 400, Col: 23} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 406, Col: 23}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -1084,7 +1084,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var49 templ.SafeURL var templ_7745c5c3_Var49 templ.SafeURL
templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinURLErrs(ep.PlayURL) templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinURLErrs(ep.PlayURL)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 405, Col: 23} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 411, Col: 23}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -1145,7 +1145,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var52 string var templ_7745c5c3_Var52 string
templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + ep.FeedID + "/episodes/" + ep.ID + "/download") templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + ep.FeedID + "/episodes/" + ep.ID + "/download")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 417, Col: 85} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 423, Col: 85}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var52) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var52)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -1158,7 +1158,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var53 string var templ_7745c5c3_Var53 string
templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.ResolveAttributeValue(downloadButtonTitle(ep.DownloadState)) templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.ResolveAttributeValue(downloadButtonTitle(ep.DownloadState))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 419, Col: 49} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 425, Col: 49}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var53) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var53)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -1179,7 +1179,7 @@ func PodcastEpisodeRowView(ep PodcastEpisodeRow, isAdmin bool) templ.Component {
var templ_7745c5c3_Var54 string var templ_7745c5c3_Var54 string
templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(downloadButtonLabel(ep.DownloadState)) templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(downloadButtonLabel(ep.DownloadState))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 422, Col: 49} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 428, Col: 49}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -1258,7 +1258,7 @@ func PodcastAutoDownloadSelect(feed PodcastFeedCard) templ.Component {
var templ_7745c5c3_Var56 string var templ_7745c5c3_Var56 string
templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + feed.ID + "/subscription") templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + feed.ID + "/subscription")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 466, Col: 61} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 472, Col: 61}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var56) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var56)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -1334,7 +1334,7 @@ func PodcastOrderSelectOOB(feedID string, order string) templ.Component {
var templ_7745c5c3_Var58 string var templ_7745c5c3_Var58 string
templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + feedID + "/order") templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + feedID + "/order")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 486, Col: 53} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 492, Col: 53}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var58) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var58)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -1418,7 +1418,7 @@ func PodcastAutoDownloadSelectOOB(feed PodcastFeedCard) templ.Component {
var templ_7745c5c3_Var60 string var templ_7745c5c3_Var60 string
templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + feed.ID + "/subscription") templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + feed.ID + "/subscription")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 505, Col: 61} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 511, Col: 61}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -1494,7 +1494,7 @@ func PodcastUnsubscribeModal(data PodcastDetailData) templ.Component {
var templ_7745c5c3_Var62 string var templ_7745c5c3_Var62 string
templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(data.Feed.DisplayTitle()) templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(data.Feed.DisplayTitle())
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 530, Col: 112} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 536, Col: 112}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -1507,7 +1507,7 @@ func PodcastUnsubscribeModal(data PodcastDetailData) templ.Component {
var templ_7745c5c3_Var63 string var templ_7745c5c3_Var63 string
templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + data.Feed.ID + "/subscription") templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + data.Feed.ID + "/subscription")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 536, Col: 72} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 542, Col: 72}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -1520,7 +1520,7 @@ func PodcastUnsubscribeModal(data PodcastDetailData) templ.Component {
var templ_7745c5c3_Var64 string var templ_7745c5c3_Var64 string
templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + data.Feed.ID) templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/podcasts/feeds/" + data.Feed.ID)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 545, Col: 54} return templ.Error{Err: templ_7745c5c3_Err, FileName: `podcast_detail.templ`, Line: 551, Col: 54}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
+2 -2
View File
@@ -104,9 +104,9 @@ templ Progress(user User, progressData []handlers.ProgressWithMedia, errorMessag
</div> </div>
} }
if item.EpubCFI != "" { if item.EpubCFI != "" {
<div class="mt-2 flex items-center gap-2 text-xs" style="color: var(--text-secondary);"> <div class="mt-2 flex items-center gap-2 text-xs min-w-0" style="color: var(--text-secondary);">
@Icon("bookmark", "h-3.5 w-3.5") @Icon("bookmark", "h-3.5 w-3.5")
<span>CFI: { item.EpubCFI }</span> <span class="min-w-0 break-all">CFI: { item.EpubCFI }</span>
</div> </div>
} }
</div> </div>
+3 -3
View File
@@ -308,7 +308,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
} }
} }
if item.EpubCFI != "" { if item.EpubCFI != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"mt-2 flex items-center gap-2 text-xs\" style=\"color: var(--text-secondary);\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"mt-2 flex items-center gap-2 text-xs min-w-0\" style=\"color: var(--text-secondary);\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -316,14 +316,14 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<span>CFI: ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<span class=\"min-w-0 break-all\">CFI: ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var19 string var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(item.EpubCFI) templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(item.EpubCFI)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 109, Col: 36} return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 109, Col: 62}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
+1 -1
View File
@@ -29,7 +29,7 @@ templ Stats(user User) {
<!-- READING tab — behavior stats from sync pushes (all libraries) --> <!-- READING tab — behavior stats from sync pushes (all libraries) -->
<div id="stats-reading-section"> <div id="stats-reading-section">
<div class="card p-4 mb-6 flex flex-wrap items-center justify-between gap-3"> <div class="card p-4 mb-6 flex flex-wrap items-center justify-between gap-3">
<div class="flex gap-2" id="stats-range-selector"> <div class="flex flex-wrap gap-2" id="stats-range-selector">
<button data-range="7" @click="selectRange('7')" class="stats-range-btn btn btn-secondary text-sm">Last 7 days</button> <button data-range="7" @click="selectRange('7')" class="stats-range-btn btn btn-secondary text-sm">Last 7 days</button>
<button data-range="30" @click="selectRange('30')" class="stats-range-btn btn btn-secondary text-sm">30 days</button> <button data-range="30" @click="selectRange('30')" class="stats-range-btn btn btn-secondary text-sm">30 days</button>
<button data-range="90" @click="selectRange('90')" class="stats-range-btn btn btn-secondary text-sm">90 days</button> <button data-range="90" @click="selectRange('90')" class="stats-range-btn btn btn-secondary text-sm">90 days</button>
+1 -1
View File
@@ -80,7 +80,7 @@ func Stats(user User) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, ") · totals derive from your devices' sync pushes, not a stopwatch.</p><div class=\"flex gap-2 mt-4\" id=\"stats-tab-row\"><button data-tab=\"reading\" @click=\"selectTab('reading')\" class=\"stats-tab-btn btn btn-secondary text-sm\">Reading</button> <button data-tab=\"library\" @click=\"selectTab('library')\" class=\"stats-tab-btn btn btn-secondary text-sm\">Library</button></div></div><!-- READING tab — behavior stats from sync pushes (all libraries) --><div id=\"stats-reading-section\"><div class=\"card p-4 mb-6 flex flex-wrap items-center justify-between gap-3\"><div class=\"flex gap-2\" id=\"stats-range-selector\"><button data-range=\"7\" @click=\"selectRange('7')\" class=\"stats-range-btn btn btn-secondary text-sm\">Last 7 days</button> <button data-range=\"30\" @click=\"selectRange('30')\" class=\"stats-range-btn btn btn-secondary text-sm\">30 days</button> <button data-range=\"90\" @click=\"selectRange('90')\" class=\"stats-range-btn btn btn-secondary text-sm\">90 days</button> <button data-range=\"year\" @click=\"selectRange('year')\" class=\"stats-range-btn btn btn-secondary text-sm\">This year</button></div><p class=\"text-xs\" style=\"color: var(--text-secondary)\" id=\"stats-window-label\"></p></div><div class=\"grid grid-cols-2 lg:grid-cols-5 gap-4 mb-8\"><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Reading Time</h3><p id=\"stats-total-minutes\" class=\"text-3xl font-bold\" style=\"color: var(--accent)\">–</p></div><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\" id=\"stats-pages-card-label\">Pages</h3><p id=\"stats-total-pages\" class=\"text-3xl font-bold\" style=\"color: var(--accent)\">–</p></div><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Streaks</h3><p id=\"stats-streaks\" class=\"text-lg font-bold leading-tight mt-1\" style=\"color: var(--text-primary)\">–</p></div><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Books Finished</h3><p id=\"stats-books-finished\" class=\"text-3xl font-bold\" style=\"color: var(--accent)\">–</p></div><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Most Active</h3><p id=\"stats-most-active\" class=\"text-sm font-bold leading-snug mt-2\" style=\"color: var(--text-primary)\">–</p></div></div><div class=\"grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8\"><div class=\"card p-6\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary)\">Daily Reading Minutes</h3><div class=\"h-64\"><canvas id=\"stats-daily-minutes-chart\"></canvas></div></div><div class=\"card p-6\" id=\"stats-pages-chart-panel\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary)\" id=\"stats-pages-chart-label\">Pages per Day</h3><div class=\"h-64\"><canvas id=\"stats-daily-pages-chart\"></canvas></div></div><div class=\"card p-6 hidden\" id=\"stats-listening-chart-panel\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary)\">Listening per Day (hrs)</h3><div class=\"h-64\"><canvas id=\"stats-daily-listening-chart\"></canvas></div></div></div><div class=\"grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8\"><div class=\"card p-6\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary)\">By Library</h3><div id=\"stats-library-breakdown\" class=\"space-y-3\"><div class=\"text-center py-6 text-sm\" style=\"color: var(--text-secondary)\">Loading…</div></div></div><div class=\"card p-6\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary)\">By Device</h3><p class=\"text-xs mb-3\" style=\"color: var(--text-secondary)\">Where the sync pushes came from — registered device names, plus a Web bucket for pushes that carry no device credential (the web reader).</p><div id=\"stats-device-breakdown\" class=\"space-y-3\"><div class=\"text-center py-6 text-sm\" style=\"color: var(--text-secondary)\">Loading…</div></div></div><div class=\"card p-6\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary)\">Most Read</h3><div id=\"stats-top-books\" class=\"space-y-3\"><div class=\"text-center py-6 text-sm\" style=\"color: var(--text-secondary)\">Loading…</div></div></div></div><div id=\"stats-empty\" class=\"card p-8 text-center hidden\"><p class=\"text-sm mb-2\" style=\"color: var(--text-primary)\">No reading sessions recorded in this range yet.</p><p class=\"text-xs\" style=\"color: var(--text-secondary)\">Sessions are captured every time a deviLine truncated templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, ") · totals derive from your devices' sync pushes, not a stopwatch.</p><div class=\"flex gap-2 mt-4\" id=\"stats-tab-row\"><button data-tab=\"reading\" @click=\"selectTab('reading')\" class=\"stats-tab-btn btn btn-secondary text-sm\">Reading</button> <button data-tab=\"library\" @click=\"selectTab('library')\" class=\"stats-tab-btn btn btn-secondary text-sm\">Library</button></div></div><!-- READING tab — behavior stats from sync pushes (all libraries) --><div id=\"stats-reading-section\"><div class=\"card p-4 mb-6 flex flex-wrap items-center justify-between gap-3\"><div class=\"flex flex-wrap gap-2\" id=\"stats-range-selector\"><button data-range=\"7\" @click=\"selectRange('7')\" class=\"stats-range-btn btn btn-secondary text-sm\">Last 7 days</button> <button data-range=\"30\" @click=\"selectRange('30')\" class=\"stats-range-btn btn btn-secondary text-sm\">30 days</button> <button data-range=\"90\" @click=\"selectRange('90')\" class=\"stats-range-btn btn btn-secondary text-sm\">90 days</button> <button data-range=\"year\" @click=\"selectRange('year')\" class=\"stats-range-btn btn btn-secondary text-sm\">This year</button></div><p class=\"text-xs\" style=\"color: var(--text-secondary)\" id=\"stats-window-label\"></p></div><div class=\"grid grid-cols-2 lg:grid-cols-5 gap-4 mb-8\"><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Reading Time</h3><p id=\"stats-total-minutes\" class=\"text-3xl font-bold\" style=\"color: var(--accent)\">–</p></div><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\" id=\"stats-pages-card-label\">Pages</h3><p id=\"stats-total-pages\" class=\"text-3xl font-bold\" style=\"color: var(--accent)\">–</p></div><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Streaks</h3><p id=\"stats-streaks\" class=\"text-lg font-bold leading-tight mt-1\" style=\"color: var(--text-primary)\">–</p></div><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Books Finished</h3><p id=\"stats-books-finished\" class=\"text-3xl font-bold\" style=\"color: var(--accent)\">–</p></div><div class=\"stat-card\"><h3 class=\"text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Most Active</h3><p id=\"stats-most-active\" class=\"text-sm font-bold leading-snug mt-2\" style=\"color: var(--text-primary)\">–</p></div></div><div class=\"grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8\"><div class=\"card p-6\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary)\">Daily Reading Minutes</h3><div class=\"h-64\"><canvas id=\"stats-daily-minutes-chart\"></canvas></div></div><div class=\"card p-6\" id=\"stats-pages-chart-panel\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary)\" id=\"stats-pages-chart-label\">Pages per Day</h3><div class=\"h-64\"><canvas id=\"stats-daily-pages-chart\"></canvas></div></div><div class=\"card p-6 hidden\" id=\"stats-listening-chart-panel\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary)\">Listening per Day (hrs)</h3><div class=\"h-64\"><canvas id=\"stats-daily-listening-chart\"></canvas></div></div></div><div class=\"grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8\"><div class=\"card p-6\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary)\">By Library</h3><div id=\"stats-library-breakdown\" class=\"space-y-3\"><div class=\"text-center py-6 text-sm\" style=\"color: var(--text-secondary)\">Loading…</div></div></div><div class=\"card p-6\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary)\">By Device</h3><p class=\"text-xs mb-3\" style=\"color: var(--text-secondary)\">Where the sync pushes came from — registered device names, plus a Web bucket for pushes that carry no device credential (the web reader).</p><div id=\"stats-device-breakdown\" class=\"space-y-3\"><div class=\"text-center py-6 text-sm\" style=\"color: var(--text-secondary)\">Loading…</div></div></div><div class=\"card p-6\"><h3 class=\"text-sm font-semibold uppercase tracking-wide mb-4\" style=\"color: var(--text-secondary)\">Most Read</h3><div id=\"stats-top-books\" class=\"space-y-3\"><div class=\"text-center py-6 text-sm\" style=\"color: var(--text-secondary)\">Loading…</div></div></div></div><div id=\"stats-empty\" class=\"card p-8 text-center hidden\"><p class=\"text-sm mb-2\" style=\"color: var(--text-primary)\">No reading sessions recorded in this range yet.</p><p class=\"text-xs\" style=\"color: var(--text-secondary)\">Sessions are captured every tLine truncated
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }