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 ). // // 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({ scope: "both", show: "all", sort: "title", filters: EMPTY_PLEX_FILTERS, q: "", playlistOpen: null, }); const ActionsContext = createContext({ 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(() => { const v = getAccountRaw(LS.plexLibrary) ?? "both"; return ["movie", "show", "both"].includes(v) ? v : "both"; }); const [show, setShowState] = useState(() => getAccountRaw(LS.plexShow) ?? "all"); const [sort, setSortState] = useState(() => getAccountRaw(LS.plexSort) ?? "title"); // The expanded Plex filters live as one JSON blob (the string-only account store serializes it). const [filtersRaw, setFiltersRaw] = useState(() => getAccountRaw(LS.plexFilters) ?? "{}"); const filters = useMemo(() => { 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(() => getAccountRaw(LS.plexQ) ?? ""); const [playlistOpen, setPlaylistOpen] = useState(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( () => ({ scope, show, sort, filters, q, playlistOpen }), [scope, show, sort, filters, q, playlistOpen], ); const actions = useMemo( () => ({ setScope, setShow, setSort, setFilters, setQ, setPlaylistOpen }), [setScope, setShow, setSort, setFilters, setQ], ); return ( {children} ); }