Files
bookhoard/templates/admin_podcasts.templ
T
John O'Keefe 88adf30aa9 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).
2026-11-02 09:30:55 -05:00

128 lines
5.6 KiB
Templ

package templates
// Admin all-podcasts view (§0 item 8 / Phase-4 item 1): EVERY feed from
// EVERY user with its maintenance aggregates, sorted by disk usage.
// The maintenance surface for the podcast subsystem — files are the
// disk hogs, so the hogs surface first. Actions: Refresh (re-fetch),
// Delete files (rows → pending, disk freed), Remove feed (rows + files
// + subscriptions gone — the same admin delete the detail page offers).
// AdminPodcastFeedData is one row of the admin all-podcasts table.
type AdminPodcastFeedData struct {
ID string
Title string
Host string
LibraryName string
Subscribers int
SubscriberNames string
EpisodeTotal int
EpisodeDownloaded int
DiskBytes int64
LastFetched string
}
templ AdminPodcasts(user User, feeds []AdminPodcastFeedData, totalFeeds int, totalDownloaded int, totalEpisodes int, totalBytes int64) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>All Podcasts - Bookhoard</title>
<link href="/static/style.css" rel="stylesheet"/>
<script type="module" src={ "/static/main.js?v=" + MainJSVersion() }></script>
</head>
<body class={ "theme-" + user.Theme }>
@Header(user, "/admin/podcasts")
<main class="container mx-auto px-4 py-8">
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
<h1 class="text-2xl font-bold" style="color: var(--text-primary)">All Podcasts</h1>
<div class="text-sm tabular-nums" style="color: var(--text-secondary)">
{ podcastInt(totalFeeds) } feeds · { podcastInt(totalDownloaded) }/{ podcastInt(totalEpisodes) } episodes downloaded · { FormatEpisodeSize(totalBytes) } on disk
</div>
</div>
<p class="text-sm mb-6" style="color: var(--text-secondary)">
Maintenance view: every feed from every user. "Delete files" frees the disk and
resets episodes to pending (they re-download on demand — a feed refresh will NOT
re-materialize them). "Remove feed" deletes rows, files and subscriptions for everyone.
</p>
if len(feeds) == 0 {
<div class="card p-6 text-center text-sm" style="color: var(--text-secondary)">No podcast feeds exist yet.</div>
} else {
<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>
for _, f := range feeds {
<tr style="border-bottom: 1px solid var(--border);" id={ "admin-feed-" + f.ID }>
<td class="px-4 py-3">
<a href={ "/podcasts/" + f.ID } class="font-medium hover:underline" style="color: var(--text-primary)">{ f.Title }</a>
if f.Host != "" {
<div class="text-xs" style="color: var(--text-secondary)">{ f.Host }</div>
}
</td>
<td class="px-4 py-3" style="color: var(--text-secondary)">{ f.LibraryName }</td>
<td class="px-4 py-3">
if f.Subscribers > 0 {
<span title={ f.SubscriberNames } style="color: var(--text-primary)">{ podcastInt(f.Subscribers) }</span>
} else {
<span class="badge" style="background-color: var(--surface-secondary); color: var(--text-secondary)">orphaned</span>
}
</td>
<td class="px-4 py-3 text-right tabular-nums" style="color: var(--text-primary)">
{ podcastInt(f.EpisodeDownloaded) }/{ podcastInt(f.EpisodeTotal) }
</td>
<td class="px-4 py-3 text-right tabular-nums" style="color: var(--text-primary)">{ FormatEpisodeSize(f.DiskBytes) }</td>
<td class="px-4 py-3 text-xs" style="color: var(--text-secondary)">{ f.LastFetched }</td>
<td class="px-4 py-3">
<div class="flex justify-end gap-2">
<button
class="btn btn-secondary text-xs"
hx-post={ "/api/podcasts/feeds/" + f.ID + "/refresh" }
hx-swap="none"
title="Check the feed for new episodes now"
>
Refresh
</button>
if f.EpisodeDownloaded > 0 {
<button
class="btn btn-secondary text-xs"
hx-post={ "/api/podcasts/feeds/" + f.ID + "/delete-files" }
hx-confirm={ "Delete the " + podcastInt(f.EpisodeDownloaded) + " downloaded file(s) of \"" + f.Title + "\"? Episodes reset to pending; a feed refresh will not re-materialize them." }
hx-swap="none"
style="color: var(--status-warning, #d97706);"
>
Delete files
</button>
}
<button
class="btn btn-secondary text-xs"
hx-delete={ "/api/podcasts/feeds/" + f.ID }
hx-confirm={ "ADMIN: remove \"" + f.Title + "\" for everyone? Episode rows AND downloaded files are deleted." }
hx-swap="none"
style="color: var(--status-danger);"
>
Remove
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</main>
</body>
</html>
}