diff --git a/CHANGELOG.md b/CHANGELOG.md index 965902b..35f75d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.36.2 - 2026-05-23 + +- Replaced the watchlist `Download all` text prompt with an inline dropdown-style picker that lets you choose `Sub` or `Dub` directly on the card. + ## 0.36.1 - 2026-05-23 - Replaced the first-visit remote-access 403 dead end with a browser sign-in page and persistent auth cookie, so remote users no longer need to know the `?token=...` bootstrap URL format up front. diff --git a/README.md b/README.md index e356ed8..398e84e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Small local web UI for a system-wide `ani-cli` install. -Current version: `0.36.1` +Current version: `0.36.2` ## Project Layout @@ -213,7 +213,7 @@ Main behavior: 1. `Refresh` updates one show. 2. `Refresh All` runs in the background. -3. `Download all` prompts for `sub` or `dub` and queues every currently available episode directly. +3. `Download all` expands an inline picker for `Sub` or `Dub` and then queues every currently available episode directly. 4. Clicking the title opens the source page. 5. `Remove` deletes the entry and cached poster. 6. Each card has an inline category selector. diff --git a/VERSION b/VERSION index 19199bc..d647c8a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.36.1 +0.36.2 diff --git a/app_support.py b/app_support.py index 2b37da2..76eddfe 100644 --- a/app_support.py +++ b/app_support.py @@ -19,7 +19,7 @@ from uuid import uuid4 PROJECT_ROOT = Path(__file__).resolve().parent ANI_CLI = os.environ.get("ANI_CLI_BIN") or shutil.which("ani-cli") or "ani-cli" APP_NAME = "ani-cli-web" -VERSION = "0.36.1" +VERSION = "0.36.2" ALLANIME_BASE = "allanime.day" ALLANIME_API = f"https://api.{ALLANIME_BASE}" ALLANIME_REFERER = "https://allmanga.to" diff --git a/test_app.py b/test_app.py index e9baa23..49b53fa 100644 --- a/test_app.py +++ b/test_app.py @@ -1726,7 +1726,10 @@ class TemplateHelperTests(unittest.TestCase): def test_watchlist_page_download_all_replaces_open_action(self): self.assertIn("Download all", APP.WATCHLIST_HTML) self.assertIn('api("/api/watchlist/download-all"', APP.WATCHLIST_HTML) - self.assertIn("Enter sub or dub", APP.WATCHLIST_HTML) + self.assertIn('class="download-picker"', APP.WATCHLIST_HTML) + self.assertIn('downloadPicker.classList.toggle("visible")', APP.WATCHLIST_HTML) + self.assertIn('downloadWatchlistItem(item, "sub"', APP.WATCHLIST_HTML) + self.assertIn('downloadWatchlistItem(item, "dub"', APP.WATCHLIST_HTML) def test_watchlist_thumbnail_warmup_skips_repeated_attempts(self): self.assertIn("thumbnailWarmupPending: new Set()", APP.WATCHLIST_HTML) diff --git a/watchlist_page.py b/watchlist_page.py index 919552b..18a3807 100644 --- a/watchlist_page.py +++ b/watchlist_page.py @@ -523,6 +523,15 @@ WATCHLIST_HTML = r""" min-height: 34px; padding: 0 10px; } + .download-picker { + display: none; + grid-column: 1 / -1; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + } + .download-picker.visible { + display: grid; + } .pager { display: flex; justify-content: space-between; @@ -1009,6 +1018,10 @@ WATCHLIST_HTML = r""" +
+ + +
`; card.querySelector(".cover-fallback").textContent = coverInitials(item.title); @@ -1051,8 +1064,11 @@ WATCHLIST_HTML = r""" }); const actionButtons = card.querySelector(".card-actions").querySelectorAll("button"); const refreshBtn = actionButtons[0]; - const searchBtn = actionButtons[1]; + const downloadBtn = actionButtons[1]; const removeBtn = actionButtons[2]; + const subBtn = actionButtons[3]; + const dubBtn = actionButtons[4]; + const downloadPicker = card.querySelector(".download-picker"); const categorySelect = card.querySelector(".category-select"); categorySelect.addEventListener("change", async () => { categorySelect.disabled = true; @@ -1071,7 +1087,12 @@ WATCHLIST_HTML = r""" } }); refreshBtn.addEventListener("click", () => refreshWatchlistItem(item.show_id)); - searchBtn.addEventListener("click", () => downloadWatchlistItem(item)); + downloadBtn.addEventListener("click", () => { + const visible = downloadPicker.classList.toggle("visible"); + downloadBtn.textContent = visible ? "Cancel download" : "Download all"; + }); + subBtn.addEventListener("click", () => downloadWatchlistItem(item, "sub", downloadPicker, downloadBtn)); + dubBtn.addEventListener("click", () => downloadWatchlistItem(item, "dub", downloadPicker, downloadBtn)); removeBtn.addEventListener("click", () => removeWatchlistItem(item.show_id, item.title)); container.appendChild(card); applyThumbnail(card, item, false); @@ -1166,30 +1187,15 @@ WATCHLIST_HTML = r""" } } - function chooseWatchlistDownloadMode(item) { - const fallback = item.dub_count > 0 && item.sub_count < 1 ? "dub" : "sub"; - const answer = window.prompt( - `Download all available episodes for ${item.title || "this anime"}.\nEnter sub or dub:`, - fallback, - ); - if (answer === null) return ""; - const mode = String(answer || "").trim().toLowerCase(); - if (mode === "sub" || mode === "dub") { - return mode; - } - setNotice("Enter sub or dub to queue a watchlist download."); - return ""; - } - - async function downloadWatchlistItem(item) { - const mode = chooseWatchlistDownloadMode(item); - if (!mode) return; + async function downloadWatchlistItem(item, mode, picker = null, toggleBtn = null) { setNotice(`Queueing ${item.title || "selected anime"} for ${mode} download...`); try { const data = await api("/api/watchlist/download-all", { method: "POST", body: JSON.stringify({ show_id: item.show_id, mode }) }); + if (picker) picker.classList.remove("visible"); + if (toggleBtn) toggleBtn.textContent = "Download all"; setNotice(data.message || "Added to queue."); } catch (error) { setNotice(error.message);