All checks were successful
Release / Scraper / Test (push) Successful in 10s
Release / UI / Build (push) Successful in 26s
Release / v2 / Build ui-v2 (push) Successful in 17s
Release / Scraper / Docker (push) Successful in 47s
Release / UI / Docker (push) Successful in 56s
CI / Scraper / Lint (pull_request) Successful in 7s
CI / Scraper / Test (pull_request) Successful in 8s
CI / UI / Build (pull_request) Successful in 16s
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Docker Push (pull_request) Has been skipped
Release / v2 / Docker / ui-v2 (push) Successful in 56s
Release / v2 / Test backend (push) Successful in 4m35s
iOS CI / Build (pull_request) Successful in 4m28s
Release / v2 / Docker / backend (push) Successful in 1m29s
Release / v2 / Docker / runner (push) Successful in 1m39s
iOS CI / Test (pull_request) Successful in 9m51s
- backend/: Go API server and runner binaries with PocketBase + MinIO storage - ui-v2/: SvelteKit frontend rewrite - docker-compose-new.yml: compose file for the v2 stack - .gitea/workflows/release-v2.yaml: CI/CD for backend, runner, and ui-v2 Docker Hub images - scripts/pb-init.sh: migrate from wget to curl, add superuser bootstrap for fresh installs - .env.example: document DOCKER_BUILDKIT=1 for Colima users
94 lines
3.2 KiB
Svelte
94 lines
3.2 KiB
Svelte
<script lang="ts">
|
|
import { untrack } from 'svelte';
|
|
import type { PageData } from './$types';
|
|
|
|
let { data }: { data: PageData } = $props();
|
|
|
|
let entries = $state(untrack(() => data.entries));
|
|
|
|
// ── Parse cache_key ─────────────────────────────────────────────────────────
|
|
// cache_key format: "slug/chapter/voice"
|
|
function parseKey(key: string) {
|
|
const parts = key.split('/');
|
|
if (parts.length >= 3) {
|
|
return { slug: parts[0], chapter: parts[1], voice: parts.slice(2).join('/') };
|
|
}
|
|
return { slug: key, chapter: '—', voice: '—' };
|
|
}
|
|
|
|
function fmtDate(s: string) {
|
|
if (!s) return '—';
|
|
return new Date(s).toLocaleString(undefined, {
|
|
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
|
|
});
|
|
}
|
|
|
|
// ── Search ──────────────────────────────────────────────────────────────────
|
|
let q = $state('');
|
|
let filtered = $derived(
|
|
q.trim()
|
|
? entries.filter((e) => e.cache_key.toLowerCase().includes(q.toLowerCase().trim()))
|
|
: entries
|
|
);
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>Audio cache — libnovel admin</title>
|
|
</svelte:head>
|
|
|
|
<div class="space-y-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-zinc-100">Audio cache</h1>
|
|
<p class="text-zinc-400 text-sm mt-1">{entries.length} cached audio file{entries.length !== 1 ? 's' : ''}</p>
|
|
</div>
|
|
|
|
<!-- Search -->
|
|
<input
|
|
type="search"
|
|
bind:value={q}
|
|
placeholder="Filter by slug, chapter or voice…"
|
|
class="w-full max-w-sm bg-zinc-800 border border-zinc-700 rounded-lg px-3 py-2 text-zinc-100 text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-amber-400"
|
|
/>
|
|
|
|
{#if filtered.length === 0}
|
|
<p class="text-zinc-500 text-sm py-8 text-center">
|
|
{q.trim() ? 'No results.' : 'Audio cache is empty.'}
|
|
</p>
|
|
{:else}
|
|
<div class="overflow-x-auto rounded-xl border border-zinc-700">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-zinc-800 text-zinc-400 text-xs uppercase tracking-wide">
|
|
<tr>
|
|
<th class="px-4 py-3 text-left">Book</th>
|
|
<th class="px-4 py-3 text-left">Chapter</th>
|
|
<th class="px-4 py-3 text-left">Voice</th>
|
|
<th class="px-4 py-3 text-left">Filename</th>
|
|
<th class="px-4 py-3 text-left">Updated</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-zinc-700/50">
|
|
{#each filtered as entry}
|
|
{@const parts = parseKey(entry.cache_key)}
|
|
<tr class="bg-zinc-900 hover:bg-zinc-800/50 transition-colors">
|
|
<td class="px-4 py-3 text-zinc-200 font-medium">
|
|
<a
|
|
href="/books/{parts.slug}"
|
|
class="hover:text-amber-400 transition-colors"
|
|
>
|
|
{parts.slug}
|
|
</a>
|
|
</td>
|
|
<td class="px-4 py-3 text-zinc-400">{parts.chapter}</td>
|
|
<td class="px-4 py-3 text-zinc-400 font-mono text-xs">{parts.voice}</td>
|
|
<td class="px-4 py-3 text-zinc-500 font-mono text-xs truncate max-w-[14rem]" title={entry.filename}>
|
|
{entry.filename}
|
|
</td>
|
|
<td class="px-4 py-3 text-zinc-400">{fmtDate(entry.updated)}</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{/if}
|
|
</div>
|