import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from "react"; import { getAccountRaw, LS, setAccountRaw } from "../lib/storage"; import { useAccountRehydrate } from "../lib/useAccountRehydrate"; import { useUrlMirror } from "../lib/useUrlMirror"; import { useNavigation } from "./NavigationProvider"; // Owns the Playlists module's shared local state — the rail's name filter and the selected playlist // — lifted out of the App god component so the header search box, the PlaylistsRail, and the // Playlists detail pane read it via hooks instead of prop-drilling through App. Mirrors // FeedFiltersProvider / NavigationProvider (split State + Actions contexts, one-line hooks; composed // in main.tsx around ). // // The `search` term is ephemeral (resets on reload, like the other list filters); the selected // playlist is per-account persisted (LS.playlist) so a reload returns you to it. Neither is // server-synced, and an account switch does a full reload, so init-from-storage is all the // hydration these need — no per-account load effect (unlike the server-backed feed filters). type PlaylistsState = { search: string; selectedId: number | null; }; interface PlaylistsActions { setSearch: (q: string) => void; setSelectedId: (id: number | null) => void; } const StateContext = createContext({ search: "", selectedId: null }); const ActionsContext = createContext({ setSearch: () => {}, setSelectedId: () => {}, }); export function usePlaylists(): PlaylistsState { return useContext(StateContext); } export function usePlaylistsActions(): PlaylistsActions { return useContext(ActionsContext); } function readSelectedId(): number | null { // A ?pl bookmark wins over the stored selection so a pasted link opens that playlist. Accept only a // positive integer id — a bare "?pl=" gives Number("")===0 (finite!) and "?pl=1.5" a fractional id, // both of which would select a non-existent playlist. const fromUrl = new URLSearchParams(window.location.search).get("pl"); const n = fromUrl ? Number(fromUrl) : NaN; if (Number.isInteger(n) && n > 0) return n; const s = getAccountRaw(LS.playlist); return s ? Number(s) : null; } export function PlaylistsProvider({ children }: { children: ReactNode }) { const [search, setSearchState] = useState(""); const [selectedId, setSelectedIdState] = useState(readSelectedId); // Re-read the selected playlist once the account is pinned after mount (first load on a fresh tab): // this provider mounts above , which pins the account only after /api/me. const markDirty = useAccountRehydrate(() => setSelectedIdState(readSelectedId())); const setSearch = useCallback((q: string) => setSearchState(q), []); const setSelectedId = useCallback( (id: number | null) => { markDirty(); setSelectedIdState(id); if (id != null) setAccountRaw(LS.playlist, String(id)); }, [markDirty], ); // Mirror the selected playlist to ?pl on the Playlists page so it's bookmarkable; clear it elsewhere // (the localStorage copy still restores it on return). useUrlMirror keeps ?page and the other nav // params intact. const { page } = useNavigation(); useUrlMirror({ pl: page === "playlists" && selectedId != null ? String(selectedId) : null }); const state = useMemo(() => ({ search, selectedId }), [search, selectedId]); const actions = useMemo( () => ({ setSearch, setSelectedId }), [setSearch, setSelectedId], ); return ( {children} ); }