fix(web): add series library selector
This commit is contained in:
+9
-3
@@ -499,14 +499,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- removal is the one library control a series carries (issue 175):
|
|
||||||
the same arming panel a movie uses — evidence first, then the
|
|
||||||
decision — wired to DELETE /api/series/{id} -->
|
|
||||||
<section class="deck-group" id="series-library" aria-label="library controls">
|
<section class="deck-group" id="series-library" aria-label="library controls">
|
||||||
<header class="deck-head">
|
<header class="deck-head">
|
||||||
<h3 class="deck-label">library</h3>
|
<h3 class="deck-label">library</h3>
|
||||||
</header>
|
</header>
|
||||||
<div class="movie-controls">
|
<div class="movie-controls">
|
||||||
|
<label class="movie-root-field">
|
||||||
|
<span class="field-label readout dim" id="series-root-label">root</span>
|
||||||
|
<select
|
||||||
|
class="form-input movie-root"
|
||||||
|
id="series-root"
|
||||||
|
data-focus-id="series-root"
|
||||||
|
aria-labelledby="series-root-label"
|
||||||
|
></select>
|
||||||
|
</label>
|
||||||
<span class="movie-controls-space"></span>
|
<span class="movie-controls-space"></span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ import {
|
|||||||
setEpisodeWanted,
|
setEpisodeWanted,
|
||||||
setSeasonTracked,
|
setSeasonTracked,
|
||||||
type TvTarget,
|
type TvTarget,
|
||||||
|
updateSeries,
|
||||||
waiveAndGrabTv,
|
waiveAndGrabTv,
|
||||||
} from "./series";
|
} from "./series";
|
||||||
import { settingsMain } from "./settings";
|
import { settingsMain } from "./settings";
|
||||||
@@ -3316,6 +3317,7 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
|
|||||||
const actionsEl = must<HTMLElement>("#series-actions");
|
const actionsEl = must<HTMLElement>("#series-actions");
|
||||||
const statusEl = must<HTMLElement>("#series-status");
|
const statusEl = must<HTMLElement>("#series-status");
|
||||||
const seasonsList = must<HTMLUListElement>("#rows-seasons");
|
const seasonsList = must<HTMLUListElement>("#rows-seasons");
|
||||||
|
const rootSelect = must<HTMLSelectElement>("#series-root");
|
||||||
const remove = must<HTMLButtonElement>("#series-remove");
|
const remove = must<HTMLButtonElement>("#series-remove");
|
||||||
const removeWrap = must<HTMLElement>("#series-remove-panel");
|
const removeWrap = must<HTMLElement>("#series-remove-panel");
|
||||||
|
|
||||||
@@ -3431,6 +3433,49 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function paintRootControl() {
|
||||||
|
const current = series;
|
||||||
|
if (!current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rootSelect.replaceChildren();
|
||||||
|
for (const root of roots.filter((candidate) => candidate.kind === "tv")) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = String(root.id);
|
||||||
|
option.textContent = `${root.audience} · ${root.policy_name}`;
|
||||||
|
rootSelect.append(option);
|
||||||
|
}
|
||||||
|
if (rootSelect.value !== String(current.root_id)) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = String(current.root_id);
|
||||||
|
option.textContent = `root ${current.root_id}`;
|
||||||
|
rootSelect.append(option);
|
||||||
|
}
|
||||||
|
rootSelect.value = String(current.root_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
rootSelect.addEventListener("change", () => {
|
||||||
|
const current = series;
|
||||||
|
if (!current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const next = Number(rootSelect.value);
|
||||||
|
if (!Number.isInteger(next) || next === current.root_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rootSelect.disabled = true;
|
||||||
|
void updateSeries(current.id, { root_id: next }).then((outcome) => {
|
||||||
|
rootSelect.disabled = false;
|
||||||
|
if (outcome.kind === "error") {
|
||||||
|
rootSelect.value = String(current.root_id);
|
||||||
|
setStatus(`root change failed — ${outcome.detail}`, "fault");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
focusKey = "series-root";
|
||||||
|
void load();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/* ---- §9.6 rich detail: one request, images hotlinked ---- */
|
/* ---- §9.6 rich detail: one request, images hotlinked ---- */
|
||||||
|
|
||||||
function clearRichDetail() {
|
function clearRichDetail() {
|
||||||
@@ -3984,6 +4029,7 @@ function seriesMain(tvDeck: TvReleasesView, views: HideableView[]): SeriesView {
|
|||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
paintHeader();
|
paintHeader();
|
||||||
|
paintRootControl();
|
||||||
clearRichDetail();
|
clearRichDetail();
|
||||||
renderSeasons();
|
renderSeasons();
|
||||||
syncRefreshWatch(ticket);
|
syncRefreshWatch(ticket);
|
||||||
|
|||||||
@@ -72,6 +72,26 @@ export async function fetchSeries(seriesId: number): Promise<ApiSeries | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Move a series to another TV root through its partial update endpoint. */
|
||||||
|
export async function updateSeries(
|
||||||
|
seriesId: number,
|
||||||
|
patch: { root_id: number },
|
||||||
|
): Promise<ActionOutcome> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/series/${seriesId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify(patch),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
return { kind: "error", detail: await errorDetail(response) };
|
||||||
|
}
|
||||||
|
return { kind: "done" };
|
||||||
|
} catch {
|
||||||
|
return { kind: "error", detail: "daemon unreachable" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** One episode by row id — the `/episodes/{id}/releases` deep link's lookup. */
|
/** One episode by row id — the `/episodes/{id}/releases` deep link's lookup. */
|
||||||
export async function fetchEpisode(episodeId: number): Promise<ApiEpisode | null> {
|
export async function fetchEpisode(episodeId: number): Promise<ApiEpisode | null> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user