Compare commits
2
Commits
6dc1505ae7
...
3fa5895bdc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fa5895bdc | ||
|
|
b44bf9bbce |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
+282
-165
@@ -1,191 +1,293 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Silksoup — Novel to EPUB</title>
|
||||
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
||||
<link rel="icon" type="image/png" href="/static/icon.png">
|
||||
<link rel="apple-touch-icon" href="/static/icon.png">
|
||||
<script>
|
||||
(function() {
|
||||
const saved = localStorage.getItem('silksoup_theme');
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (saved === 'dark' || (!saved && prefersDark)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = { darkMode: 'class', theme: { extend: { fontFamily: { sans: ['Inter','system-ui','sans-serif'] } } } }
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
theme: {
|
||||
bg: 'var(--bg)',
|
||||
surface: 'var(--surface)',
|
||||
subtle: 'var(--surface-subtle)',
|
||||
border: 'var(--border)',
|
||||
'border-strong': 'var(--border-strong)',
|
||||
primary: 'var(--primary)',
|
||||
'primary-hover': 'var(--primary-hover)',
|
||||
'primary-fg': 'var(--primary-fg)',
|
||||
secondary: 'var(--secondary)',
|
||||
text: 'var(--text)',
|
||||
muted: 'var(--text-muted)',
|
||||
danger: 'var(--danger)',
|
||||
'danger-bg': 'var(--danger-bg)',
|
||||
'danger-border': 'var(--danger-border)',
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-thumb{background:#27272a;border-radius:999px}::-webkit-scrollbar-track{background:transparent}
|
||||
@keyframes pulseGlow { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
|
||||
.animate-glow { animation: pulseGlow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
||||
:root {
|
||||
--bg: #fafafa;
|
||||
--surface: #ffffff;
|
||||
--surface-subtle: #f4f4f5;
|
||||
--border: #e4e4e7;
|
||||
--border-strong: #d4d4d8;
|
||||
--primary: #18181b;
|
||||
--primary-hover: #27272a;
|
||||
--primary-fg: #ffffff;
|
||||
--secondary: #71717a;
|
||||
--text: #09090b;
|
||||
--text-muted: #71717a;
|
||||
--danger: #dc2626;
|
||||
--danger-bg: #fef2f2;
|
||||
--danger-border: #fecaca;
|
||||
--log-bg: #09090b;
|
||||
--log-border: #27272a;
|
||||
--log-text: #f4f4f5;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--bg: #09090b;
|
||||
--surface: #121215;
|
||||
--surface-subtle: #1c1c21;
|
||||
--border: #27272a;
|
||||
--border-strong: #3f3f46;
|
||||
--primary: #fafafa;
|
||||
--primary-hover: #f4f4f5;
|
||||
--primary-fg: #09090b;
|
||||
--secondary: #a1a1aa;
|
||||
--text: #fafafa;
|
||||
--text-muted: #a1a1aa;
|
||||
--danger: #f87171;
|
||||
--danger-bg: #2b1114;
|
||||
--danger-border: #4c1d24;
|
||||
--log-bg: #000000;
|
||||
--log-border: #27272a;
|
||||
--log-text: #f4f4f5;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 4px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-[#09090b] text-zinc-100 min-h-screen flex flex-col antialiased selection:bg-zinc-800">
|
||||
<header class="border-b border-zinc-800/80 bg-zinc-950/80 backdrop-blur sticky top-0 z-30">
|
||||
<body class="bg-theme-bg text-theme-text min-h-screen flex flex-col font-sans antialiased selection:bg-zinc-800 selection:text-white dark:selection:bg-zinc-200 dark:selection:text-zinc-900">
|
||||
<header class="border-b border-theme-border bg-theme-surface sticky top-0 z-30">
|
||||
<div class="max-w-4xl mx-auto px-4 py-3.5 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-lg bg-white text-black font-black grid place-items-center text-sm shadow-sm">S</div>
|
||||
<img src="/static/icon.png" alt="Silksoup" class="w-8 h-8 rounded-md object-contain shadow-sm">
|
||||
<div>
|
||||
<h1 class="font-semibold tracking-tight text-sm leading-none flex items-center gap-2">
|
||||
Silksoup
|
||||
<span id="headerBadge" class="hidden text-[10px] font-medium px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">Active</span>
|
||||
</h1>
|
||||
<p class="text-[11px] text-zinc-400 mt-0.5">Novel to EPUB pipeline</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold text-base leading-none text-theme-text">Silksoup</span>
|
||||
<span id="headerBadge" class="hidden text-xs font-medium px-2 py-0.5 rounded bg-emerald-50 dark:bg-emerald-950/60 text-emerald-700 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-800/80">Active</span>
|
||||
</div>
|
||||
<p class="text-sm text-theme-muted mt-0.5">Novel to EPUB pipeline</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button id="notifyBtn" onclick="toggleNotifications()" title="Enable Desktop Notifications" class="text-xs text-zinc-400 hover:text-zinc-200 px-2.5 py-1.5 rounded-lg border border-zinc-800 hover:border-zinc-700 transition flex items-center gap-1.5">
|
||||
<span id="notifyIcon">🔔</span>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<button id="themeToggleBtn" onclick="toggleTheme()" type="button" aria-label="Toggle theme" class="text-sm text-theme-muted hover:text-theme-text px-3 py-1.5 rounded-md border border-theme-border hover:bg-theme-subtle hover:border-theme-border-strong transition flex items-center gap-1.5">
|
||||
<span id="themeIcon" class="w-4 h-4 flex items-center justify-center"></span>
|
||||
<span id="themeText" class="hidden sm:inline">Theme</span>
|
||||
</button>
|
||||
<button id="notifyBtn" onclick="toggleNotifications()" type="button" aria-label="Desktop notifications" class="text-sm text-theme-muted hover:text-theme-text px-3 py-1.5 rounded-md border border-theme-border hover:bg-theme-subtle hover:border-theme-border-strong transition flex items-center gap-1.5">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/>
|
||||
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>
|
||||
</svg>
|
||||
<span id="notifyText" class="hidden sm:inline">Notify</span>
|
||||
</button>
|
||||
<a href="/api/epubs" target="_blank" class="text-xs text-zinc-400 hover:text-zinc-200 px-2.5 py-1.5 rounded-lg border border-zinc-800 hover:border-zinc-700 transition">API</a>
|
||||
<a href="/api/epubs" target="_blank" class="text-sm text-theme-muted hover:text-theme-text px-3 py-1.5 rounded-md border border-theme-border hover:bg-theme-subtle hover:border-theme-border-strong transition flex items-center gap-1.5">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="16 18 22 12 16 6"/>
|
||||
<polyline points="8 6 2 12 8 18"/>
|
||||
</svg>
|
||||
<span class="hidden sm:inline">API</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="max-w-4xl mx-auto w-full px-4 py-6 flex flex-col gap-6 flex-1">
|
||||
<!-- New Job Card -->
|
||||
<section class="rounded-2xl border border-zinc-800/80 bg-zinc-900/40 p-5 shadow-sm">
|
||||
<section class="rounded-lg border border-theme-border bg-theme-surface p-5 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="text-xs font-semibold uppercase tracking-wider text-zinc-400">Scrape New Novel</h2>
|
||||
<button onclick="pasteClipboard()" class="text-[11px] text-zinc-400 hover:text-white flex items-center gap-1 transition">
|
||||
<span>📋</span> Paste URL
|
||||
</button>
|
||||
<h2 class="text-base font-semibold text-theme-text">Scrape Novel</h2>
|
||||
</div>
|
||||
<form id="jobForm" class="space-y-4" onsubmit="return false">
|
||||
<div class="relative">
|
||||
<input id="urlInput" type="url" required placeholder="https://kakuyomu.jp/works/... or novel index URL" class="w-full rounded-xl bg-zinc-950 border border-zinc-800 px-4 py-3.5 text-sm placeholder:text-zinc-600 focus:outline-none focus:border-zinc-500 transition shadow-inner">
|
||||
<div>
|
||||
<label for="urlInput" class="block text-sm font-medium text-theme-text mb-1.5">Novel URL</label>
|
||||
<div class="flex gap-2">
|
||||
<input id="urlInput" type="url" required placeholder="https://kakuyomu.jp/works/... or novel index URL" class="flex-1 rounded-md bg-theme-subtle border border-theme-border hover:border-theme-border-strong focus:border-theme-primary px-3.5 py-2.5 text-base text-theme-text placeholder:text-theme-muted/60 focus:outline-none focus:ring-1 focus:ring-theme-primary transition">
|
||||
<button type="button" onclick="pasteClipboard()" class="px-3.5 py-2.5 text-sm font-medium rounded-md border border-theme-border text-theme-muted hover:text-theme-text hover:bg-theme-subtle hover:border-theme-border-strong transition shrink-0 flex items-center gap-1.5">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="8" height="4" x="8" y="2" rx="1" ry="1"/>
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
|
||||
</svg>
|
||||
<span>Paste</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2 pt-1">
|
||||
<label class="text-[11px] uppercase tracking-wider text-zinc-400 font-semibold">Cache Mode</label>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
<label class="flex items-start gap-2.5 p-2.5 rounded-xl border border-zinc-800 bg-zinc-950/60 hover:border-zinc-700 cursor-pointer select-none transition group">
|
||||
<input type="radio" name="refreshMode" value="none" checked class="mt-0.5 text-white bg-zinc-900 border-zinc-700 focus:ring-0">
|
||||
<div>
|
||||
<span class="block text-sm font-medium text-theme-text mb-1.5">Cache Mode</span>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
|
||||
<label class="flex items-start gap-3 p-3.5 rounded-md border border-theme-border bg-theme-subtle/50 hover:border-theme-border-strong cursor-pointer select-none transition">
|
||||
<input type="radio" name="refreshMode" value="none" checked class="mt-0.5 text-theme-primary border-theme-border focus:ring-0">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xs font-medium text-zinc-200 group-hover:text-white">Full Cache</span>
|
||||
<span class="text-[10px] text-zinc-500">Reuse HTML & AI results</span>
|
||||
<span class="text-sm font-medium text-theme-text">Full Cache</span>
|
||||
<span class="text-xs text-theme-muted mt-0.5">Reuse HTML and parsed results</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex items-start gap-2.5 p-2.5 rounded-xl border border-zinc-800 bg-zinc-950/60 hover:border-zinc-700 cursor-pointer select-none transition group">
|
||||
<input type="radio" name="refreshMode" value="llm" class="mt-0.5 text-white bg-zinc-900 border-zinc-700 focus:ring-0">
|
||||
<label class="flex items-start gap-3 p-3.5 rounded-md border border-theme-border bg-theme-subtle/50 hover:border-theme-border-strong cursor-pointer select-none transition">
|
||||
<input type="radio" name="refreshMode" value="llm" class="mt-0.5 text-theme-primary border-theme-border focus:ring-0">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xs font-medium text-zinc-200 group-hover:text-white">Re-run AI</span>
|
||||
<span class="text-[10px] text-zinc-500">Reuse HTML, re-sanitize chapters</span>
|
||||
<span class="text-sm font-medium text-theme-text">Re-run AI</span>
|
||||
<span class="text-xs text-theme-muted mt-0.5">Reuse HTML, re-sanitize text</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex items-start gap-2.5 p-2.5 rounded-xl border border-zinc-800 bg-zinc-950/60 hover:border-zinc-700 cursor-pointer select-none transition group">
|
||||
<input type="radio" name="refreshMode" value="all" class="mt-0.5 text-white bg-zinc-900 border-zinc-700 focus:ring-0">
|
||||
<label class="flex items-start gap-3 p-3.5 rounded-md border border-theme-border bg-theme-subtle/50 hover:border-theme-border-strong cursor-pointer select-none transition">
|
||||
<input type="radio" name="refreshMode" value="all" class="mt-0.5 text-theme-primary border-theme-border focus:ring-0">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xs font-medium text-zinc-200 group-hover:text-white">Full Refresh</span>
|
||||
<span class="text-[10px] text-zinc-500">Bypass cache & fetch fresh from web</span>
|
||||
<span class="text-sm font-medium text-theme-text">Full Refresh</span>
|
||||
<span class="text-xs text-theme-muted mt-0.5">Bypass cache and fetch from web</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-1">
|
||||
<button id="startBtn" class="sm:w-auto w-full rounded-xl bg-white text-black text-sm font-semibold px-7 py-3 hover:bg-zinc-200 active:scale-[0.99] transition shrink-0 flex items-center justify-center gap-2">
|
||||
<span id="btnSpinner" class="hidden inline-block w-4 h-4 border-2 border-black border-t-transparent rounded-full animate-spin"></span>
|
||||
<span id="btnText">Start Scraping → EPUB</span>
|
||||
<button id="startBtn" type="button" class="w-full sm:w-auto rounded-md bg-theme-primary hover:bg-theme-primary-hover text-theme-primary-fg text-sm font-medium px-5 py-2.5 transition flex items-center justify-center gap-2 shadow-sm">
|
||||
<svg id="btnSpinner" class="hidden w-4 h-4 animate-spin text-current" viewBox="0 0 24 24" fill="none">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
|
||||
</svg>
|
||||
<span id="btnText">Start Scraping</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Active / In-Progress Job Section -->
|
||||
<section id="activeJob" class="hidden rounded-2xl border border-zinc-800 bg-zinc-900/60 p-5 shadow-sm transition-all">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<section id="activeJob" class="hidden rounded-lg border border-theme-border bg-theme-surface p-5 shadow-sm space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span id="statusPulse" class="w-2.5 h-2.5 rounded-full bg-emerald-400 animate-glow"></span>
|
||||
<h2 class="text-xs font-semibold uppercase tracking-wider text-zinc-400">Task Monitor</h2>
|
||||
<span id="statusPulse" class="w-2.5 h-2.5 rounded-full bg-emerald-500"></span>
|
||||
<h2 class="text-base font-semibold text-theme-text">Task Monitor</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="cancelJobBtn" onclick="requestCancelJob()" class="text-[11px] px-2.5 py-0.5 rounded-full border border-rose-900/60 bg-rose-950/40 text-rose-300 hover:bg-rose-900/60 transition">Stop Process</button>
|
||||
<span id="jobStatusBadge" class="text-[11px] font-mono uppercase px-2.5 py-0.5 rounded-full bg-zinc-800 text-zinc-300 border border-zinc-700">idle</span>
|
||||
<button id="cancelJobBtn" type="button" onclick="requestCancelJob()" class="text-sm px-3 py-1 rounded-md border border-theme-danger-border bg-theme-danger-bg text-theme-danger hover:opacity-90 transition font-medium">Stop Process</button>
|
||||
<span id="jobStatusBadge" class="text-xs font-mono uppercase px-2.5 py-0.5 rounded border border-theme-border bg-theme-subtle text-theme-text font-medium">idle</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3.5">
|
||||
<div class="flex items-center justify-between text-xs gap-3">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between text-sm gap-3">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span id="jobStage" class="px-2 py-0.5 rounded bg-zinc-800/80 text-zinc-300 font-mono text-[10px] uppercase tracking-wider">init</span>
|
||||
<span id="jobMessage" class="text-zinc-300 truncate font-medium">Starting...</span>
|
||||
<span id="jobStage" class="px-2 py-0.5 rounded bg-theme-subtle border border-theme-border text-theme-text font-mono text-xs uppercase font-medium">init</span>
|
||||
<span id="jobMessage" class="text-theme-text truncate font-medium">Starting...</span>
|
||||
</div>
|
||||
<span id="jobCounter" class="text-zinc-400 tabular-nums font-mono font-medium shrink-0">0 / 0</span>
|
||||
<span id="jobCounter" class="text-theme-muted font-mono tabular-nums shrink-0 font-medium">0 / 0</span>
|
||||
</div>
|
||||
|
||||
<div class="h-2 rounded-full bg-zinc-950 border border-zinc-800 overflow-hidden relative">
|
||||
<div id="jobProgress" class="h-full bg-white transition-all duration-300" style="width:0%"></div>
|
||||
<div class="h-2 rounded bg-theme-subtle border border-theme-border overflow-hidden">
|
||||
<div id="jobProgress" class="h-full bg-theme-primary transition-all duration-200" style="width: 0%"></div>
|
||||
</div>
|
||||
|
||||
<!-- Log Toolbar -->
|
||||
<div class="flex items-center justify-between text-[11px] text-zinc-500 px-0.5 pt-1">
|
||||
<div class="flex items-center justify-between text-sm text-theme-muted pt-1">
|
||||
<span>Execution Log</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="flex items-center gap-1.5 cursor-pointer select-none hover:text-zinc-300">
|
||||
<input id="autoScrollCheck" type="checkbox" checked class="rounded border-zinc-700 bg-zinc-950 text-white focus:ring-0">
|
||||
Auto-scroll
|
||||
<label class="flex items-center gap-1.5 cursor-pointer select-none">
|
||||
<input id="autoScrollCheck" type="checkbox" checked class="rounded border-theme-border bg-theme-subtle text-theme-primary focus:ring-0">
|
||||
<span>Auto-scroll</span>
|
||||
</label>
|
||||
<button onclick="clearLogs()" class="hover:text-zinc-300">Clear</button>
|
||||
<button onclick="toggleLogHeight()" id="expandLogBtn" class="hover:text-zinc-300">Expand</button>
|
||||
<button type="button" onclick="clearLogs()" class="hover:text-theme-text transition">Clear</button>
|
||||
<button type="button" onclick="toggleLogHeight()" id="expandLogBtn" class="hover:text-theme-text transition">Expand</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="jobLog" class="h-44 overflow-auto rounded-xl bg-zinc-950 border border-zinc-800/90 p-3.5 font-mono text-[11px] leading-relaxed text-zinc-400 whitespace-pre-wrap transition-all"></div>
|
||||
<div id="jobLog" class="h-44 overflow-auto rounded-md bg-[var(--log-bg)] border border-[var(--log-border)] p-3.5 font-mono text-sm text-[var(--log-text)] whitespace-pre-wrap leading-relaxed"></div>
|
||||
|
||||
<div id="jobResult" class="hidden pt-1 flex items-center gap-3">
|
||||
<a id="downloadLink" href="#" class="inline-flex items-center gap-2 rounded-xl bg-white text-black text-xs font-semibold px-5 py-2.5 hover:bg-zinc-200 transition shadow-sm">
|
||||
<span>📥</span> Download EPUB
|
||||
<div id="jobResult" class="hidden pt-2 flex items-center gap-2">
|
||||
<a id="downloadLink" href="#" class="inline-flex items-center gap-2 rounded-md bg-theme-primary hover:bg-theme-primary-hover text-theme-primary-fg text-sm font-medium px-4 py-2.5 transition shadow-sm">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
<span>Download EPUB</span>
|
||||
</a>
|
||||
<button onclick="dismissActiveJob()" class="text-xs text-zinc-400 hover:text-zinc-200 px-3 py-2 rounded-xl border border-zinc-800 hover:bg-zinc-800/50 transition">Dismiss</button>
|
||||
<button type="button" onclick="dismissActiveJob()" class="text-sm text-theme-muted hover:text-theme-text px-3.5 py-2.5 rounded-md border border-theme-border hover:bg-theme-subtle transition">Dismiss</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Library Section -->
|
||||
<section class="rounded-2xl border border-zinc-800/80 bg-zinc-900/40 p-5 shadow-sm">
|
||||
<section class="rounded-lg border border-theme-border bg-theme-surface p-5 shadow-sm">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="text-xs font-semibold uppercase tracking-wider text-zinc-400">Library</h2>
|
||||
<span id="epubCount" class="text-[11px] text-zinc-500 font-mono"></span>
|
||||
<h2 class="text-base font-semibold text-theme-text">Library</h2>
|
||||
<span id="epubCount" class="text-sm text-theme-muted font-mono font-medium"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative flex-1 sm:w-60">
|
||||
<input id="searchEpub" type="text" oninput="filterEpubs()" placeholder="Filter books..." class="w-full rounded-xl bg-zinc-950 border border-zinc-800 px-3 py-1.5 text-xs placeholder:text-zinc-600 focus:outline-none focus:border-zinc-600">
|
||||
<div class="relative flex-1 sm:w-64">
|
||||
<input id="searchEpub" type="text" oninput="filterEpubs()" placeholder="Filter books..." class="w-full rounded-md bg-theme-subtle border border-theme-border hover:border-theme-border-strong focus:border-theme-primary px-3.5 py-2 text-sm text-theme-text placeholder:text-theme-muted/60 focus:outline-none focus:ring-1 focus:ring-theme-primary transition">
|
||||
</div>
|
||||
<button onclick="loadEpubs()" class="text-xs px-3 py-1.5 rounded-xl border border-zinc-800 hover:bg-zinc-800 hover:text-white text-zinc-400 transition shrink-0">Refresh</button>
|
||||
<button type="button" onclick="loadEpubs()" class="text-sm px-3.5 py-2 rounded-md border border-theme-border text-theme-muted hover:text-theme-text hover:bg-theme-subtle transition shrink-0">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="epubList" class="space-y-2.5"></div>
|
||||
<div id="epubEmpty" class="text-xs text-zinc-500 py-8 text-center hidden">
|
||||
<div class="text-2xl mb-1">📚</div>
|
||||
<div id="epubEmpty" class="text-sm text-theme-muted py-8 text-center hidden">
|
||||
No EPUBs found. Scrape a novel to populate your library.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- How it works Footer -->
|
||||
<section class="rounded-2xl border border-zinc-800/60 bg-zinc-900/20 p-4">
|
||||
<h3 class="text-xs font-semibold text-zinc-400 mb-2">Workflow Pipeline</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-2 text-[11px] text-zinc-500 leading-normal">
|
||||
<div class="p-2.5 rounded-xl bg-zinc-950/40 border border-zinc-800/50">
|
||||
<span class="font-semibold text-zinc-400 block mb-0.5">1. Ingest & Cache</span>
|
||||
Fetches novel HTML via anti-bot curl_cffi into Postgres/SQLite.
|
||||
<section class="rounded-lg border border-theme-border bg-theme-surface p-5">
|
||||
<h3 class="text-sm font-semibold text-theme-text mb-3">Workflow Pipeline</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-2.5 text-xs text-theme-muted">
|
||||
<div class="p-3 rounded-md bg-theme-subtle/70 border border-theme-border/70">
|
||||
<span class="text-sm font-medium text-theme-text block mb-1">1. Ingest & Cache</span>
|
||||
Fetches novel HTML via anti-bot client into database.
|
||||
</div>
|
||||
<div class="p-2.5 rounded-xl bg-zinc-950/40 border border-zinc-800/50">
|
||||
<span class="font-semibold text-zinc-400 block mb-0.5">2. Parse Index</span>
|
||||
DeepSeek LLM extracts chronological volumes and chapter URLs.
|
||||
<div class="p-3 rounded-md bg-theme-subtle/70 border border-theme-border/70">
|
||||
<span class="text-sm font-medium text-theme-text block mb-1">2. Parse Index</span>
|
||||
LLM extracts chronological volumes and chapter URLs.
|
||||
</div>
|
||||
<div class="p-2.5 rounded-xl bg-zinc-950/40 border border-zinc-800/50">
|
||||
<span class="font-semibold text-zinc-400 block mb-0.5">3. Sanitize Content</span>
|
||||
Cleans site bloat, keeps translator notes, wraps semantic HTML.
|
||||
<div class="p-3 rounded-md bg-theme-subtle/70 border border-theme-border/70">
|
||||
<span class="text-sm font-medium text-theme-text block mb-1">3. Sanitize Content</span>
|
||||
Cleans site bloat and structures semantic chapter content.
|
||||
</div>
|
||||
<div class="p-2.5 rounded-xl bg-zinc-950/40 border border-zinc-800/50">
|
||||
<span class="font-semibold text-zinc-400 block mb-0.5">4. Build EPUB</span>
|
||||
Downloads images with referers, compresses WEBP/JPG, compiles book.
|
||||
<div class="p-3 rounded-md bg-theme-subtle/70 border border-theme-border/70">
|
||||
<span class="text-sm font-medium text-theme-text block mb-1">4. Build EPUB</span>
|
||||
Downloads images, compresses assets, and compiles book.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- In-Page Toast Container -->
|
||||
<div id="toastContainer" class="fixed bottom-4 right-4 z-50 flex flex-col gap-2 pointer-events-none"></div>
|
||||
|
||||
<footer class="border-t border-zinc-800/80 py-4 text-center text-[11px] text-zinc-600">
|
||||
Silksoup — Self-hosted EPUB Generator • Fast, headless, and cached.
|
||||
<footer class="border-t border-theme-border py-4 text-center text-sm text-theme-muted">
|
||||
Silksoup — Novel to EPUB Pipeline
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
@@ -194,56 +296,68 @@ let currentJobId = localStorage.getItem('silksoup_active_job') || null;
|
||||
let evtSource = null;
|
||||
let allEpubs = [];
|
||||
let originalTitle = document.title;
|
||||
let notificationsAllowed = Notification.permission === 'granted';
|
||||
|
||||
updateNotifyButton();
|
||||
const sunSvg = `<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg>`;
|
||||
const moonSvg = `<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>`;
|
||||
|
||||
function updateNotifyButton(){
|
||||
function updateThemeUI() {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
$('#themeIcon').innerHTML = isDark ? sunSvg : moonSvg;
|
||||
$('#themeText').textContent = isDark ? 'Light' : 'Dark';
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const isDark = document.documentElement.classList.toggle('dark');
|
||||
localStorage.setItem('silksoup_theme', isDark ? 'dark' : 'light');
|
||||
updateThemeUI();
|
||||
}
|
||||
|
||||
function updateNotifyButton() {
|
||||
if (Notification.permission === 'granted') {
|
||||
$('#notifyBtn').classList.add('text-emerald-400', 'border-emerald-500/30');
|
||||
$('#notifyBtn').classList.add('text-emerald-700', 'dark:text-emerald-400', 'border-emerald-300', 'dark:border-emerald-800');
|
||||
$('#notifyText').textContent = 'Notifications on';
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleNotifications(){
|
||||
async function toggleNotifications() {
|
||||
if (Notification.permission === 'default') {
|
||||
const res = await Notification.requestPermission();
|
||||
if (res === 'granted') {
|
||||
showToast('Notifications enabled!', 'success');
|
||||
showToast('Notifications enabled', 'success');
|
||||
updateNotifyButton();
|
||||
}
|
||||
} else if (Notification.permission === 'denied') {
|
||||
showToast('Notifications are blocked in your browser settings.', 'error');
|
||||
showToast('Notifications are blocked in browser settings', 'error');
|
||||
} else {
|
||||
showToast('Notifications are already enabled.', 'info');
|
||||
showToast('Notifications are already enabled', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
function notify(title, body){
|
||||
function notify(title, body) {
|
||||
if (Notification.permission === 'granted' && document.hidden) {
|
||||
new Notification(title, { body, icon: '/static/favicon.ico' });
|
||||
new Notification(title, { body, icon: '/static/icon.png' });
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info'){
|
||||
function showToast(message, type = 'info') {
|
||||
const container = $('#toastContainer');
|
||||
const toast = document.createElement('div');
|
||||
const bg = type === 'error' ? 'bg-rose-950/90 border-rose-800 text-rose-200' :
|
||||
type === 'success' ? 'bg-emerald-950/90 border-emerald-800 text-emerald-200' :
|
||||
'bg-zinc-900/90 border-zinc-800 text-zinc-200';
|
||||
toast.className = `pointer-events-auto rounded-xl border px-4 py-3 text-xs shadow-lg backdrop-blur flex items-center gap-2 transition-all transform translate-y-2 opacity-0 duration-200 ${bg}`;
|
||||
toast.innerHTML = `<span>${type === 'error' ? '⚠️' : type === 'success' ? '✓' : 'ℹ️'}</span><span class="flex-1">${message}</span>`;
|
||||
const borderClass = type === 'error' ? 'border-theme-danger-border bg-theme-danger-bg text-theme-danger' :
|
||||
type === 'success' ? 'border-emerald-300 dark:border-emerald-800 bg-emerald-50 dark:bg-emerald-950 text-emerald-900 dark:text-emerald-200' :
|
||||
'border-theme-border bg-theme-surface text-theme-text';
|
||||
toast.className = `pointer-events-auto rounded-md border px-3.5 py-2.5 text-sm shadow-md flex items-center gap-2.5 transition-opacity duration-150 opacity-0 ${borderClass}`;
|
||||
toast.textContent = message;
|
||||
container.appendChild(toast);
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.remove('translate-y-2', 'opacity-0');
|
||||
toast.classList.remove('opacity-0');
|
||||
});
|
||||
setTimeout(() => {
|
||||
toast.classList.add('opacity-0', 'translate-y-2');
|
||||
setTimeout(() => toast.remove(), 250);
|
||||
toast.classList.add('opacity-0');
|
||||
setTimeout(() => toast.remove(), 150);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
async function pasteClipboard(){
|
||||
async function pasteClipboard() {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text) {
|
||||
@@ -255,7 +369,7 @@ async function pasteClipboard(){
|
||||
}
|
||||
}
|
||||
|
||||
function setBtnLoading(loading){
|
||||
function setBtnLoading(loading) {
|
||||
const btn = $('#startBtn');
|
||||
const spinner = $('#btnSpinner');
|
||||
const text = $('#btnText');
|
||||
@@ -266,7 +380,7 @@ function setBtnLoading(loading){
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
spinner.classList.add('hidden');
|
||||
text.textContent = 'Start Scraping → EPUB';
|
||||
text.textContent = 'Start Scraping';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +396,7 @@ $('#startBtn').addEventListener('click', async () => {
|
||||
const refresh_mode = selectedMode ? selectedMode.value : 'none';
|
||||
const res = await fetch('/api/jobs', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json'},
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ url, refresh_mode })
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -302,7 +416,7 @@ $('#startBtn').addEventListener('click', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
function showActive(){
|
||||
function showActive() {
|
||||
$('#activeJob').classList.remove('hidden');
|
||||
$('#headerBadge').classList.remove('hidden');
|
||||
$('#cancelJobBtn').classList.remove('hidden');
|
||||
@@ -312,11 +426,11 @@ function showActive(){
|
||||
$('#jobStage').textContent = 'queued';
|
||||
$('#jobStatusBadge').textContent = 'running';
|
||||
$('#jobResult').classList.add('hidden');
|
||||
$('#statusPulse').className = 'w-2.5 h-2.5 rounded-full bg-emerald-400 animate-glow';
|
||||
$('#statusPulse').className = 'w-2 h-2 rounded-full bg-emerald-500';
|
||||
$('#activeJob').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
|
||||
async function requestCancelJob(){
|
||||
async function requestCancelJob() {
|
||||
if (!currentJobId) return;
|
||||
const btn = $('#cancelJobBtn');
|
||||
btn.disabled = true;
|
||||
@@ -324,7 +438,7 @@ async function requestCancelJob(){
|
||||
try {
|
||||
const res = await fetch(`/api/jobs/${currentJobId}/cancel`, { method: 'POST' });
|
||||
if (res.ok) {
|
||||
showToast('Process cancellation requested. Cached chapters remain safe.', 'info');
|
||||
showToast('Process cancellation requested', 'info');
|
||||
}
|
||||
} catch(e) {
|
||||
showToast('Failed to cancel job', 'error');
|
||||
@@ -334,7 +448,7 @@ async function requestCancelJob(){
|
||||
}
|
||||
}
|
||||
|
||||
function dismissActiveJob(){
|
||||
function dismissActiveJob() {
|
||||
localStorage.removeItem('silksoup_active_job');
|
||||
currentJobId = null;
|
||||
if (evtSource) evtSource.close();
|
||||
@@ -343,7 +457,7 @@ function dismissActiveJob(){
|
||||
document.title = originalTitle;
|
||||
}
|
||||
|
||||
function connectSSE(jobId){
|
||||
function connectSSE(jobId) {
|
||||
if (evtSource) evtSource.close();
|
||||
evtSource = new EventSource(`/api/jobs/${jobId}/events`);
|
||||
|
||||
@@ -368,25 +482,25 @@ function connectSSE(jobId){
|
||||
|
||||
if (evt.stage === 'failed') {
|
||||
$('#jobStatusBadge').textContent = 'failed';
|
||||
$('#statusPulse').className = 'w-2.5 h-2.5 rounded-full bg-rose-500';
|
||||
$('#statusPulse').className = 'w-2 h-2 rounded-full bg-rose-500';
|
||||
$('#cancelJobBtn').classList.add('hidden');
|
||||
document.title = `❌ Failed — Silksoup`;
|
||||
document.title = `Failed — Silksoup`;
|
||||
showToast(`Job failed: ${evt.message}`, 'error');
|
||||
}
|
||||
if (evt.stage === 'cancelled') {
|
||||
$('#jobStatusBadge').textContent = 'cancelled';
|
||||
$('#statusPulse').className = 'w-2.5 h-2.5 rounded-full bg-amber-500';
|
||||
$('#statusPulse').className = 'w-2 h-2 rounded-full bg-amber-500';
|
||||
$('#cancelJobBtn').classList.add('hidden');
|
||||
document.title = `Silksoup`;
|
||||
showToast('Scraping stopped. Completed chapters are cached in database.', 'info');
|
||||
showToast('Scraping stopped. Completed chapters cached.', 'info');
|
||||
}
|
||||
if (evt.stage === 'completed') {
|
||||
$('#jobStatusBadge').textContent = 'completed';
|
||||
$('#statusPulse').className = 'w-2.5 h-2.5 rounded-full bg-emerald-400';
|
||||
$('#statusPulse').className = 'w-2 h-2 rounded-full bg-emerald-500';
|
||||
$('#cancelJobBtn').classList.add('hidden');
|
||||
document.title = `✓ Completed — Silksoup`;
|
||||
notify('Silksoup EPUB Ready!', evt.message || 'Your book has been compiled successfully.');
|
||||
showToast('EPUB compiled successfully!', 'success');
|
||||
document.title = `Completed — Silksoup`;
|
||||
notify('Silksoup EPUB Ready', evt.message || 'Book compiled successfully.');
|
||||
showToast('EPUB compiled successfully', 'success');
|
||||
}
|
||||
|
||||
if (evt.message && evt.message !== '__DONE__') {
|
||||
@@ -398,60 +512,58 @@ function connectSSE(jobId){
|
||||
}
|
||||
};
|
||||
|
||||
evtSource.onerror = () => {
|
||||
// Retry handled by browser EventSource
|
||||
};
|
||||
evtSource.onerror = () => {};
|
||||
}
|
||||
|
||||
async function refreshJob(){
|
||||
async function refreshJob() {
|
||||
if (!currentJobId) return;
|
||||
const res = await fetch(`/api/jobs/${currentJobId}`);
|
||||
if (!res.ok) return;
|
||||
const j = await res.json();
|
||||
$('#jobStatusBadge').textContent = j.status;
|
||||
if (j.status === 'completed') {
|
||||
$('#statusPulse').className = 'w-2.5 h-2.5 rounded-full bg-emerald-400';
|
||||
$('#statusPulse').className = 'w-2 h-2 rounded-full bg-emerald-500';
|
||||
$('#cancelJobBtn').classList.add('hidden');
|
||||
if (j.result_path) {
|
||||
const name = j.result_path.split(/[\\/]/).pop();
|
||||
$('#downloadLink').href = `/api/epubs/${encodeURIComponent(name)}`;
|
||||
$('#downloadLink').innerHTML = `<span>📥</span> Download ${name}`;
|
||||
$('#downloadLink').querySelector('span').textContent = `Download ${name}`;
|
||||
$('#jobResult').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
if (j.status === 'cancelled') {
|
||||
$('#statusPulse').className = 'w-2.5 h-2.5 rounded-full bg-amber-500';
|
||||
$('#statusPulse').className = 'w-2 h-2 rounded-full bg-amber-500';
|
||||
$('#cancelJobBtn').classList.add('hidden');
|
||||
}
|
||||
if (j.status === 'failed') {
|
||||
$('#statusPulse').className = 'w-2.5 h-2.5 rounded-full bg-rose-500';
|
||||
$('#statusPulse').className = 'w-2 h-2 rounded-full bg-rose-500';
|
||||
$('#cancelJobBtn').classList.add('hidden');
|
||||
$('#jobLog').textContent += `\n[ERROR] ${j.error || j.message}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
function clearLogs(){
|
||||
function clearLogs() {
|
||||
$('#jobLog').textContent = '';
|
||||
}
|
||||
|
||||
let isLogExpanded = false;
|
||||
function toggleLogHeight(){
|
||||
function toggleLogHeight() {
|
||||
const log = $('#jobLog');
|
||||
const btn = $('#expandLogBtn');
|
||||
if (isLogExpanded) {
|
||||
log.classList.remove('h-96');
|
||||
log.classList.add('h-44');
|
||||
log.classList.add('h-40');
|
||||
btn.textContent = 'Expand';
|
||||
isLogExpanded = false;
|
||||
} else {
|
||||
log.classList.remove('h-44');
|
||||
log.classList.remove('h-40');
|
||||
log.classList.add('h-96');
|
||||
btn.textContent = 'Collapse';
|
||||
isLogExpanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEpubs(){
|
||||
async function loadEpubs() {
|
||||
try {
|
||||
const res = await fetch('/api/epubs');
|
||||
const data = await res.json();
|
||||
@@ -463,7 +575,7 @@ async function loadEpubs(){
|
||||
}
|
||||
}
|
||||
|
||||
function filterEpubs(){
|
||||
function filterEpubs() {
|
||||
const query = $('#searchEpub').value.toLowerCase().trim();
|
||||
if (!query) {
|
||||
renderEpubs(allEpubs);
|
||||
@@ -473,7 +585,7 @@ function filterEpubs(){
|
||||
renderEpubs(filtered);
|
||||
}
|
||||
|
||||
function renderEpubs(list){
|
||||
function renderEpubs(list) {
|
||||
const container = $('#epubList');
|
||||
container.innerHTML = '';
|
||||
if (!list.length) {
|
||||
@@ -484,21 +596,26 @@ function renderEpubs(list){
|
||||
|
||||
for (const f of list) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'group flex items-center gap-3.5 rounded-xl border border-zinc-800/80 bg-zinc-950/80 px-4 py-3 hover:border-zinc-700 transition';
|
||||
row.className = 'flex items-center justify-between gap-3.5 rounded-md border border-theme-border bg-theme-subtle/50 px-4 py-3 hover:border-theme-border-strong transition';
|
||||
|
||||
// Clean filename for nice display
|
||||
const cleanTitle = f.name.replace(/\.epub$/i, '').replace(/[-_]/g, ' ');
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="w-8 h-8 rounded-lg bg-zinc-900 border border-zinc-800 flex items-center justify-center text-xs shrink-0 text-zinc-400 group-hover:text-white transition">📖</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="truncate text-zinc-200 text-xs font-medium capitalize" title="${cleanTitle}">${cleanTitle}</div>
|
||||
<div class="text-[11px] text-zinc-400 font-mono mt-0.5">${f.size_human} • ${new Date(f.mtime*1000).toLocaleDateString()} ${new Date(f.mtime*1000).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</div>
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
<svg class="w-4 h-4 text-theme-muted shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1-2.5-2.5Z"/>
|
||||
<path d="M6 6h10"/>
|
||||
<path d="M6 10h10"/>
|
||||
</svg>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm font-medium text-theme-text capitalize" title="${cleanTitle}">${cleanTitle}</div>
|
||||
<div class="text-xs text-theme-muted font-mono mt-0.5">${f.size_human} • ${new Date(f.mtime * 1000).toLocaleDateString()} ${new Date(f.mtime * 1000).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'})}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<a href="/api/epubs/${encodeURIComponent(f.name)}" download class="text-xs px-3.5 py-1.5 rounded-xl bg-white text-black font-semibold hover:bg-zinc-200 transition">Download</a>
|
||||
<a href="/api/epubs/${encodeURIComponent(f.name)}" download class="text-sm px-3.5 py-1.5 rounded-md bg-theme-primary hover:bg-theme-primary-hover text-theme-primary-fg font-medium transition shadow-sm">Download</a>
|
||||
<div class="relative inline-block" id="delWrap_${CSS.escape(f.name)}">
|
||||
<button onclick="confirmDelete('${encodeURIComponent(f.name)}')" class="text-xs px-2.5 py-1.5 rounded-xl border border-zinc-800 hover:border-rose-900/60 hover:text-rose-400 text-zinc-400 transition">Delete</button>
|
||||
<button type="button" onclick="confirmDelete('${encodeURIComponent(f.name)}')" class="text-sm px-3 py-1.5 rounded-md border border-theme-border hover:border-theme-danger-border text-theme-muted hover:text-theme-danger transition">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -506,19 +623,19 @@ function renderEpubs(list){
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(encodedName){
|
||||
function confirmDelete(encodedName) {
|
||||
const name = decodeURIComponent(encodedName);
|
||||
const wrap = document.getElementById(`delWrap_${CSS.escape(name)}`);
|
||||
if (!wrap) return;
|
||||
wrap.innerHTML = `
|
||||
<div class="flex items-center gap-1.5 bg-rose-950/60 border border-rose-800/80 rounded-xl p-0.5">
|
||||
<button onclick="executeDelete('${encodeURIComponent(name)}')" class="text-[11px] px-2 py-1 bg-rose-600 text-white rounded-lg font-medium hover:bg-rose-500">Confirm</button>
|
||||
<button onclick="loadEpubs()" class="text-[11px] px-2 py-1 text-zinc-400 hover:text-white">Cancel</button>
|
||||
<div class="flex items-center gap-1.5 bg-theme-danger-bg border border-theme-danger-border rounded-md p-1">
|
||||
<button type="button" onclick="executeDelete('${encodeURIComponent(name)}')" class="text-xs px-2.5 py-1 bg-rose-600 text-white rounded font-medium hover:bg-rose-500">Confirm</button>
|
||||
<button type="button" onclick="loadEpubs()" class="text-xs px-2.5 py-1 text-theme-muted hover:text-theme-text">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function executeDelete(encodedName){
|
||||
async function executeDelete(encodedName) {
|
||||
try {
|
||||
const res = await fetch(`/api/epubs/${encodedName}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error('Delete failed');
|
||||
@@ -529,13 +646,11 @@ async function executeDelete(encodedName){
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-restore active job on load if any
|
||||
async function checkStoredJob(){
|
||||
async function checkStoredJob() {
|
||||
if (!currentJobId) return;
|
||||
try {
|
||||
const res = await fetch(`/api/jobs/${currentJobId}`);
|
||||
if (res.ok) {
|
||||
const j = await res.json();
|
||||
showActive();
|
||||
$('#jobStage').textContent = j.stage;
|
||||
$('#jobMessage').textContent = j.message || '';
|
||||
@@ -558,6 +673,8 @@ async function checkStoredJob(){
|
||||
}
|
||||
}
|
||||
|
||||
updateThemeUI();
|
||||
updateNotifyButton();
|
||||
checkStoredJob();
|
||||
loadEpubs();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user