Collapse watchlist alternative titles

This commit is contained in:
Dymas
2026-05-23 19:01:30 +02:00
parent 84c2e533d0
commit 0c1b46e395
7 changed files with 58 additions and 17 deletions
+4
View File
@@ -1,5 +1,9 @@
# Changelog # Changelog
## 0.37.1 - 2026-05-23
- Fixed AllManga alternative title parsing to read the visible `span.altnames` markup and changed watchlist cards to reveal those titles only when the new `Alternative titles` button is pressed.
## 0.37.0 - 2026-05-23 ## 0.37.0 - 2026-05-23
- Added watchlist support for alternative English and synonym titles pulled from the AllManga show page and shown directly on each watchlist card. - Added watchlist support for alternative English and synonym titles pulled from the AllManga show page and shown directly on each watchlist card.
+3 -2
View File
@@ -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.37.0` Current version: `0.37.1`
## Project Layout ## Project Layout
@@ -242,7 +242,8 @@ Thumbnail behavior:
Title behavior: Title behavior:
- Watchlist refreshes also try to pull alternative English and synonym titles from the AllManga show page. - Watchlist refreshes also try to pull alternative English and synonym titles from the AllManga show page.
- Up to three alternate names are shown as a muted secondary line under the main watchlist title. - Watchlist cards show an `Alternative titles` button when names are available.
- Pressing that button expands the stored alternative titles on demand.
- Missing covers are warmed in small batches. - Missing covers are warmed in small batches.
- Failed lookups back off before retrying. - Failed lookups back off before retrying.
- You can force `Reload` or use `Upload` when no thumbnail exists. - You can force `Reload` or use `Upload` when no thumbnail exists.
+1 -1
View File
@@ -1 +1 @@
0.37.0 0.37.1
+10
View File
@@ -199,6 +199,16 @@ def parse_allmanga_alternative_titles(html_text, primary_title=""):
for match in re.findall(r'"([^"]+)"', body): for match in re.findall(r'"([^"]+)"', body):
add_candidate(match, english_preferred=english_preferred) add_candidate(match, english_preferred=english_preferred)
for match in re.findall(
r'<span[^>]*class="[^"]*\baltnames\b[^"]*"[^>]*>\s*(.*?)\s*</span>',
text,
flags=re.IGNORECASE | re.DOTALL,
):
cleaned = re.sub(r"<[^>]+>", " ", match)
cleaned = cleaned.replace("&nbsp;", " ")
cleaned = re.sub(r"^[\s\u25cf\u26aa\u2022]+", "", cleaned)
add_candidate(cleaned, english_preferred=not preferred_english)
deduped = unique_title_values(candidates, primary) deduped = unique_title_values(candidates, primary)
english_title = "" english_title = ""
for candidate in unique_title_values([preferred_english], primary): for candidate in unique_title_values([preferred_english], primary):
+1 -1
View File
@@ -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.37.0" VERSION = "0.37.1"
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"
+9 -5
View File
@@ -1746,8 +1746,10 @@ class TemplateHelperTests(unittest.TestCase):
self.assertIn("Enter sub or dub", APP.WATCHLIST_HTML) self.assertIn("Enter sub or dub", APP.WATCHLIST_HTML)
def test_watchlist_page_shows_alternative_titles(self): def test_watchlist_page_shows_alternative_titles(self):
self.assertIn('class="alt-title"', APP.WATCHLIST_HTML) self.assertIn('class="ghost alt-toggle"', APP.WATCHLIST_HTML)
self.assertIn('Alt: ${item.display_alt_titles.join(" · ")}', APP.WATCHLIST_HTML) self.assertIn('class="alt-titles"', APP.WATCHLIST_HTML)
self.assertIn('Alternative titles', APP.WATCHLIST_HTML)
self.assertIn('altTitlesNode.classList.toggle("visible")', 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)
@@ -1757,9 +1759,11 @@ class TemplateHelperTests(unittest.TestCase):
def test_parse_allmanga_alternative_titles_extracts_english_and_synonyms(self): def test_parse_allmanga_alternative_titles_extracts_english_and_synonyms(self):
html_text = """ html_text = """
<script> <div class="show-more-content card-text p-0" style="">
{"englishTitle":"Example Show English","synonyms":["Sample Alias","Example Show"]} <span class="mr-1 altnames"> ⚪ Example Show English &nbsp; </span>
</script> <span class="mr-1 altnames"> ⚪ Sample Alias &nbsp; </span>
<span class="mr-1 altnames"> ⚪ Example Show &nbsp; </span>
</div>
""" """
parsed = APP.parse_allmanga_alternative_titles(html_text, primary_title="Example Show") parsed = APP.parse_allmanga_alternative_titles(html_text, primary_title="Example Show")
self.assertEqual(parsed["english_title"], "Example Show English") self.assertEqual(parsed["english_title"], "Example Show English")
+30 -8
View File
@@ -413,13 +413,23 @@ WATCHLIST_HTML = r"""<!doctype html>
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.alt-title { .alt-toggle {
min-height: 30px;
padding: 0 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
}
.alt-titles {
color: var(--muted); color: var(--muted);
font-size: 12px; font-size: 12px;
line-height: 1.35; line-height: 1.35;
margin-top: 4px; margin-top: 8px;
overflow-wrap: anywhere; overflow-wrap: anywhere;
min-height: 1.35em; display: none;
}
.alt-titles.visible {
display: block;
} }
.card-title a { .card-title a {
color: inherit; color: inherit;
@@ -1001,7 +1011,8 @@ WATCHLIST_HTML = r"""<!doctype html>
<div class="title-row"> <div class="title-row">
<div> <div>
<div class="card-title"></div> <div class="card-title"></div>
<div class="alt-title"></div> <button class="ghost alt-toggle" type="button" hidden>Alternative titles</button>
<div class="alt-titles" hidden></div>
</div> </div>
<span class="finish-check ${item.finished_badge || ""}" ${item.finished_badge ? "" : "hidden"}>${item.finished_badge ? "&check;" : ""}</span> <span class="finish-check ${item.finished_badge || ""}" ${item.finished_badge ? "" : "hidden"}>${item.finished_badge ? "&check;" : ""}</span>
</div> </div>
@@ -1030,10 +1041,21 @@ WATCHLIST_HTML = r"""<!doctype html>
titleLink.rel = "noopener noreferrer"; titleLink.rel = "noopener noreferrer";
titleLink.textContent = item.title; titleLink.textContent = item.title;
titleNode.replaceChildren(titleLink); titleNode.replaceChildren(titleLink);
const altTitleNode = card.querySelector(".alt-title"); const altToggle = card.querySelector(".alt-toggle");
altTitleNode.textContent = (item.display_alt_titles || []).length const altTitlesNode = card.querySelector(".alt-titles");
? `Alt: ${item.display_alt_titles.join(" · ")}` const altTitles = item.display_alt_titles || [];
: ""; if (altTitles.length) {
altToggle.hidden = false;
altTitlesNode.hidden = false;
altTitlesNode.textContent = altTitles.join(" · ");
altToggle.setAttribute("aria-expanded", "false");
altToggle.addEventListener("click", () => {
const visible = altTitlesNode.classList.toggle("visible");
altToggle.setAttribute("aria-expanded", visible ? "true" : "false");
altToggle.textContent = visible ? "Hide alternative titles" : "Alternative titles";
});
altToggle.textContent = "Alternative titles";
}
const finishCheck = card.querySelector(".finish-check"); const finishCheck = card.querySelector(".finish-check");
if (finishCheck && item.finished_badge) { if (finishCheck && item.finished_badge) {
finishCheck.title = item.finished_badge === "downloaded" finishCheck.title = item.finished_badge === "downloaded"