feat(ui): watch a series from its dialog and review new parts
This commit is contained in:
1 parent
55d7c8bdda
commit
1487512183
4 files changed
+153
-1
No files matched your search
+111
-1
@@ -93,7 +93,7 @@ async function refreshMissingBadge() {
|
||||
|
||||
// ---- navigation ----
|
||||
// the view lives in the hash so reload, back button and links keep it
|
||||
const VIEWS = ["search", "missing", "imported", "import", "activity", "settings", "system"];
|
||||
const VIEWS = ["search", "missing", "imported", "import", "series", "activity", "settings", "system"];
|
||||
|
||||
function showView() {
|
||||
const hash = location.hash.slice(1);
|
||||
@@ -103,6 +103,7 @@ function showView() {
|
||||
if (name === "missing" || name === "imported") loadRequests(name);
|
||||
if (name === "activity") loadHistory();
|
||||
if (name === "system") loadSystem();
|
||||
if (name === "series") loadWatches();
|
||||
if (name === "settings") loadLibraries().catch((err) => toast(err.message, true));
|
||||
}
|
||||
|
||||
@@ -320,6 +321,8 @@ function renderSearchResults() {
|
||||
// ---- request a whole series ----
|
||||
let seriesEpisodes = [];
|
||||
let seriesType = "audiobook";
|
||||
let seriesResult = null;
|
||||
let seriesLanguage = "";
|
||||
|
||||
function renderSeriesList() {
|
||||
const from = parseInt($("#series-from").value);
|
||||
@@ -366,6 +369,8 @@ function updateSeriesCount() {
|
||||
async function openSeriesDialog(result, type = "audiobook", language = "") {
|
||||
const dlg = $("#series-dialog");
|
||||
seriesType = type;
|
||||
seriesResult = result;
|
||||
seriesLanguage = language;
|
||||
const unit = t(type === "ebook" ? "series.unitVolumes" : "series.unitEpisodes");
|
||||
$("#series-title").textContent = result.series || t("series.fallbackTitle");
|
||||
$("#series-info").innerHTML =
|
||||
@@ -411,6 +416,31 @@ $("#series-none").addEventListener("click", () => {
|
||||
});
|
||||
$("#series-cancel").addEventListener("click", () => $("#series-dialog").close());
|
||||
|
||||
$("#series-watch").addEventListener("click", async () => {
|
||||
const libId = $("#series-library").value;
|
||||
if (!libId) {
|
||||
toast(t("series.needLibrary", { type: seriesType === "ebook" ? "Ebook" : "Audiobook" }), true);
|
||||
return;
|
||||
}
|
||||
const btn = $("#series-watch");
|
||||
btn.disabled = true;
|
||||
try {
|
||||
await api("/api/series", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
media_type: seriesType, library_id: parseInt(libId),
|
||||
series_id: seriesResult.series_id, source: seriesResult.source || "",
|
||||
title: seriesResult.series || seriesResult.title, language: seriesLanguage,
|
||||
}),
|
||||
});
|
||||
toast(t("watch.added"));
|
||||
} catch (err) {
|
||||
toast(err.status === 409 ? t("watch.already") : err.message, true);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
$("#series-submit").addEventListener("click", async () => {
|
||||
const libId = $("#series-library").value;
|
||||
if (!libId) {
|
||||
@@ -701,6 +731,84 @@ function renderHistory() {
|
||||
.join("");
|
||||
}
|
||||
|
||||
// ---- watched series ----
|
||||
let watches = [];
|
||||
|
||||
async function loadWatches(path = "/api/series", opts = {}) {
|
||||
try {
|
||||
watches = await api(path, opts);
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
return;
|
||||
}
|
||||
renderWatches();
|
||||
}
|
||||
|
||||
function renderWatches() {
|
||||
const n = watches.reduce((sum, w) => sum + w.new.length, 0);
|
||||
$("#watch-badge").textContent = n;
|
||||
$("#watch-badge").hidden = n === 0;
|
||||
const box = $("#watch-list");
|
||||
if (!watches.length) {
|
||||
box.innerHTML = `<p class="muted">${t("watch.empty")}</p>`;
|
||||
return;
|
||||
}
|
||||
const locale = lang === "de" ? "de-DE" : "en-GB";
|
||||
box.innerHTML = watches
|
||||
.map((w) => {
|
||||
const checked = w.checked_at ? new Date(w.checked_at).toLocaleString(locale) : "—";
|
||||
const parts = w.new
|
||||
.map((p) => `<tr>
|
||||
<td>${p.volume ?? ""}</td>
|
||||
<td>${esc(p.title)}${p.year ? ` <span class="muted">(${p.year})</span>` : ""}</td>
|
||||
<td class="muted">${esc(p.authors)}</td>
|
||||
<td class="row">
|
||||
<button data-part="${p.id}" data-action="request">${t("common.request")}</button>
|
||||
<button class="secondary" data-part="${p.id}" data-action="ignore">${t("watch.ignore")}</button>
|
||||
</td>
|
||||
</tr>`)
|
||||
.join("");
|
||||
return `<div class="watch">
|
||||
<div class="row">
|
||||
<strong>${esc(w.title)}</strong>
|
||||
<span class="muted">${esc(w.library_name)} · ${esc(w.media_type)} · ${t("watch.checked", { at: checked })}</span>
|
||||
<button class="danger" data-unwatch="${w.id}">${t("watch.remove")}</button>
|
||||
</div>
|
||||
${w.error ? `<p class="muted error-text">${esc(t("watch.error", { error: w.error }))}</p>` : ""}
|
||||
${parts ? `<table><tbody>${parts}</tbody></table>` : `<p class="muted">${t("watch.nothingNew")}</p>`}
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
$("#watch-list").addEventListener("click", async (e) => {
|
||||
const btn = e.target.closest("button");
|
||||
if (!btn) return;
|
||||
try {
|
||||
if (btn.dataset.part) {
|
||||
btn.disabled = true;
|
||||
await api("/api/series/parts", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ ids: [parseInt(btn.dataset.part)], action: btn.dataset.action }),
|
||||
});
|
||||
if (btn.dataset.action === "request") refreshMissingBadge();
|
||||
} else if (btn.dataset.unwatch) {
|
||||
if (!confirm(t("watch.confirmRemove"))) return;
|
||||
await api(`/api/series/${btn.dataset.unwatch}`, { method: "DELETE" });
|
||||
} else return;
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
}
|
||||
loadWatches();
|
||||
});
|
||||
|
||||
$("#watch-check").addEventListener("click", async () => {
|
||||
const btn = $("#watch-check");
|
||||
btn.disabled = true;
|
||||
await loadWatches("/api/series/check", { method: "POST" });
|
||||
btn.disabled = false;
|
||||
});
|
||||
|
||||
// ---- system ----
|
||||
let healthIssues = [];
|
||||
let logRows = [];
|
||||
@@ -1962,6 +2070,7 @@ function onLangChange() {
|
||||
renderHistory();
|
||||
renderHealth();
|
||||
renderLogs();
|
||||
renderWatches();
|
||||
if (libraries.length) loadLibraries();
|
||||
const editId = $("#lib-form").dataset.editId;
|
||||
editId ? startLibraryEdit(parseInt(editId)) : endLibraryEdit();
|
||||
@@ -1987,5 +2096,6 @@ updateLangButton();
|
||||
endLibraryEdit();
|
||||
loadLibraries();
|
||||
refreshMissingBadge();
|
||||
loadWatches();
|
||||
showVersion();
|
||||
showView();
|
||||
@@ -47,6 +47,19 @@ const STRINGS = {
|
||||
"nav.import": "Import",
|
||||
"nav.settings": "Libraries",
|
||||
"nav.activity": "Aktivität",
|
||||
"nav.series": "Serien",
|
||||
"watch.button": "Beobachten",
|
||||
"watch.buttonTitle": "Täglich nach neuen Teilen dieser Serie schauen",
|
||||
"watch.added": "Serie wird beobachtet",
|
||||
"watch.already": "Diese Serie wird schon beobachtet",
|
||||
"watch.check": "Jetzt prüfen",
|
||||
"watch.empty": "Noch keine Serie beobachtet. In der Suche unter „Ganze Serie…“ auf „Beobachten“ klicken.",
|
||||
"watch.checked": "geprüft {at}",
|
||||
"watch.error": "Letzte Prüfung fehlgeschlagen: {error}",
|
||||
"watch.nothingNew": "Nichts Neues.",
|
||||
"watch.ignore": "Ignorieren",
|
||||
"watch.remove": "Nicht mehr beobachten",
|
||||
"watch.confirmRemove": "Diese Serie nicht mehr beobachten?",
|
||||
"nav.system": "System",
|
||||
"system.health": "Zustand",
|
||||
"system.healthy": "Alles in Ordnung.",
|
||||
@@ -309,6 +322,19 @@ const STRINGS = {
|
||||
"nav.import": "Import",
|
||||
"nav.settings": "Libraries",
|
||||
"nav.activity": "Activity",
|
||||
"nav.series": "Series",
|
||||
"watch.button": "Watch",
|
||||
"watch.buttonTitle": "Look for new parts of this series every day",
|
||||
"watch.added": "Series is being watched",
|
||||
"watch.already": "This series is already watched",
|
||||
"watch.check": "Check now",
|
||||
"watch.empty": "No series watched yet. In the search, open “Whole series…” and click “Watch”.",
|
||||
"watch.checked": "checked {at}",
|
||||
"watch.error": "Last check failed: {error}",
|
||||
"watch.nothingNew": "Nothing new.",
|
||||
"watch.ignore": "Ignore",
|
||||
"watch.remove": "Stop watching",
|
||||
"watch.confirmRemove": "Stop watching this series?",
|
||||
"nav.system": "System",
|
||||
"system.health": "Health",
|
||||
"system.healthy": "All good.",
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
</button>
|
||||
<button data-view="imported" data-i18n="nav.imported"></button>
|
||||
<button data-view="import" data-i18n="nav.import"></button>
|
||||
<button data-view="series">
|
||||
<span data-i18n="nav.series"></span>
|
||||
<span id="watch-badge" class="badge" hidden></span>
|
||||
</button>
|
||||
<button data-view="activity" data-i18n="nav.activity"></button>
|
||||
<button data-view="settings" data-i18n="nav.settings"></button>
|
||||
<button data-view="system">
|
||||
@@ -97,6 +101,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<button type="button" id="series-cancel" class="secondary" data-i18n="common.cancel"></button>
|
||||
<button type="button" id="series-watch" class="secondary" data-i18n="watch.button" data-i18n-title="watch.buttonTitle"></button>
|
||||
<button type="button" id="series-submit" data-i18n="common.request"></button>
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -287,6 +292,13 @@
|
||||
</dialog>
|
||||
</section>
|
||||
|
||||
<section id="view-series" hidden>
|
||||
<div class="bar">
|
||||
<button type="button" id="watch-check" data-i18n="watch.check"></button>
|
||||
</div>
|
||||
<div id="watch-list"></div>
|
||||
</section>
|
||||
|
||||
<section id="view-activity" hidden>
|
||||
<div class="table-scroll">
|
||||
<table id="history-table">
|
||||
|
||||
@@ -77,6 +77,10 @@ td.state.running { color: var(--muted); }
|
||||
td.state.warn { color: var(--warn); }
|
||||
.badge.warn { background: var(--warn); color: #000; }
|
||||
.badge.error { background: var(--danger); }
|
||||
.watch { background: var(--panel); border: 1px solid var(--border); border-radius: 6px; padding: 0.7rem 0.9rem; margin-bottom: 0.8rem; }
|
||||
.watch .row:first-child { margin-top: 0; }
|
||||
.watch table { margin-top: 0.5rem; }
|
||||
.watch .error-text { color: #e08585; }
|
||||
#health-list { list-style: none; margin: 0.5rem 0 1rem; }
|
||||
#health-list li { background: var(--panel); border-left: 3px solid var(--warn); border-radius: 4px; padding: 0.5rem 0.8rem; margin: 0.3rem 0; }
|
||||
#health-list li.error { border-left-color: var(--danger); }
|
||||
|
||||
Reference in new issue
Block a user