Show watchlist refresh progress as a single bar

This commit is contained in:
Dymas
2026-08-18 08:48:13 +02:00
parent d663318f27
commit ba8e55bd97
5 changed files with 89 additions and 7 deletions
+4
View File
@@ -1,5 +1,9 @@
# Changelog # Changelog
## 0.52.15 - 2026-08-18
- Replaced duplicated Watchlist refresh-all progress text with a single progress bar and current-anime label.
## 0.52.14 - 2026-08-18 ## 0.52.14 - 2026-08-18
- Moved each completed episode from staging into the configured downloads library as soon as the downloader reports that episode finished. - Moved each completed episode from staging into the configured downloads library as soon as the downloader reports that episode finished.
+1 -1
View File
@@ -15,7 +15,7 @@ Kaizoku is a local web app for searching, tracking, and downloading anime from A
- Save files with Jellyfin-friendly layout: `TV/Series Name/Season 01/Series Name - S01E01.mp4`. - Save files with Jellyfin-friendly layout: `TV/Series Name/Season 01/Series Name - S01E01.mp4`.
- Save English external subtitles when the provider exposes usable subtitle tracks. - Save English external subtitles when the provider exposes usable subtitle tracks.
- Manage focused watchlists for `Watching`, `Planned`, `Finished`, and `Dropped`, with provider source tags and title links back to the original provider page. - Manage focused watchlists for `Watching`, `Planned`, `Finished`, and `Dropped`, with provider source tags and title links back to the original provider page.
- Periodically refresh selected watchlists and auto-download newly available episodes. - Periodically refresh selected watchlists with a single progress bar and auto-download newly available episodes.
- Sync watchlist download flags from the filesystem. - Sync watchlist download flags from the filesystem.
- Hand completed libraries to Jellyfin TV or movie folders. - Hand completed libraries to Jellyfin TV or movie folders.
- Export/import watchlist backups. - Export/import watchlist backups.
+1 -1
View File
@@ -1 +1 @@
0.52.14 0.52.15
+9
View File
@@ -4301,6 +4301,15 @@ class TemplateHelperTests(unittest.TestCase):
self.assertIn("else if (state.queuedCount > 0)", APP.WATCHLIST_HTML) self.assertIn("else if (state.queuedCount > 0)", APP.WATCHLIST_HTML)
self.assertIn("fetchWatchlist(state.watchlistPage, { silent: true })", APP.WATCHLIST_HTML) self.assertIn("fetchWatchlist(state.watchlistPage, { silent: true })", APP.WATCHLIST_HTML)
def test_watchlist_page_renders_single_refresh_progress_bar(self):
self.assertIn('class="refresh-progress" id="refreshJobStatus"', APP.WATCHLIST_HTML)
self.assertIn('class="refresh-progress-fill" id="refreshJobFill"', APP.WATCHLIST_HTML)
self.assertIn("function refreshJobPercent(job)", APP.WATCHLIST_HTML)
self.assertIn('$("refreshJobFill").style.width = `${percent}%`;', APP.WATCHLIST_HTML)
self.assertIn('`Refreshing ${job.current_title}`', APP.WATCHLIST_HTML)
self.assertEqual(APP.WATCHLIST_HTML.count('id="refreshJobStatus"'), 1)
self.assertNotIn("refreshed${job.current_title", APP.WATCHLIST_HTML)
def test_watchlist_page_highlights_fully_ready_cards(self): def test_watchlist_page_highlights_fully_ready_cards(self):
self.assertIn(".anime-card.fully-ready", APP.WATCHLIST_HTML) self.assertIn(".anime-card.fully-ready", APP.WATCHLIST_HTML)
self.assertIn("const isFullyReady = item.sub_complete && item.dub_complete;", APP.WATCHLIST_HTML) self.assertIn("const isFullyReady = item.sub_complete && item.dub_complete;", APP.WATCHLIST_HTML)
+74 -5
View File
@@ -254,6 +254,44 @@ WATCHLIST_HTML = r"""<!doctype html>
color: #ddd0ff; color: #ddd0ff;
font-size: 13px; font-size: 13px;
} }
.refresh-progress {
display: none;
gap: 7px;
min-width: 0;
}
.refresh-progress.visible {
display: grid;
}
.refresh-progress-header {
align-items: center;
color: var(--muted);
display: flex;
font-size: 12px;
font-weight: 700;
justify-content: space-between;
gap: 10px;
text-transform: uppercase;
}
.refresh-progress-track {
background: rgba(255, 255, 255, 0.08);
border: 1px solid var(--line);
border-radius: 999px;
height: 11px;
overflow: hidden;
}
.refresh-progress-fill {
background: linear-gradient(90deg, var(--accent), var(--accent-2));
border-radius: inherit;
height: 100%;
transition: width 0.2s ease;
width: 0%;
}
.refresh-progress-current {
color: #ddd0ff;
font-size: 13px;
min-height: 20px;
overflow-wrap: anywhere;
}
.field-hint { .field-hint {
color: var(--muted); color: var(--muted);
font-size: 12px; font-size: 12px;
@@ -716,6 +754,16 @@ WATCHLIST_HTML = r"""<!doctype html>
<button class="ghost" id="refreshAllWatchlistBtn" type="button">Refresh All</button> <button class="ghost" id="refreshAllWatchlistBtn" type="button">Refresh All</button>
</div> </div>
<div class="notice" id="notice"></div> <div class="notice" id="notice"></div>
<div class="refresh-progress" id="refreshJobStatus">
<div class="refresh-progress-header">
<span>Refresh progress</span>
<span id="refreshJobCount">0 / 0</span>
</div>
<div class="refresh-progress-track" aria-hidden="true">
<div class="refresh-progress-fill" id="refreshJobFill"></div>
</div>
<div class="refresh-progress-current" id="refreshJobCurrent"></div>
</div>
<div class="summary-grid" id="watchlist-summary"> <div class="summary-grid" id="watchlist-summary">
<div class="summary-card"> <div class="summary-card">
<span class="summary-value">0</span> <span class="summary-value">0</span>
@@ -737,7 +785,6 @@ WATCHLIST_HTML = r"""<!doctype html>
<div> <div>
<h2>Watchlist library</h2> <h2>Watchlist library</h2>
<p class="muted" id="watchlist-meta">Compact poster grid for faster scanning.</p> <p class="muted" id="watchlist-meta">Compact poster grid for faster scanning.</p>
<p class="muted" id="refreshJobStatus"></p>
</div> </div>
</div> </div>
<div class="category-tabs" id="watchlistTabs"></div> <div class="category-tabs" id="watchlistTabs"></div>
@@ -867,18 +914,40 @@ WATCHLIST_HTML = r"""<!doctype html>
button.textContent = running ? "Refreshing..." : "Refresh All"; button.textContent = running ? "Refreshing..." : "Refresh All";
} }
function refreshJobPercent(job) {
const total = Number((job || {}).total || 0);
if (total <= 0) return 0;
const completed = Math.max(0, Math.min(total, Number((job || {}).completed || 0)));
return Math.round((completed / total) * 100);
}
function setRefreshJobStatus(job) { function setRefreshJobStatus(job) {
const el = $("refreshJobStatus"); const el = $("refreshJobStatus");
if (!el) return; if (!el) return;
if (!job) { if (!job) {
el.textContent = ""; el.classList.remove("visible");
$("refreshJobFill").style.width = "0%";
$("refreshJobCount").textContent = "0 / 0";
$("refreshJobCurrent").textContent = "";
return;
}
const completed = Number(job.completed || 0);
const total = Number(job.total || 0);
const percent = refreshJobPercent(job);
el.classList.toggle("visible", job.status === "running" || job.status === "pending");
$("refreshJobFill").style.width = `${percent}%`;
$("refreshJobCount").textContent = `${completed} / ${total}`;
if (job.status === "pending") {
$("refreshJobCurrent").textContent = job.message || "Waiting to start refresh.";
return; return;
} }
if (job.status === "running") { if (job.status === "running") {
el.textContent = `${job.completed || 0}/${job.total || 0} refreshed${job.current_title ? ` · ${job.current_title}` : ""}`; $("refreshJobCurrent").textContent = job.current_title
? `Refreshing ${job.current_title}`
: "Refreshing watchlist entries";
return; return;
} }
el.textContent = job.message || ""; $("refreshJobCurrent").textContent = "";
} }
function coverInitials(title) { function coverInitials(title) {
@@ -1497,7 +1566,7 @@ WATCHLIST_HTML = r"""<!doctype html>
setRefreshAllButton(running); setRefreshAllButton(running);
setRefreshJobStatus(job); setRefreshJobStatus(job);
if (running) { if (running) {
setNotice(job.message || "Refreshing all watchlist entries..."); setNotice("");
} else if (job && wasRunning && refreshOnComplete) { } else if (job && wasRunning && refreshOnComplete) {
const message = job.message || "Watchlist refreshed."; const message = job.message || "Watchlist refreshed.";
await fetchWatchlist(); await fetchWatchlist();