refactor(plex): scope module state in PlexProvider
Lift the Plex module's shared local state (scope / watch-state / sort / expanded filters / search term / open-playlist hand-off) out of App into a split-context PlexProvider with stable useCallback setters. PlexSidebar, PlexBrowse, and the header search box read it via usePlex()/usePlexActions() instead of props drilled through App; PlexBrowse is now prop-free. App loses six useStates + the filters JSON memo/serialise helpers. (Platform S6b.2)
This commit is contained in:
+1
-48
@@ -3,11 +3,9 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
api,
|
api,
|
||||||
EMPTY_PLEX_FILTERS,
|
|
||||||
getActiveAccount,
|
getActiveAccount,
|
||||||
setActiveAccount,
|
setActiveAccount,
|
||||||
setUnauthorizedHandler,
|
setUnauthorizedHandler,
|
||||||
type PlexFilters,
|
|
||||||
} from "./lib/api";
|
} from "./lib/api";
|
||||||
import { setLanguage, isSupported, type LangCode } from "./i18n";
|
import { setLanguage, isSupported, type LangCode } from "./i18n";
|
||||||
import {
|
import {
|
||||||
@@ -156,30 +154,6 @@ export default function App() {
|
|||||||
: channelsViewRaw === "blocked"
|
: channelsViewRaw === "blocked"
|
||||||
? "blocked"
|
? "blocked"
|
||||||
: "subscribed";
|
: "subscribed";
|
||||||
// Plex module filters (its own left-sidebar filter section) — per-account persisted.
|
|
||||||
// The former per-library picker is now a cross-library SCOPE: movie | show | both (unified library).
|
|
||||||
// (Reuses the old LS.plexLibrary key; an old stored library-id value falls back to "both".)
|
|
||||||
const [plexScopeRaw, setPlexScope] = useAccountPersistedState(LS.plexLibrary, "both");
|
|
||||||
const plexScope = ["movie", "show", "both"].includes(plexScopeRaw) ? plexScopeRaw : "both";
|
|
||||||
const [plexShowFilter, setPlexShowFilter] = useAccountPersistedState(LS.plexShow, "all");
|
|
||||||
const [plexSort, setPlexSort] = useAccountPersistedState(LS.plexSort, "title");
|
|
||||||
const [plexPlaylistOpen, setPlexPlaylistOpen] = useState<number | null>(null); // sidebar → open a playlist
|
|
||||||
// The expanded Plex filters live as one JSON blob (the string-only account store serializes it).
|
|
||||||
const [plexFiltersRaw, setPlexFiltersRaw] = useAccountPersistedState(LS.plexFilters, "{}");
|
|
||||||
const plexFilters = useMemo<PlexFilters>(() => {
|
|
||||||
try {
|
|
||||||
return { ...EMPTY_PLEX_FILTERS, ...JSON.parse(plexFiltersRaw) };
|
|
||||||
} catch {
|
|
||||||
return EMPTY_PLEX_FILTERS;
|
|
||||||
}
|
|
||||||
}, [plexFiltersRaw]);
|
|
||||||
const setPlexFilters = (f: PlexFilters) => setPlexFiltersRaw(JSON.stringify(f));
|
|
||||||
// The Plex library search term is its OWN ephemeral state — deliberately NOT the persisted feed
|
|
||||||
// `filters.q`. Sharing that box meant a Plex search leaked into the feed and, being persisted,
|
|
||||||
// greeted you with a stale query (colliding with a persisted collection filter → confusing
|
|
||||||
// "0 matches") after a reload. Kept in App so it survives page switches within a session, but
|
|
||||||
// resets on reload.
|
|
||||||
const [plexQ, setPlexQ] = useAccountPersistedState(LS.plexQ, ""); // survives F5, unlike the feed search
|
|
||||||
// Bumped to tell the channel manager to drop a stale column filter when we send the user
|
// Bumped to tell the channel manager to drop a stale column filter when we send the user
|
||||||
// there to see a specific set (the header's "without full history" link).
|
// there to see a specific set (the header's "without full history" link).
|
||||||
const [channelsFilterReset, setChannelsFilterReset] = useState(0);
|
const [channelsFilterReset, setChannelsFilterReset] = useState(0);
|
||||||
@@ -581,15 +555,6 @@ export default function App() {
|
|||||||
{page === "plex" && meQuery.data!.plex_enabled && (
|
{page === "plex" && meQuery.data!.plex_enabled && (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<PlexSidebar
|
<PlexSidebar
|
||||||
scope={plexScope}
|
|
||||||
setScope={setPlexScope}
|
|
||||||
show={plexShowFilter}
|
|
||||||
setShow={setPlexShowFilter}
|
|
||||||
sort={plexSort}
|
|
||||||
setSort={setPlexSort}
|
|
||||||
filters={plexFilters}
|
|
||||||
setFilters={setPlexFilters}
|
|
||||||
onOpenPlaylist={setPlexPlaylistOpen}
|
|
||||||
layout={panelLayouts.plex}
|
layout={panelLayouts.plex}
|
||||||
setLayout={(l) => setPanelLayout("plex", l)}
|
setLayout={(l) => setPanelLayout("plex", l)}
|
||||||
collapsed={filterCollapsed}
|
collapsed={filterCollapsed}
|
||||||
@@ -644,8 +609,6 @@ export default function App() {
|
|||||||
header={
|
header={
|
||||||
<Header
|
<Header
|
||||||
me={meQuery.data!}
|
me={meQuery.data!}
|
||||||
plexQ={plexQ}
|
|
||||||
setPlexQ={setPlexQ}
|
|
||||||
channelsSearch={channelsSearch}
|
channelsSearch={channelsSearch}
|
||||||
setChannelsSearch={setChannelsSearch}
|
setChannelsSearch={setChannelsSearch}
|
||||||
onYtSearch={enterYtSearch}
|
onYtSearch={enterYtSearch}
|
||||||
@@ -714,17 +677,7 @@ export default function App() {
|
|||||||
onOpenWizard={() => setWizardOpen(true)}
|
onOpenWizard={() => setWizardOpen(true)}
|
||||||
/>
|
/>
|
||||||
) : page === "plex" && meQuery.data!.plex_enabled ? (
|
) : page === "plex" && meQuery.data!.plex_enabled ? (
|
||||||
<PlexBrowse
|
<PlexBrowse />
|
||||||
q={plexQ}
|
|
||||||
scope={plexScope}
|
|
||||||
setScope={setPlexScope}
|
|
||||||
show={plexShowFilter}
|
|
||||||
sort={plexSort}
|
|
||||||
filters={plexFilters}
|
|
||||||
setFilters={setPlexFilters}
|
|
||||||
openPlaylist={plexPlaylistOpen}
|
|
||||||
onPlaylistOpened={() => setPlexPlaylistOpen(null)}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<Feed
|
<Feed
|
||||||
filters={filters}
|
filters={filters}
|
||||||
|
|||||||
@@ -9,23 +9,19 @@ import SyncStatus from "./SyncStatus";
|
|||||||
import { useNavigation, useNavigationActions } from "./NavigationProvider";
|
import { useNavigation, useNavigationActions } from "./NavigationProvider";
|
||||||
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
|
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
|
||||||
import { usePlaylists, usePlaylistsActions } from "./PlaylistsProvider";
|
import { usePlaylists, usePlaylistsActions } from "./PlaylistsProvider";
|
||||||
|
import { usePlex, usePlexActions } from "./PlexProvider";
|
||||||
|
|
||||||
// The floating top header: a row of glass pills over the content — a ModuleName pill (label +
|
// The floating top header: a row of glass pills over the content — a ModuleName pill (label +
|
||||||
// accent dot + back/forward arrows), the module's SearchBar where it has one, and the per-user
|
// accent dot + back/forward arrows), the module's SearchBar where it has one, and the per-user
|
||||||
// sync chip at the right end.
|
// sync chip at the right end.
|
||||||
export default function Header({
|
export default function Header({
|
||||||
me,
|
me,
|
||||||
plexQ,
|
|
||||||
setPlexQ,
|
|
||||||
channelsSearch,
|
channelsSearch,
|
||||||
setChannelsSearch,
|
setChannelsSearch,
|
||||||
onYtSearch,
|
onYtSearch,
|
||||||
onGoToFullHistory,
|
onGoToFullHistory,
|
||||||
}: {
|
}: {
|
||||||
me: Me;
|
me: Me;
|
||||||
// Plex has its own ephemeral search term (see App) — the box is shared UI, the state is not.
|
|
||||||
plexQ: string;
|
|
||||||
setPlexQ: (q: string) => void;
|
|
||||||
channelsSearch: string;
|
channelsSearch: string;
|
||||||
setChannelsSearch: (q: string) => void;
|
setChannelsSearch: (q: string) => void;
|
||||||
// Trigger a live YouTube search for the current term (feed only; hidden for the demo account,
|
// Trigger a live YouTube search for the current term (feed only; hidden for the demo account,
|
||||||
@@ -39,9 +35,12 @@ export default function Header({
|
|||||||
const { setPage } = useNavigationActions();
|
const { setPage } = useNavigationActions();
|
||||||
const filters = useFeedFilters();
|
const filters = useFeedFilters();
|
||||||
const { setFilters } = useFeedFiltersActions();
|
const { setFilters } = useFeedFiltersActions();
|
||||||
// The playlists name filter lives in PlaylistsProvider (shared with its rail + detail pane).
|
// The playlists name filter lives in PlaylistsProvider (shared with its rail + detail pane); the
|
||||||
|
// Plex search term lives in PlexProvider (shared with its rail + browse grid).
|
||||||
const { search: playlistsSearch } = usePlaylists();
|
const { search: playlistsSearch } = usePlaylists();
|
||||||
const { setSearch: setPlaylistsSearch } = usePlaylistsActions();
|
const { setSearch: setPlaylistsSearch } = usePlaylistsActions();
|
||||||
|
const { q: plexQ } = usePlex();
|
||||||
|
const { setQ: setPlexQ } = usePlexActions();
|
||||||
// The ◀/▶ arrows step cyclically through the user's available modules (derived from `me`, so
|
// The ◀/▶ arrows step cyclically through the user's available modules (derived from `me`, so
|
||||||
// it stays correct as modules are added/gated). Labels of every active module feed ModuleName's
|
// it stays correct as modules are added/gated). Labels of every active module feed ModuleName's
|
||||||
// dynamic width so the pill is sized to the widest reachable title in the current language.
|
// dynamic width so the pill is sized to the widest reachable title in the current language.
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { DetailCustomizeMenu, Filterable, useArtBackdrop, useDetailPrefs } from
|
|||||||
import PlexPlaylistAdd, { type PlexAddTarget } from "./PlexPlaylistAdd";
|
import PlexPlaylistAdd, { type PlexAddTarget } from "./PlexPlaylistAdd";
|
||||||
import PlexCollectionEditor from "./PlexCollectionEditor";
|
import PlexCollectionEditor from "./PlexCollectionEditor";
|
||||||
import { PageToolbar } from "./PageShell";
|
import { PageToolbar } from "./PageShell";
|
||||||
|
import { usePlex, usePlexActions } from "./PlexProvider";
|
||||||
|
|
||||||
// Lazy: the rich player (pulls in hls.js) loads only when something is first played.
|
// Lazy: the rich player (pulls in hls.js) loads only when something is first played.
|
||||||
const PlexPlayer = lazy(() => import("./PlexPlayer"));
|
const PlexPlayer = lazy(() => import("./PlexPlayer"));
|
||||||
@@ -54,34 +55,16 @@ type Sub =
|
|||||||
// (useHistorySubview) so browser Back returns to the grid. Playback lands in P2 — a card announces
|
// (useHistorySubview) so browser Back returns to the grid. Playback lands in P2 — a card announces
|
||||||
// for now. Rendered by App for page==="plex" (its own nav module).
|
// for now. Rendered by App for page==="plex" (its own nav module).
|
||||||
|
|
||||||
type Props = {
|
|
||||||
q: string;
|
|
||||||
scope: string; // movie | show | both (unified cross-library scope)
|
|
||||||
setScope: (v: string) => void;
|
|
||||||
show: string;
|
|
||||||
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;
|
const PAGE = 40;
|
||||||
|
|
||||||
export default function PlexBrowse({
|
export default function PlexBrowse() {
|
||||||
q,
|
|
||||||
scope,
|
|
||||||
setScope,
|
|
||||||
show,
|
|
||||||
sort,
|
|
||||||
filters,
|
|
||||||
setFilters,
|
|
||||||
openPlaylist,
|
|
||||||
onPlaylistOpened,
|
|
||||||
}: Props) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
|
// Scope / watch-state / sort / filters live in PlexProvider (edited mainly from PlexSidebar); the
|
||||||
|
// search term (q) comes from the shared header box; the rail hands off a playlist to open via the
|
||||||
|
// shared `playlistOpen` slot, which we consume into a history subview then clear.
|
||||||
|
const { q, scope, show, sort, filters, playlistOpen: openPlaylist } = usePlex();
|
||||||
|
const { setScope, setFilters, setPlaylistOpen } = usePlexActions();
|
||||||
const dq = useDebounced(q.trim(), 350);
|
const dq = useDebounced(q.trim(), 350);
|
||||||
const sub = useHistorySubview<Sub>({ kind: "grid" });
|
const sub = useHistorySubview<Sub>({ kind: "grid" });
|
||||||
|
|
||||||
@@ -90,7 +73,7 @@ export default function PlexBrowse({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openPlaylist != null) {
|
if (openPlaylist != null) {
|
||||||
sub.open({ kind: "playlist", id: openPlaylist });
|
sub.open({ kind: "playlist", id: openPlaylist });
|
||||||
onPlaylistOpened?.();
|
setPlaylistOpen(null);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [openPlaylist]);
|
}, [openPlaylist]);
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
|
import { EMPTY_PLEX_FILTERS, type PlexFilters } from "../lib/api";
|
||||||
|
import { getAccountRaw, LS, setAccountRaw } from "../lib/storage";
|
||||||
|
|
||||||
|
// Owns the Plex module's shared local state — the left-rail library scope / watch-state / sort /
|
||||||
|
// expanded filters, the header search term, and the "open this playlist" hand-off — lifted out of
|
||||||
|
// App so the PlexSidebar (rail), PlexBrowse (content), and the header search box read it via hooks
|
||||||
|
// instead of prop-drilling through App. Mirrors FeedFiltersProvider (split State + Actions contexts,
|
||||||
|
// stable useCallback setters, one-line hooks; composed in main.tsx around <App/>).
|
||||||
|
//
|
||||||
|
// scope / show / sort / filters / q are per-account persisted (localStorage); the account switch does
|
||||||
|
// a full reload, so init-from-storage is all the hydration they need. `playlistOpen` is an ephemeral
|
||||||
|
// hand-off (the rail sets it, PlexBrowse consumes it into a history subview then clears it). The Plex
|
||||||
|
// search term is deliberately its OWN state, NOT the feed's `filters.q`, so a Plex search never leaks
|
||||||
|
// into (and is never greeted by a stale value from) the feed.
|
||||||
|
|
||||||
|
type PlexState = {
|
||||||
|
scope: string; // movie | show | both (unified cross-library scope)
|
||||||
|
show: string;
|
||||||
|
sort: string;
|
||||||
|
filters: PlexFilters;
|
||||||
|
q: string;
|
||||||
|
playlistOpen: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PlexActions {
|
||||||
|
setScope: (v: string) => void;
|
||||||
|
setShow: (v: string) => void;
|
||||||
|
setSort: (v: string) => void;
|
||||||
|
setFilters: (f: PlexFilters) => void;
|
||||||
|
setQ: (q: string) => void;
|
||||||
|
setPlaylistOpen: (id: number | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StateContext = createContext<PlexState>({
|
||||||
|
scope: "both",
|
||||||
|
show: "all",
|
||||||
|
sort: "title",
|
||||||
|
filters: EMPTY_PLEX_FILTERS,
|
||||||
|
q: "",
|
||||||
|
playlistOpen: null,
|
||||||
|
});
|
||||||
|
const ActionsContext = createContext<PlexActions>({
|
||||||
|
setScope: () => {},
|
||||||
|
setShow: () => {},
|
||||||
|
setSort: () => {},
|
||||||
|
setFilters: () => {},
|
||||||
|
setQ: () => {},
|
||||||
|
setPlaylistOpen: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function usePlex(): PlexState {
|
||||||
|
return useContext(StateContext);
|
||||||
|
}
|
||||||
|
export function usePlexActions(): PlexActions {
|
||||||
|
return useContext(ActionsContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PlexProvider({ children }: { children: ReactNode }) {
|
||||||
|
// The former per-library picker is now a cross-library SCOPE: movie | show | both (unified library).
|
||||||
|
// (Reuses the old LS.plexLibrary key; an old stored library-id value falls back to "both".)
|
||||||
|
const [scope, setScopeState] = useState<string>(() => {
|
||||||
|
const v = getAccountRaw(LS.plexLibrary) ?? "both";
|
||||||
|
return ["movie", "show", "both"].includes(v) ? v : "both";
|
||||||
|
});
|
||||||
|
const [show, setShowState] = useState<string>(() => getAccountRaw(LS.plexShow) ?? "all");
|
||||||
|
const [sort, setSortState] = useState<string>(() => getAccountRaw(LS.plexSort) ?? "title");
|
||||||
|
// The expanded Plex filters live as one JSON blob (the string-only account store serializes it).
|
||||||
|
const [filtersRaw, setFiltersRaw] = useState<string>(() => getAccountRaw(LS.plexFilters) ?? "{}");
|
||||||
|
const filters = useMemo<PlexFilters>(() => {
|
||||||
|
try {
|
||||||
|
return { ...EMPTY_PLEX_FILTERS, ...JSON.parse(filtersRaw) };
|
||||||
|
} catch {
|
||||||
|
return EMPTY_PLEX_FILTERS;
|
||||||
|
}
|
||||||
|
}, [filtersRaw]);
|
||||||
|
// Survives F5 (persisted), unlike the feed search — see the scope note above.
|
||||||
|
const [q, setQState] = useState<string>(() => getAccountRaw(LS.plexQ) ?? "");
|
||||||
|
const [playlistOpen, setPlaylistOpen] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const setScope = useCallback((v: string) => {
|
||||||
|
setScopeState(v);
|
||||||
|
setAccountRaw(LS.plexLibrary, v);
|
||||||
|
}, []);
|
||||||
|
const setShow = useCallback((v: string) => {
|
||||||
|
setShowState(v);
|
||||||
|
setAccountRaw(LS.plexShow, v);
|
||||||
|
}, []);
|
||||||
|
const setSort = useCallback((v: string) => {
|
||||||
|
setSortState(v);
|
||||||
|
setAccountRaw(LS.plexSort, v);
|
||||||
|
}, []);
|
||||||
|
const setFilters = useCallback((f: PlexFilters) => {
|
||||||
|
const s = JSON.stringify(f);
|
||||||
|
setFiltersRaw(s);
|
||||||
|
setAccountRaw(LS.plexFilters, s);
|
||||||
|
}, []);
|
||||||
|
const setQ = useCallback((v: string) => {
|
||||||
|
setQState(v);
|
||||||
|
setAccountRaw(LS.plexQ, v);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const state = useMemo<PlexState>(
|
||||||
|
() => ({ scope, show, sort, filters, q, playlistOpen }),
|
||||||
|
[scope, show, sort, filters, q, playlistOpen],
|
||||||
|
);
|
||||||
|
const actions = useMemo<PlexActions>(
|
||||||
|
() => ({ setScope, setShow, setSort, setFilters, setQ, setPlaylistOpen }),
|
||||||
|
[setScope, setShow, setSort, setFilters, setQ],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionsContext.Provider value={actions}>
|
||||||
|
<StateContext.Provider value={state}>{children}</StateContext.Provider>
|
||||||
|
</ActionsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { useDebounced } from "../lib/useDebounced";
|
|||||||
import { useScrollFade } from "../lib/useScrollFade";
|
import { useScrollFade } from "../lib/useScrollFade";
|
||||||
import SidePanel from "./SidePanel";
|
import SidePanel from "./SidePanel";
|
||||||
import PanelGroups, { type PanelItem } from "./PanelGroups";
|
import PanelGroups, { type PanelItem } from "./PanelGroups";
|
||||||
|
import { usePlex, usePlexActions } from "./PlexProvider";
|
||||||
|
|
||||||
// The Plex module's left filter column — now the shared floating SidePanel with its filter groups
|
// The Plex module's left filter column — now the shared floating SidePanel with its filter groups
|
||||||
// as reorderable "islands" (same system as the feed rail). Movie libraries get the full metadata
|
// as reorderable "islands" (same system as the feed rail). Movie libraries get the full metadata
|
||||||
@@ -15,15 +16,6 @@ import PanelGroups, { type PanelItem } from "./PanelGroups";
|
|||||||
// the backend so the panel only offers what the library actually contains.
|
// the backend so the panel only offers what the library actually contains.
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
scope: string; // movie | show | both (unified cross-library scope)
|
|
||||||
setScope: (v: string) => void;
|
|
||||||
show: string;
|
|
||||||
setShow: (v: string) => void;
|
|
||||||
sort: string;
|
|
||||||
setSort: (v: string) => void;
|
|
||||||
filters: PlexFilters;
|
|
||||||
setFilters: (f: PlexFilters) => void;
|
|
||||||
onOpenPlaylist: (id: number) => void;
|
|
||||||
layout: PanelLayout;
|
layout: PanelLayout;
|
||||||
setLayout: (l: PanelLayout) => void;
|
setLayout: (l: PanelLayout) => void;
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
@@ -42,21 +34,17 @@ const DURATION_BUCKETS: { key: string; min: number | null; max: number | null }[
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function PlexSidebar({
|
export default function PlexSidebar({
|
||||||
scope,
|
|
||||||
setScope,
|
|
||||||
show,
|
|
||||||
setShow,
|
|
||||||
sort,
|
|
||||||
setSort,
|
|
||||||
filters,
|
|
||||||
setFilters,
|
|
||||||
onOpenPlaylist,
|
|
||||||
layout,
|
layout,
|
||||||
setLayout,
|
setLayout,
|
||||||
collapsed,
|
collapsed,
|
||||||
onToggleCollapse,
|
onToggleCollapse,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
// Scope / watch-state / sort / expanded filters live in PlexProvider (shared with PlexBrowse and
|
||||||
|
// the header search box); this rail is the primary editor of them. Opening a playlist is a
|
||||||
|
// hand-off to PlexBrowse via the shared `playlistOpen` slot.
|
||||||
|
const { scope, show, sort, filters } = usePlex();
|
||||||
|
const { setScope, setShow, setSort, setFilters, setPlaylistOpen: onOpenPlaylist } = usePlexActions();
|
||||||
const collFade = useScrollFade();
|
const collFade = useScrollFade();
|
||||||
const playlistsQ = useQuery({ queryKey: ["plex-playlists"], queryFn: () => api.plexPlaylists() });
|
const playlistsQ = useQuery({ queryKey: ["plex-playlists"], queryFn: () => api.plexPlaylists() });
|
||||||
const [newPlaylist, setNewPlaylist] = useState("");
|
const [newPlaylist, setNewPlaylist] = useState("");
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ConfirmProvider } from "./components/ConfirmProvider";
|
|||||||
import { NavigationProvider } from "./components/NavigationProvider";
|
import { NavigationProvider } from "./components/NavigationProvider";
|
||||||
import { FeedFiltersProvider } from "./components/FeedFiltersProvider";
|
import { FeedFiltersProvider } from "./components/FeedFiltersProvider";
|
||||||
import { PlaylistsProvider } from "./components/PlaylistsProvider";
|
import { PlaylistsProvider } from "./components/PlaylistsProvider";
|
||||||
|
import { PlexProvider } from "./components/PlexProvider";
|
||||||
import "./i18n";
|
import "./i18n";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
@@ -41,7 +42,9 @@ const root =
|
|||||||
<NavigationProvider>
|
<NavigationProvider>
|
||||||
<FeedFiltersProvider>
|
<FeedFiltersProvider>
|
||||||
<PlaylistsProvider>
|
<PlaylistsProvider>
|
||||||
<App />
|
<PlexProvider>
|
||||||
|
<App />
|
||||||
|
</PlexProvider>
|
||||||
</PlaylistsProvider>
|
</PlaylistsProvider>
|
||||||
</FeedFiltersProvider>
|
</FeedFiltersProvider>
|
||||||
</NavigationProvider>
|
</NavigationProvider>
|
||||||
|
|||||||
Reference in New Issue
Block a user