serias categories favs
This commit is contained in:
parent
b8df5d0997
commit
28c5cb8c0c
@ -24,6 +24,8 @@
|
||||
favoriteLiveCategoryStreamsById: {},
|
||||
expandedFavoriteVodCategoryById: {},
|
||||
favoriteVodCategoryStreamsById: {},
|
||||
expandedFavoriteSeriesCategoryById: {},
|
||||
favoriteSeriesCategoryItemsById: {},
|
||||
currentLiveEpgStreamId: null,
|
||||
currentStreamInfo: null
|
||||
};
|
||||
@ -60,6 +62,7 @@
|
||||
seriesCategory: document.getElementById("series-category"),
|
||||
seriesSearch: document.getElementById("series-search"),
|
||||
seriesRefresh: document.getElementById("series-refresh"),
|
||||
seriesFavoriteCategory: document.getElementById("series-favorite-category"),
|
||||
seriesList: document.getElementById("series-list"),
|
||||
|
||||
customForm: document.getElementById("custom-form"),
|
||||
@ -283,9 +286,20 @@
|
||||
}
|
||||
|
||||
function bindSeriesTab() {
|
||||
el.seriesCategory.addEventListener("change", () => loadSeriesList().catch(showError));
|
||||
el.seriesCategory.addEventListener("change", () => {
|
||||
loadSeriesList().catch(showError);
|
||||
updateSeriesCategoryFavoriteButton();
|
||||
});
|
||||
el.seriesSearch.addEventListener("input", renderSeriesList);
|
||||
el.seriesRefresh.addEventListener("click", () => loadSeriesData().catch(showError));
|
||||
el.seriesFavoriteCategory.addEventListener("click", () => {
|
||||
const favorite = selectedSeriesCategoryFavorite();
|
||||
if (!favorite) {
|
||||
setSettingsMessage("Select a series category first.", "err");
|
||||
return;
|
||||
}
|
||||
toggleFavorite(favorite).catch(showError);
|
||||
});
|
||||
}
|
||||
|
||||
function bindCustomTab() {
|
||||
@ -410,6 +424,8 @@
|
||||
state.favoriteLiveCategoryStreamsById = {};
|
||||
state.expandedFavoriteVodCategoryById = {};
|
||||
state.favoriteVodCategoryStreamsById = {};
|
||||
state.expandedFavoriteSeriesCategoryById = {};
|
||||
state.favoriteSeriesCategoryItemsById = {};
|
||||
}
|
||||
|
||||
async function loadLiveData() {
|
||||
@ -599,9 +615,39 @@
|
||||
const categoriesPayload = await apiJson("/api/library/categories?type=series");
|
||||
state.seriesCategories = sanitizeCategories(categoriesPayload.items);
|
||||
fillCategorySelect(el.seriesCategory, state.seriesCategories, "All series");
|
||||
updateSeriesCategoryFavoriteButton();
|
||||
await loadSeriesList();
|
||||
}
|
||||
|
||||
function selectedSeriesCategoryFavorite() {
|
||||
const categoryId = String(el.seriesCategory.value || "").trim();
|
||||
if (!categoryId) {
|
||||
return null;
|
||||
}
|
||||
const category = state.seriesCategories.find((item) => String(item?.category_id || "") === categoryId);
|
||||
if (!category) {
|
||||
return null;
|
||||
}
|
||||
return makeFavoriteSeriesCategory(category);
|
||||
}
|
||||
|
||||
function updateSeriesCategoryFavoriteButton() {
|
||||
const favorite = selectedSeriesCategoryFavorite();
|
||||
if (!favorite) {
|
||||
el.seriesFavoriteCategory.disabled = true;
|
||||
el.seriesFavoriteCategory.textContent = "☆";
|
||||
el.seriesFavoriteCategory.title = "Select category";
|
||||
el.seriesFavoriteCategory.setAttribute("aria-label", "Select category");
|
||||
return;
|
||||
}
|
||||
el.seriesFavoriteCategory.disabled = false;
|
||||
const active = isFavorite(favorite.key);
|
||||
el.seriesFavoriteCategory.textContent = favoriteIcon(active);
|
||||
const label = active ? "Remove category from favorites" : "Add category to favorites";
|
||||
el.seriesFavoriteCategory.title = label;
|
||||
el.seriesFavoriteCategory.setAttribute("aria-label", label);
|
||||
}
|
||||
|
||||
async function loadSeriesList() {
|
||||
ensureLibraryReady();
|
||||
const query = new URLSearchParams({type: "series"});
|
||||
@ -826,6 +872,7 @@
|
||||
renderFavorites();
|
||||
updateLiveCategoryFavoriteButton();
|
||||
updateVodCategoryFavoriteButton();
|
||||
updateSeriesCategoryFavoriteButton();
|
||||
}
|
||||
|
||||
function renderFavorites() {
|
||||
@ -846,19 +893,23 @@
|
||||
filtered.forEach((favorite) => {
|
||||
const li = document.createElement("li");
|
||||
li.className = "stream-item";
|
||||
const isSeriesCategory = favorite?.mode === "series_item";
|
||||
const isSeriesItem = favorite?.mode === "series_item";
|
||||
const isLiveCategory = favorite?.mode === "live_category";
|
||||
const isVodCategory = favorite?.mode === "vod_category";
|
||||
const isSeriesCategory = favorite?.mode === "series_category";
|
||||
const seriesId = String(favorite?.id || "");
|
||||
const isExpanded = isSeriesCategory && Boolean(state.expandedFavoriteSeriesById[seriesId]);
|
||||
const isSeriesItemExpanded = isSeriesItem && Boolean(state.expandedFavoriteSeriesById[seriesId]);
|
||||
const liveCategoryId = String(favorite?.id || "");
|
||||
const isLiveCategoryExpanded = isLiveCategory && Boolean(state.expandedFavoriteLiveCategoryById[liveCategoryId]);
|
||||
const vodCategoryId = String(favorite?.id || "");
|
||||
const isVodCategoryExpanded = isVodCategory && Boolean(state.expandedFavoriteVodCategoryById[vodCategoryId]);
|
||||
li.innerHTML = isSeriesCategory
|
||||
const seriesCategoryId = String(favorite?.id || "");
|
||||
const isSeriesCategoryExpanded = isSeriesCategory
|
||||
&& Boolean(state.expandedFavoriteSeriesCategoryById[seriesCategoryId]);
|
||||
li.innerHTML = isSeriesItem
|
||||
? `
|
||||
<div>
|
||||
<button type="button" class="stream-title stream-link" data-action="toggle-favorite-series">${isExpanded ? "Hide" : "Show"} ${esc(favorite.title || "Untitled")}</button>
|
||||
<button type="button" class="stream-title stream-link" data-action="toggle-favorite-series">${isSeriesItemExpanded ? "Hide" : "Show"} ${esc(favorite.title || "Untitled")}</button>
|
||||
<div class="stream-meta">${esc(favoriteSummary(favorite))}</div>
|
||||
</div>
|
||||
<div class="stream-actions">
|
||||
@ -882,9 +933,19 @@
|
||||
<div class="stream-meta">${esc(favoriteSummary(favorite))}</div>
|
||||
</div>
|
||||
<div class="stream-actions">
|
||||
<button type="button" data-action="remove-favorite" class="danger">Remove</button>
|
||||
</div>
|
||||
`
|
||||
<button type="button" data-action="remove-favorite" class="danger">Remove</button>
|
||||
</div>
|
||||
`
|
||||
: isSeriesCategory
|
||||
? `
|
||||
<div>
|
||||
<button type="button" class="stream-title stream-link" data-action="toggle-favorite-series-category">${isSeriesCategoryExpanded ? "Hide" : "Show"} ${esc(favorite.title || "Untitled")}</button>
|
||||
<div class="stream-meta">${esc(favoriteSummary(favorite))}</div>
|
||||
</div>
|
||||
<div class="stream-actions">
|
||||
<button type="button" data-action="remove-favorite" class="danger">Remove</button>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div>
|
||||
<button type="button" class="stream-title stream-link" data-action="open-favorite">${esc(favorite.title || "Untitled")}</button>
|
||||
@ -894,7 +955,7 @@
|
||||
<button type="button" data-action="remove-favorite" class="danger">Remove</button>
|
||||
</div>
|
||||
`;
|
||||
if (isSeriesCategory) {
|
||||
if (isSeriesItem) {
|
||||
li.querySelector("button[data-action='toggle-favorite-series']").addEventListener("click", () => {
|
||||
toggleFavoriteSeriesItem(favorite).catch(showError);
|
||||
});
|
||||
@ -906,6 +967,10 @@
|
||||
li.querySelector("button[data-action='toggle-favorite-vod-category']").addEventListener("click", () => {
|
||||
toggleFavoriteVodCategory(favorite).catch(showError);
|
||||
});
|
||||
} else if (isSeriesCategory) {
|
||||
li.querySelector("button[data-action='toggle-favorite-series-category']").addEventListener("click", () => {
|
||||
toggleFavoriteSeriesCategory(favorite).catch(showError);
|
||||
});
|
||||
} else {
|
||||
li.querySelector("button[data-action='open-favorite']").addEventListener("click", () => {
|
||||
openFavorite(favorite).catch(showError);
|
||||
@ -926,6 +991,10 @@
|
||||
delete state.expandedFavoriteVodCategoryById[vodCategoryId];
|
||||
delete state.favoriteVodCategoryStreamsById[vodCategoryId];
|
||||
}
|
||||
if (favorite?.mode === "series_category") {
|
||||
delete state.expandedFavoriteSeriesCategoryById[seriesCategoryId];
|
||||
delete state.favoriteSeriesCategoryItemsById[seriesCategoryId];
|
||||
}
|
||||
renderFavorites();
|
||||
renderLiveStreams();
|
||||
renderVodStreams();
|
||||
@ -935,7 +1004,7 @@
|
||||
});
|
||||
el.favoritesList.appendChild(li);
|
||||
|
||||
if (!isSeriesCategory && !isLiveCategory && !isVodCategory) {
|
||||
if (!isSeriesItem && !isLiveCategory && !isVodCategory && !isSeriesCategory) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -945,7 +1014,10 @@
|
||||
if (isVodCategory && !isVodCategoryExpanded) {
|
||||
return;
|
||||
}
|
||||
if (isSeriesCategory && !isExpanded) {
|
||||
if (isSeriesItem && !isSeriesItemExpanded) {
|
||||
return;
|
||||
}
|
||||
if (isSeriesCategory && !isSeriesCategoryExpanded) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -953,7 +1025,9 @@
|
||||
? state.favoriteLiveCategoryStreamsById[liveCategoryId]
|
||||
: isVodCategory
|
||||
? state.favoriteVodCategoryStreamsById[vodCategoryId]
|
||||
: state.favoriteSeriesEpisodesById[seriesId];
|
||||
: isSeriesCategory
|
||||
? state.favoriteSeriesCategoryItemsById[seriesCategoryId]
|
||||
: state.favoriteSeriesEpisodesById[seriesId];
|
||||
const episodesLi = document.createElement("li");
|
||||
episodesLi.className = "card episodes series-inline-episodes";
|
||||
|
||||
@ -1033,6 +1107,106 @@
|
||||
el.favoritesList.appendChild(episodesLi);
|
||||
return;
|
||||
}
|
||||
if (isSeriesCategory) {
|
||||
const seriesItems = Array.isArray(episodesEntry.episodes) ? episodesEntry.episodes : [];
|
||||
seriesItems.forEach((seriesItem) => {
|
||||
const seriesItemFavorite = makeFavoriteSeriesItem(seriesItem);
|
||||
const localSeriesId = String(seriesItem?.series_id || "");
|
||||
const isLocalExpanded = Boolean(state.expandedFavoriteSeriesById[localSeriesId]);
|
||||
const row = document.createElement("div");
|
||||
row.className = "stream-item";
|
||||
row.innerHTML = `
|
||||
<div>
|
||||
<button type="button" class="stream-title stream-link" data-action="toggle-favorite-series">${isLocalExpanded ? "Hide episodes" : "Episodes"} ${esc(seriesItem?.name || "Untitled")}</button>
|
||||
<div class="stream-meta">Series ID: ${esc(localSeriesId)}</div>
|
||||
</div>
|
||||
<div class="stream-actions">
|
||||
<button type="button" class="favorite-toggle" data-action="toggle-favorite" aria-label="${isFavorite(seriesItemFavorite.key) ? "Remove from favorites" : "Add to favorites"}" title="${isFavorite(seriesItemFavorite.key) ? "Remove from favorites" : "Add to favorites"}">${favoriteIcon(isFavorite(seriesItemFavorite.key))}</button>
|
||||
</div>
|
||||
`;
|
||||
row.querySelector("button[data-action='toggle-favorite-series']").addEventListener("click", () => {
|
||||
toggleFavoriteSeriesItem(seriesItemFavorite).catch(showError);
|
||||
});
|
||||
row.querySelector("button[data-action='toggle-favorite']").addEventListener("click", () => {
|
||||
toggleFavorite(seriesItemFavorite).then(() => renderFavorites()).catch(showError);
|
||||
});
|
||||
wrap.appendChild(row);
|
||||
|
||||
if (!isLocalExpanded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const seriesEntry = state.favoriteSeriesEpisodesById[localSeriesId];
|
||||
const episodesBlock = document.createElement("div");
|
||||
episodesBlock.className = "season-list";
|
||||
if (!seriesEntry || seriesEntry.loading) {
|
||||
episodesBlock.innerHTML = `<div class="muted">Loading episodes...</div>`;
|
||||
wrap.appendChild(episodesBlock);
|
||||
return;
|
||||
}
|
||||
if (seriesEntry.error) {
|
||||
episodesBlock.innerHTML = `<div class="danger">Unable to load episodes: ${esc(seriesEntry.error)}</div>`;
|
||||
wrap.appendChild(episodesBlock);
|
||||
return;
|
||||
}
|
||||
const groupedBySeason = groupEpisodesBySeason(seriesEntry.episodes);
|
||||
groupedBySeason.forEach((group) => {
|
||||
const isSeasonExpanded = Boolean(state.expandedSeasonByFavoriteSeries?.[localSeriesId]?.[group.season]);
|
||||
const seasonBlock = document.createElement("div");
|
||||
seasonBlock.className = "season-block";
|
||||
seasonBlock.innerHTML = `
|
||||
<button type="button" class="season-title season-toggle" data-action="toggle-favorite-season">
|
||||
${isSeasonExpanded ? "Hide" : "Show"} Season ${esc(group.season)} (${group.episodes.length})
|
||||
</button>
|
||||
`;
|
||||
seasonBlock.querySelector("button[data-action='toggle-favorite-season']").addEventListener("click", () => {
|
||||
toggleFavoriteSeasonGroup(localSeriesId, group.season);
|
||||
});
|
||||
|
||||
if (!isSeasonExpanded) {
|
||||
episodesBlock.appendChild(seasonBlock);
|
||||
return;
|
||||
}
|
||||
|
||||
const seasonList = document.createElement("div");
|
||||
seasonList.className = "season-list";
|
||||
group.episodes.forEach((episode) => {
|
||||
const episodeFavorite = makeFavoriteSeriesEpisode(
|
||||
{name: seriesItem?.name || "Series", series_id: localSeriesId},
|
||||
episode
|
||||
);
|
||||
const episodeRow = document.createElement("div");
|
||||
episodeRow.className = "stream-item";
|
||||
episodeRow.innerHTML = `
|
||||
<div>
|
||||
<button type="button" class="stream-title stream-link" data-action="play-title">S${esc(episode.season)}E${esc(episode.episodeNum)} - ${esc(episode.title)}</button>
|
||||
<div class="stream-meta">Episode ID: ${esc(episode.id)} | Ext: ${esc(episode.ext)}</div>
|
||||
</div>
|
||||
<div class="stream-actions">
|
||||
<button type="button" class="favorite-toggle" data-action="toggle-favorite" aria-label="${isFavorite(episodeFavorite.key) ? "Remove from favorites" : "Add to favorites"}" title="${isFavorite(episodeFavorite.key) ? "Remove from favorites" : "Add to favorites"}">${favoriteIcon(isFavorite(episodeFavorite.key))}</button>
|
||||
</div>
|
||||
`;
|
||||
episodeRow.querySelector("button[data-action='play-title']").addEventListener("click", () => {
|
||||
playXtream("series", episode.id, episode.ext, `${seriesItem?.name || "Series"} - ${episode.title}`, null, {
|
||||
seriesId: localSeriesId,
|
||||
season: episode.season,
|
||||
episode: episode.episodeNum
|
||||
}).catch(showError);
|
||||
});
|
||||
episodeRow.querySelector("button[data-action='toggle-favorite']").addEventListener("click", () => {
|
||||
toggleFavorite(episodeFavorite).then(() => renderFavorites()).catch(showError);
|
||||
});
|
||||
seasonList.appendChild(episodeRow);
|
||||
});
|
||||
seasonBlock.appendChild(seasonList);
|
||||
episodesBlock.appendChild(seasonBlock);
|
||||
});
|
||||
wrap.appendChild(episodesBlock);
|
||||
});
|
||||
episodesLi.appendChild(wrap);
|
||||
el.favoritesList.appendChild(episodesLi);
|
||||
return;
|
||||
}
|
||||
|
||||
const groupedBySeason = groupEpisodesBySeason(episodesEntry.episodes);
|
||||
groupedBySeason.forEach((group) => {
|
||||
@ -1138,6 +1312,9 @@
|
||||
case "vod_category":
|
||||
await toggleFavoriteVodCategory(favorite);
|
||||
break;
|
||||
case "series_category":
|
||||
await toggleFavoriteSeriesCategory(favorite);
|
||||
break;
|
||||
case "custom":
|
||||
playCustom(favorite.title || "Custom stream", String(favorite.url || ""));
|
||||
break;
|
||||
@ -1279,6 +1456,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleFavoriteSeriesCategory(favorite) {
|
||||
const categoryId = String(favorite?.id || "");
|
||||
if (!categoryId) {
|
||||
throw new Error("Missing series category id.");
|
||||
}
|
||||
if (state.expandedFavoriteSeriesCategoryById[categoryId]) {
|
||||
delete state.expandedFavoriteSeriesCategoryById[categoryId];
|
||||
renderFavorites();
|
||||
return;
|
||||
}
|
||||
state.expandedFavoriteSeriesCategoryById[categoryId] = true;
|
||||
renderFavorites();
|
||||
await ensureFavoriteSeriesCategoryItemsLoaded(categoryId);
|
||||
}
|
||||
|
||||
async function ensureFavoriteSeriesCategoryItemsLoaded(categoryId) {
|
||||
ensureLibraryReady();
|
||||
if (state.favoriteSeriesCategoryItemsById[categoryId]?.loaded
|
||||
|| state.favoriteSeriesCategoryItemsById[categoryId]?.loading) {
|
||||
return;
|
||||
}
|
||||
state.favoriteSeriesCategoryItemsById[categoryId] = {loading: true, loaded: false, episodes: []};
|
||||
renderFavorites();
|
||||
try {
|
||||
const query = new URLSearchParams({type: "series", category_id: categoryId});
|
||||
const payload = await apiJson(`/api/library/items?${query.toString()}`);
|
||||
state.favoriteSeriesCategoryItemsById[categoryId] = {
|
||||
loading: false,
|
||||
loaded: true,
|
||||
episodes: sanitizeSeriesItems(payload.items)
|
||||
};
|
||||
} catch (error) {
|
||||
state.favoriteSeriesCategoryItemsById[categoryId] = {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
error: error.message || String(error),
|
||||
episodes: []
|
||||
};
|
||||
throw error;
|
||||
} finally {
|
||||
renderFavorites();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFavoriteSeasonGroup(seriesId, season) {
|
||||
const current = state.expandedSeasonByFavoriteSeries[seriesId] || {};
|
||||
const seasonKey = String(season || "?");
|
||||
@ -1301,6 +1522,9 @@
|
||||
if (mode === "vod_category") {
|
||||
return `VOD category | ID: ${favorite.id || "-"}`;
|
||||
}
|
||||
if (mode === "series_category") {
|
||||
return `Series category | ID: ${favorite.id || "-"}`;
|
||||
}
|
||||
if (mode === "series_episode") {
|
||||
return `Series episode | ID: ${favorite.id || "-"} | Ext: ${favorite.ext || "mp4"}`;
|
||||
}
|
||||
@ -1357,6 +1581,16 @@
|
||||
};
|
||||
}
|
||||
|
||||
function makeFavoriteSeriesCategory(category) {
|
||||
const categoryId = String(category?.category_id || "");
|
||||
return {
|
||||
key: `series-category:${categoryId}`,
|
||||
mode: "series_category",
|
||||
id: categoryId,
|
||||
title: String(category?.category_name || `Category ${categoryId}`)
|
||||
};
|
||||
}
|
||||
|
||||
function makeFavoriteSeriesItem(item) {
|
||||
const seriesId = String(item?.series_id || "");
|
||||
return {
|
||||
@ -1407,6 +1641,7 @@
|
||||
state.favorites = state.favorites.filter((item) => item?.key !== key);
|
||||
updateLiveCategoryFavoriteButton();
|
||||
updateVodCategoryFavoriteButton();
|
||||
updateSeriesCategoryFavoriteButton();
|
||||
}
|
||||
|
||||
function renderCustomStreams() {
|
||||
@ -1993,6 +2228,7 @@
|
||||
fillCategorySelect(el.seriesCategory, state.seriesCategories, "All series");
|
||||
updateLiveCategoryFavoriteButton();
|
||||
updateVodCategoryFavoriteButton();
|
||||
updateSeriesCategoryFavoriteButton();
|
||||
renderLiveStreams();
|
||||
renderVodStreams();
|
||||
renderSeriesList();
|
||||
|
||||
@ -112,7 +112,10 @@
|
||||
Search
|
||||
<input id="series-search" type="search" placeholder="Series title">
|
||||
</label>
|
||||
<button id="series-refresh">Reload</button>
|
||||
<div class="actions">
|
||||
<button id="series-refresh">Reload</button>
|
||||
<button id="series-favorite-category" class="favorite-toggle" type="button" title="Add selected category to favorites" aria-label="Add selected category to favorites">☆</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul id="series-list" class="stream-list"></ul>
|
||||
</article>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user