Use inline watchlist download mode picker
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# 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
|
## 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.
|
- 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.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Small local web UI for a system-wide `ani-cli` install.
|
Small local web UI for a system-wide `ani-cli` install.
|
||||||
|
|
||||||
Current version: `0.36.1`
|
Current version: `0.36.2`
|
||||||
|
|
||||||
## Project Layout
|
## Project Layout
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ Main behavior:
|
|||||||
|
|
||||||
1. `Refresh` updates one show.
|
1. `Refresh` updates one show.
|
||||||
2. `Refresh All` runs in the background.
|
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.
|
4. Clicking the title opens the source page.
|
||||||
5. `Remove` deletes the entry and cached poster.
|
5. `Remove` deletes the entry and cached poster.
|
||||||
6. Each card has an inline category selector.
|
6. Each card has an inline category selector.
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ from uuid import uuid4
|
|||||||
PROJECT_ROOT = Path(__file__).resolve().parent
|
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||||
ANI_CLI = os.environ.get("ANI_CLI_BIN") or shutil.which("ani-cli") or "ani-cli"
|
ANI_CLI = os.environ.get("ANI_CLI_BIN") or shutil.which("ani-cli") or "ani-cli"
|
||||||
APP_NAME = "ani-cli-web"
|
APP_NAME = "ani-cli-web"
|
||||||
VERSION = "0.36.1"
|
VERSION = "0.36.2"
|
||||||
ALLANIME_BASE = "allanime.day"
|
ALLANIME_BASE = "allanime.day"
|
||||||
ALLANIME_API = f"https://api.{ALLANIME_BASE}"
|
ALLANIME_API = f"https://api.{ALLANIME_BASE}"
|
||||||
ALLANIME_REFERER = "https://allmanga.to"
|
ALLANIME_REFERER = "https://allmanga.to"
|
||||||
|
|||||||
+4
-1
@@ -1726,7 +1726,10 @@ class TemplateHelperTests(unittest.TestCase):
|
|||||||
def test_watchlist_page_download_all_replaces_open_action(self):
|
def test_watchlist_page_download_all_replaces_open_action(self):
|
||||||
self.assertIn("Download all", APP.WATCHLIST_HTML)
|
self.assertIn("Download all", APP.WATCHLIST_HTML)
|
||||||
self.assertIn('api("/api/watchlist/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):
|
def test_watchlist_thumbnail_warmup_skips_repeated_attempts(self):
|
||||||
self.assertIn("thumbnailWarmupPending: new Set()", APP.WATCHLIST_HTML)
|
self.assertIn("thumbnailWarmupPending: new Set()", APP.WATCHLIST_HTML)
|
||||||
|
|||||||
+26
-20
@@ -523,6 +523,15 @@ WATCHLIST_HTML = r"""<!doctype html>
|
|||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
padding: 0 10px;
|
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 {
|
.pager {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -1009,6 +1018,10 @@ WATCHLIST_HTML = r"""<!doctype html>
|
|||||||
<button class="primary" type="button">Refresh</button>
|
<button class="primary" type="button">Refresh</button>
|
||||||
<button class="ghost" type="button">Download all</button>
|
<button class="ghost" type="button">Download all</button>
|
||||||
<button class="danger" type="button">Remove</button>
|
<button class="danger" type="button">Remove</button>
|
||||||
|
<div class="download-picker">
|
||||||
|
<button class="ghost" type="button">Sub</button>
|
||||||
|
<button class="ghost" type="button">Dub</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
card.querySelector(".cover-fallback").textContent = coverInitials(item.title);
|
card.querySelector(".cover-fallback").textContent = coverInitials(item.title);
|
||||||
@@ -1051,8 +1064,11 @@ WATCHLIST_HTML = r"""<!doctype html>
|
|||||||
});
|
});
|
||||||
const actionButtons = card.querySelector(".card-actions").querySelectorAll("button");
|
const actionButtons = card.querySelector(".card-actions").querySelectorAll("button");
|
||||||
const refreshBtn = actionButtons[0];
|
const refreshBtn = actionButtons[0];
|
||||||
const searchBtn = actionButtons[1];
|
const downloadBtn = actionButtons[1];
|
||||||
const removeBtn = actionButtons[2];
|
const removeBtn = actionButtons[2];
|
||||||
|
const subBtn = actionButtons[3];
|
||||||
|
const dubBtn = actionButtons[4];
|
||||||
|
const downloadPicker = card.querySelector(".download-picker");
|
||||||
const categorySelect = card.querySelector(".category-select");
|
const categorySelect = card.querySelector(".category-select");
|
||||||
categorySelect.addEventListener("change", async () => {
|
categorySelect.addEventListener("change", async () => {
|
||||||
categorySelect.disabled = true;
|
categorySelect.disabled = true;
|
||||||
@@ -1071,7 +1087,12 @@ WATCHLIST_HTML = r"""<!doctype html>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
refreshBtn.addEventListener("click", () => refreshWatchlistItem(item.show_id));
|
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));
|
removeBtn.addEventListener("click", () => removeWatchlistItem(item.show_id, item.title));
|
||||||
container.appendChild(card);
|
container.appendChild(card);
|
||||||
applyThumbnail(card, item, false);
|
applyThumbnail(card, item, false);
|
||||||
@@ -1166,30 +1187,15 @@ WATCHLIST_HTML = r"""<!doctype html>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseWatchlistDownloadMode(item) {
|
async function downloadWatchlistItem(item, mode, picker = null, toggleBtn = null) {
|
||||||
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;
|
|
||||||
setNotice(`Queueing ${item.title || "selected anime"} for ${mode} download...`);
|
setNotice(`Queueing ${item.title || "selected anime"} for ${mode} download...`);
|
||||||
try {
|
try {
|
||||||
const data = await api("/api/watchlist/download-all", {
|
const data = await api("/api/watchlist/download-all", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ show_id: item.show_id, mode })
|
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.");
|
setNotice(data.message || "Added to queue.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setNotice(error.message);
|
setNotice(error.message);
|
||||||
|
|||||||
Reference in New Issue
Block a user