feat(plex): Playlists Phase 1 — per-user ordered watch-lists (Siftlode-native)
Personal ordered lists of Plex items, kept in Siftlode's own DB (works for users without a Plex account, like watch-state) — the "your own lists" counterpart to shared collections. Plex-direction sync is a later phase (plex_rating_key reserved). Backend: migration 0048 (plex_playlists + plex_playlist_items with position), PlexPlaylist/PlexPlaylistItem models, and per-user CRUD endpoints under /api/plex/playlists (list [+?contains for the add dialog], create [seeded], detail [ordered cards], rename, delete, add/remove item, reorder). _leaf_card handles the movie/episode mix. Frontend: "Add to playlist" dialog from the movie info page (all users), a Playlists section in PlexSidebar (list + create), PlexPlaylistView (reorder up/down, remove, rename, delete, Play all), and PlexPlayer gained an optional `queue` so play-through follows the list order (prev/next + auto-advance). i18n en/hu/de. Verified end-to-end on localdev (backend CRUD + the create→add→view→play-through UI flow).
This commit is contained in:
parent
1fd3003038
commit
25197ed817
16 changed files with 796 additions and 18 deletions
|
|
@ -9,12 +9,14 @@ import { useHistorySubview } from "../lib/history";
|
|||
// Lazy: the rich player (pulls in hls.js) loads only when something is first played.
|
||||
const PlexPlayer = lazy(() => import("./PlexPlayer"));
|
||||
const PlexInfo = lazy(() => import("./PlexInfo"));
|
||||
const PlexPlaylistView = lazy(() => import("./PlexPlaylistView"));
|
||||
|
||||
type Sub =
|
||||
| { kind: "grid" }
|
||||
| { kind: "show"; id: string }
|
||||
| { kind: "player"; id: string }
|
||||
| { kind: "info"; id: string };
|
||||
| { kind: "player"; id: string; queue?: string[] }
|
||||
| { kind: "info"; id: string }
|
||||
| { kind: "playlist"; id: number };
|
||||
|
||||
// The Plex module's content area: browse/search the mirrored library as poster cards, drill from a
|
||||
// show into seasons/episodes. Library scope / watch-state / sort live in the left PlexSidebar; the
|
||||
|
|
@ -30,6 +32,9 @@ type Props = {
|
|||
sort: string;
|
||||
filters: PlexFilters;
|
||||
setFilters: (f: PlexFilters) => void;
|
||||
// The sidebar opens a playlist by setting this; PlexBrowse turns it into a history subview + clears it.
|
||||
openPlaylist?: number | null;
|
||||
onPlaylistOpened?: () => void;
|
||||
};
|
||||
|
||||
const PAGE = 40;
|
||||
|
|
@ -41,12 +46,32 @@ function dur(n?: number | null): string {
|
|||
return h ? `${h}h ${m}m` : `${m}m`;
|
||||
}
|
||||
|
||||
export default function PlexBrowse({ q, onClearSearch, library, show, sort, filters, setFilters }: Props) {
|
||||
export default function PlexBrowse({
|
||||
q,
|
||||
onClearSearch,
|
||||
library,
|
||||
show,
|
||||
sort,
|
||||
filters,
|
||||
setFilters,
|
||||
openPlaylist,
|
||||
onPlaylistOpened,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
const dq = useDebounced(q.trim(), 350);
|
||||
const sub = useHistorySubview<Sub>({ kind: "grid" });
|
||||
|
||||
// Sidebar asked to open a playlist → push it as a subview (browser Back returns to the grid), then
|
||||
// clear the signal so it doesn't re-open.
|
||||
useEffect(() => {
|
||||
if (openPlaylist != null) {
|
||||
sub.open({ kind: "playlist", id: openPlaylist });
|
||||
onPlaylistOpened?.();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [openPlaylist]);
|
||||
|
||||
// Opening a show/player replaces the grid entirely (the component returns early below), so the
|
||||
// scroll container resets to the top. Remember where the grid was scrolled when leaving it, and
|
||||
// restore it when we come back — so the browser/mouse Back from the player lands on the same card
|
||||
|
|
@ -137,7 +162,19 @@ export default function PlexBrowse({ q, onClearSearch, library, show, sort, filt
|
|||
if (sub.view.kind === "player") {
|
||||
return (
|
||||
<Suspense fallback={<div className="fixed inset-0 z-50 bg-black" />}>
|
||||
<PlexPlayer itemId={sub.view.id} onClose={sub.back} />
|
||||
<PlexPlayer itemId={sub.view.id} queue={sub.view.queue} onClose={sub.back} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
if (sub.view.kind === "playlist") {
|
||||
const pid = sub.view.id;
|
||||
return (
|
||||
<Suspense fallback={<p className="p-8 text-muted text-sm">{t("plex.loading")}</p>}>
|
||||
<PlexPlaylistView
|
||||
playlistId={pid}
|
||||
onBack={sub.back}
|
||||
onPlay={(itemRk, queue) => sub.open({ kind: "player", id: itemRk, queue })}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue