feat (ui): selecteer meerdere episodes tegelijkertijd
This commit is contained in:
+119
-35
@@ -7,6 +7,8 @@
|
||||
selectedSeries: null,
|
||||
selectedSeriesSummary: null,
|
||||
episodes: [],
|
||||
selectedEpisodeIds: new Set(),
|
||||
episodeSelectionAnchorId: null,
|
||||
selectedEpisodes: [],
|
||||
selectedFiles: [],
|
||||
selectedPairIndex: null,
|
||||
@@ -63,6 +65,7 @@
|
||||
rememberMaxSeriesInput: document.getElementById("rememberMaxSeriesInput"),
|
||||
purgeRememberedSeriesBtn: document.getElementById("purgeRememberedSeriesBtn"),
|
||||
refreshEpisodesBtn: document.getElementById("refreshEpisodesBtn"),
|
||||
addSelectedEpisodesBtn: document.getElementById("addSelectedEpisodesBtn"),
|
||||
episodesList: document.getElementById("episodesList"),
|
||||
episodeMeta: document.getElementById("episodeMeta"),
|
||||
refreshSelectedEpisodesBtn: document.getElementById("refreshSelectedEpisodesBtn"),
|
||||
@@ -159,14 +162,6 @@
|
||||
console.log(text);
|
||||
}
|
||||
|
||||
function makeBtn(label, handler, secondary) {
|
||||
const btn = document.createElement("button");
|
||||
btn.textContent = label;
|
||||
if (secondary) btn.className = "secondary";
|
||||
btn.addEventListener("click", handler);
|
||||
return btn;
|
||||
}
|
||||
|
||||
function setBusy(btn, busy) {
|
||||
if (btn) btn.disabled = busy;
|
||||
}
|
||||
@@ -653,12 +648,24 @@
|
||||
out("Search result", data);
|
||||
}
|
||||
|
||||
async function loadEpisodes() {
|
||||
if (!state.selectedSeries || !state.selectedSeries.id) {
|
||||
throw new Error("Select a series first");
|
||||
}
|
||||
const data = await api(`/api/tvdb/series/${encodeURIComponent(state.selectedSeries.id)}/episodes?order_type=aired`);
|
||||
state.episodes = data.items || [];
|
||||
function episodeKey(episode) {
|
||||
const id = (episode && episode.id != null) ? String(episode.id).trim() : "";
|
||||
if (id) return id;
|
||||
return `${episode.season_number || ""}-${episode.episode_number || ""}-${episode.title || ""}`;
|
||||
}
|
||||
|
||||
function updateAddSelectedEpisodesControl() {
|
||||
if (!el.addSelectedEpisodesBtn) return;
|
||||
el.addSelectedEpisodesBtn.disabled = state.selectedEpisodeIds.size === 0;
|
||||
}
|
||||
|
||||
function clearEpisodeSelection() {
|
||||
state.selectedEpisodeIds = new Set();
|
||||
state.episodeSelectionAnchorId = null;
|
||||
renderEpisodesList();
|
||||
}
|
||||
|
||||
function renderEpisodesList() {
|
||||
el.episodesList.innerHTML = "";
|
||||
let previousSeasonKey = null;
|
||||
state.episodes.forEach((episode) => {
|
||||
@@ -678,7 +685,14 @@
|
||||
previousSeasonKey = seasonKey;
|
||||
}
|
||||
|
||||
const key = episodeKey(episode);
|
||||
const li = document.createElement("li");
|
||||
li.classList.add("episode-row");
|
||||
if (state.selectedEpisodeIds.has(key)) li.classList.add("selected");
|
||||
if (state.episodeSelectionAnchorId && state.episodeSelectionAnchorId === key) {
|
||||
li.classList.add("episode-anchor");
|
||||
}
|
||||
|
||||
const left = document.createElement("span");
|
||||
left.className = "episode-main";
|
||||
const title = document.createElement("div");
|
||||
@@ -694,30 +708,97 @@
|
||||
dateLine.textContent = `${future ? "Airing" : "Aired"} ${formattedDate}`;
|
||||
left.appendChild(dateLine);
|
||||
}
|
||||
|
||||
const right = document.createElement("div");
|
||||
right.appendChild(makeBtn("Add", async () => {
|
||||
const payload = {
|
||||
id: episode.id,
|
||||
series: state.selectedSeries.name,
|
||||
year: state.selectedSeries.year,
|
||||
season_number: episode.season_number,
|
||||
episode_number: episode.episode_number,
|
||||
title: episode.title,
|
||||
aired: episode.aired,
|
||||
label: episode.label,
|
||||
};
|
||||
await api(q("/api/session/selected-episodes"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items: [payload] }),
|
||||
});
|
||||
await loadSelectedEpisodes();
|
||||
}));
|
||||
li.appendChild(left);
|
||||
li.appendChild(right);
|
||||
|
||||
li.addEventListener("click", (event) => {
|
||||
handleEpisodeRowClick(key, event);
|
||||
});
|
||||
el.episodesList.appendChild(li);
|
||||
});
|
||||
updateAddSelectedEpisodesControl();
|
||||
}
|
||||
|
||||
function handleEpisodeRowClick(clickedId, event) {
|
||||
const clicked = String(clickedId || "").trim();
|
||||
if (!clicked) return;
|
||||
|
||||
const visibleEpisodeIds = (state.episodes || []).map((ep) => episodeKey(ep));
|
||||
const isShift = !!(event && event.shiftKey);
|
||||
const isToggle = !!(event && (event.ctrlKey || event.metaKey));
|
||||
|
||||
if (isShift) {
|
||||
const anchor = String(state.episodeSelectionAnchorId || "").trim();
|
||||
const from = visibleEpisodeIds.indexOf(anchor);
|
||||
const to = visibleEpisodeIds.indexOf(clicked);
|
||||
if (from >= 0 && to >= 0) {
|
||||
const start = Math.min(from, to);
|
||||
const end = Math.max(from, to);
|
||||
state.selectedEpisodeIds = new Set(visibleEpisodeIds.slice(start, end + 1));
|
||||
} else {
|
||||
state.selectedEpisodeIds = new Set([clicked]);
|
||||
}
|
||||
state.episodeSelectionAnchorId = clicked;
|
||||
renderEpisodesList();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isToggle) {
|
||||
if (state.selectedEpisodeIds.has(clicked)) {
|
||||
state.selectedEpisodeIds.delete(clicked);
|
||||
} else {
|
||||
state.selectedEpisodeIds.add(clicked);
|
||||
}
|
||||
state.episodeSelectionAnchorId = clicked;
|
||||
renderEpisodesList();
|
||||
return;
|
||||
}
|
||||
|
||||
state.selectedEpisodeIds = new Set([clicked]);
|
||||
state.episodeSelectionAnchorId = clicked;
|
||||
renderEpisodesList();
|
||||
}
|
||||
|
||||
async function addSelectedEpisodes() {
|
||||
const selectedIds = new Set(state.selectedEpisodeIds);
|
||||
if (!selectedIds.size) return;
|
||||
if (!state.selectedSeries || !state.selectedSeries.name) {
|
||||
throw new Error("Select a series first");
|
||||
}
|
||||
|
||||
const items = (state.episodes || [])
|
||||
.filter((episode) => selectedIds.has(episodeKey(episode)))
|
||||
.map((episode) => ({
|
||||
id: episode.id,
|
||||
series: state.selectedSeries.name,
|
||||
year: state.selectedSeries.year,
|
||||
season_number: episode.season_number,
|
||||
episode_number: episode.episode_number,
|
||||
title: episode.title,
|
||||
aired: episode.aired,
|
||||
label: episode.label,
|
||||
}));
|
||||
|
||||
if (!items.length) return;
|
||||
|
||||
await api(q("/api/session/selected-episodes"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items }),
|
||||
});
|
||||
|
||||
clearEpisodeSelection();
|
||||
await loadSelectedEpisodes();
|
||||
}
|
||||
|
||||
async function loadEpisodes() {
|
||||
if (!state.selectedSeries || !state.selectedSeries.id) {
|
||||
throw new Error("Select a series first");
|
||||
}
|
||||
const data = await api(`/api/tvdb/series/${encodeURIComponent(state.selectedSeries.id)}/episodes?order_type=aired`);
|
||||
state.episodes = data.items || [];
|
||||
state.selectedEpisodeIds = new Set();
|
||||
state.episodeSelectionAnchorId = null;
|
||||
renderEpisodesList();
|
||||
out("Episodes loaded", data);
|
||||
}
|
||||
|
||||
@@ -1051,6 +1132,9 @@
|
||||
if (e.target === el.settingsModal) closeSettingsModal();
|
||||
});
|
||||
el.refreshEpisodesBtn.addEventListener("click", () => withHandler(loadEpisodes, el.refreshEpisodesBtn));
|
||||
el.addSelectedEpisodesBtn.addEventListener("click", () =>
|
||||
withHandler(addSelectedEpisodes, el.addSelectedEpisodesBtn)
|
||||
);
|
||||
|
||||
el.refreshSelectedEpisodesBtn.addEventListener("click", () => withHandler(loadSelectedEpisodes, el.refreshSelectedEpisodesBtn));
|
||||
el.clearSelectedEpisodesBtn.addEventListener("click", () =>
|
||||
|
||||
@@ -343,6 +343,10 @@ button.secondary {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#episodesList li.episode-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#episodesList .episode-title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -359,6 +363,10 @@ button.secondary {
|
||||
color: var(--button-primary-bg);
|
||||
}
|
||||
|
||||
#episodesList li.episode-row.episode-anchor {
|
||||
box-shadow: inset 0 0 0 1px var(--button-primary-border);
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
|
||||
Reference in New Issue
Block a user