Show watchlist refresh progress as a single bar
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user