diff --git a/static/app.js b/static/app.js index ead96b9..c346d13 100644 --- a/static/app.js +++ b/static/app.js @@ -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 = `

${t("watch.empty")}

`; + 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) => ` + ${p.volume ?? ""} + ${esc(p.title)}${p.year ? ` (${p.year})` : ""} + ${esc(p.authors)} + + + + + `) + .join(""); + return `
+
+ ${esc(w.title)} + ${esc(w.library_name)} · ${esc(w.media_type)} · ${t("watch.checked", { at: checked })} + +
+ ${w.error ? `

${esc(t("watch.error", { error: w.error }))}

` : ""} + ${parts ? `${parts}
` : `

${t("watch.nothingNew")}

`} +
`; + }) + .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(); diff --git a/static/i18n.js b/static/i18n.js index 98eaf4a..7a62c18 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -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.", diff --git a/static/index.html b/static/index.html index 0209978..3fa76ff 100644 --- a/static/index.html +++ b/static/index.html @@ -20,6 +20,10 @@ + + @@ -287,6 +292,13 @@ + +