feat(plex): expand shows to episodes so a show-collection's Play-all plays (S4)
A collection's Play-all/Shuffle queue held only playable leaves (movies/episodes), filtering out shows — so on a show-collection the queue was empty and Play-all was hidden/dead. Build the queue by expanding each show member into its episodes (season then episode order), concatenated in member order; leaves still queue as themselves. Shows are fetched lazily on click (parallel plexShow fetches), not on page load. Gate Play-all/Shuffle and the poster on "has any playable member" (leaves OR shows), and disable them while the queue builds. Scope: option A — no member manual-order migration. Adds plex.collection.playFailed (HU/EN).
This commit is contained in:
@@ -57,6 +57,9 @@ export default function PlexCollectionView({
|
||||
const [name, setName] = useState("");
|
||||
const [adding, setAdding] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
// Building the play queue can await show → episode expansion (see buildQueue); disable Play/Shuffle
|
||||
// meanwhile so a double-tap can't fire two queues.
|
||||
const [queueBusy, setQueueBusy] = useState(false);
|
||||
|
||||
const invalidate = () => {
|
||||
qc.invalidateQueries({ queryKey: qk.plexCollections() });
|
||||
@@ -64,8 +67,45 @@ export default function PlexCollectionView({
|
||||
};
|
||||
|
||||
const items = d?.items ?? [];
|
||||
// The Play queue is the playable-leaf members (movies/episodes) in order — a show can't queue directly.
|
||||
const playable = items.filter((c) => c.type !== "show").map((c) => c.id);
|
||||
// Movies/episodes are playable leaves that queue as themselves; a show can't queue directly, so
|
||||
// Play-all expands it into its episodes (buildQueue). Either kind means there's something to play —
|
||||
// this is what a show-collection needs: before, `playable` held only leaves, so a show-collection's
|
||||
// Play-all was empty and did nothing.
|
||||
const leafKeys = items.filter((c) => c.type !== "show").map((c) => c.id);
|
||||
const hasShows = items.some((c) => c.type === "show");
|
||||
const canPlay = leafKeys.length > 0 || hasShows;
|
||||
|
||||
// Build the ordered play queue: leaf members as themselves, each show member expanded into its
|
||||
// episodes (season then episode order). Shows are fetched lazily HERE (on Play-all/Shuffle click),
|
||||
// in parallel, but concatenated in member order so the queue follows the grid — not on page load,
|
||||
// which would fetch every show up front for a button that may never be pressed.
|
||||
async function buildQueue(): Promise<string[]> {
|
||||
const perMember = await Promise.all(
|
||||
items.map(async (c) => {
|
||||
if (c.type !== "show") return [c.id];
|
||||
const detail = await qc.fetchQuery({
|
||||
queryKey: qk.plexShow(c.id),
|
||||
queryFn: () => api.plexShow(c.id),
|
||||
});
|
||||
return detail.seasons.flatMap((s) => s.episodes.map((e) => e.id));
|
||||
}),
|
||||
);
|
||||
return perMember.flat();
|
||||
}
|
||||
|
||||
async function playAll(shuffle: boolean) {
|
||||
setQueueBusy(true);
|
||||
try {
|
||||
let queue = await buildQueue();
|
||||
if (shuffle) queue = shuffled(queue);
|
||||
if (queue.length > 0) onPlay(queue[0]!, queue);
|
||||
} catch (e) {
|
||||
notifyActionError(e, t("plex.collection.playFailed"));
|
||||
} finally {
|
||||
setQueueBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const canEdit = isAdmin && !!d?.can_edit;
|
||||
// A plain (non-smart, non-external) collection can be admin-enabled for editing. Shares
|
||||
// isIntrinsicReadOnly with the browse tile so the lock/edit affordance can't drift between them.
|
||||
@@ -171,9 +211,9 @@ export default function PlexCollectionView({
|
||||
<DetailHero
|
||||
thumb={d.thumb ?? `/api/plex/image/${d.id}`}
|
||||
posterTitle={t("plex.play")}
|
||||
posterDisabled={playable.length === 0}
|
||||
showPlayOverlay={playable.length > 0}
|
||||
onPosterClick={() => playable.length > 0 && onPlay(playable[0]!, playable)}
|
||||
posterDisabled={!canPlay || queueBusy}
|
||||
showPlayOverlay={canPlay}
|
||||
onPosterClick={() => canPlay && !queueBusy && playAll(false)}
|
||||
customizeMenu={null}
|
||||
>
|
||||
{renaming ? (
|
||||
@@ -232,19 +272,18 @@ export default function PlexCollectionView({
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{playable.length > 0 && (
|
||||
{canPlay && (
|
||||
<>
|
||||
<ActionBtn
|
||||
primary
|
||||
onClick={() => onPlay(playable[0]!, playable)}
|
||||
onClick={() => playAll(false)}
|
||||
disabled={queueBusy}
|
||||
icon={Play}
|
||||
label={t("plex.collection.playAll")}
|
||||
/>
|
||||
<ActionBtn
|
||||
onClick={() => {
|
||||
const order = shuffled(playable);
|
||||
onPlay(order[0]!, order);
|
||||
}}
|
||||
onClick={() => playAll(true)}
|
||||
disabled={queueBusy}
|
||||
icon={Shuffle}
|
||||
label={t("plex.collection.shuffle")}
|
||||
/>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"added": "Added",
|
||||
"deleteConfirm": "Delete the collection \"{{title}}\"? This removes it from Plex too.",
|
||||
"editFailed": "Collection edit failed",
|
||||
"playFailed": "Couldn't build the play queue — try again.",
|
||||
"newCollection": "New collection",
|
||||
"namePlaceholder": "Collection name",
|
||||
"nameTaken": "A collection with this name already exists.",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"added": "Hozzáadva",
|
||||
"deleteConfirm": "Törlöd a(z) „{{title}}” kollekciót? Ez a Plexből is eltávolítja.",
|
||||
"editFailed": "A kollekció szerkesztése sikertelen",
|
||||
"playFailed": "Nem sikerült összeállítani a lejátszási sort — próbáld újra.",
|
||||
"newCollection": "Új kollekció",
|
||||
"namePlaceholder": "Kollekció neve",
|
||||
"nameTaken": "Már létezik ilyen nevű kollekció.",
|
||||
|
||||
Reference in New Issue
Block a user