refactor(prefs): move Settings auto-save machinery into PrefsProvider
Lift the four editable Settings prefs (theme/perf/hints/notifications), their live-apply effects, the debounced auto-save + baseline echo-guard, and the server-adopt-on-login into PrefsProvider, exposing the PrefsController via usePrefs(). SettingsPanel and GlassTuner read it from the hook instead of props; App's big adopt effect keeps only the shell prefs (panel layouts, rail collapse, language). App.tsx 655->489 lines. E2E 17/17 incl. settings auto-save. (Platform S6b.3)
This commit is contained in:
+7
-148
@@ -1,4 +1,4 @@
|
||||
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { lazy, Suspense, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
@@ -8,13 +8,6 @@ import {
|
||||
setUnauthorizedHandler,
|
||||
} from "./lib/api";
|
||||
import { setLanguage, isSupported, type LangCode } from "./i18n";
|
||||
import {
|
||||
applyTheme,
|
||||
DEFAULT_THEME,
|
||||
loadLocalTheme,
|
||||
saveLocalTheme,
|
||||
type ThemePrefs,
|
||||
} from "./lib/theme";
|
||||
import { hasFilterParams, stripUrlParams } from "./lib/urlState";
|
||||
import {
|
||||
loadLayout,
|
||||
@@ -24,16 +17,13 @@ import {
|
||||
type PanelId,
|
||||
type PanelLayout,
|
||||
} from "./lib/panelLayout";
|
||||
import { configureNotifications, getNotifSettings, notify, removeByMetaKind, type NotifSettings } from "./lib/notifications";
|
||||
import { hintsEnabled, setHintsEnabled } from "./lib/hints";
|
||||
import { notify, removeByMetaKind } from "./lib/notifications";
|
||||
import {
|
||||
getAccountRaw,
|
||||
LS,
|
||||
readAccount,
|
||||
setAccountRaw,
|
||||
writeAccount,
|
||||
} from "./lib/storage";
|
||||
import type { PrefsController } from "./components/SettingsPanel";
|
||||
import { useNavigation, useNavigationActions } from "./components/NavigationProvider";
|
||||
import { useFeedFilters, useFeedFiltersActions } from "./components/FeedFiltersProvider";
|
||||
import { useFeedView, useFeedViewActions } from "./components/FeedViewProvider";
|
||||
@@ -76,23 +66,10 @@ const DownloadCenter = lazy(() => import("./components/DownloadCenter"));
|
||||
const About = lazy(() => import("./components/About"));
|
||||
const ReleaseNotes = lazy(() => import("./components/ReleaseNotes"));
|
||||
|
||||
const PERF_KEY = LS.perfMode;
|
||||
|
||||
// The preferences edited on the Settings page. They apply locally for instant preview but
|
||||
// persist to the server only on an explicit Save — so App holds both the live "draft" (the
|
||||
// individual states below) and the last-saved "baseline" to compute dirty / revert on discard.
|
||||
type EditablePrefs = {
|
||||
theme: ThemePrefs;
|
||||
performanceMode: boolean;
|
||||
hints: boolean;
|
||||
notifications: NotifSettings;
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { page, channelView, ytSearch } = useNavigation();
|
||||
const { setPage, enterYtSearch, exitYtSearch } = useNavigationActions();
|
||||
const [theme, setThemeState] = useState<ThemePrefs>(() => loadLocalTheme());
|
||||
// The global feed filters live in FeedFiltersProvider (per-account persistence + the account-load
|
||||
// effect + share-link capture all moved there). App reads them to hand to <Feed> and to the
|
||||
// handlers that mutate the feed view (onShowInFeed, tag-click, the feed scrollKey).
|
||||
@@ -102,23 +79,6 @@ export default function App() {
|
||||
// by the feed page and the channel page. App reads it only to hand to <Feed>/<ChannelPage>.
|
||||
const view = useFeedView();
|
||||
const { setView } = useFeedViewActions();
|
||||
// Settings-page prefs: applied live by the effects below, and auto-saved (debounced) on change
|
||||
// — no draft, no Save button (see the auto-save effect).
|
||||
const [perf, setPerf] = useState(() => getAccountRaw(PERF_KEY) === "1");
|
||||
const [hints, setHints] = useState(() => hintsEnabled());
|
||||
const [notif, setNotif] = useState<NotifSettings>(() => getNotifSettings());
|
||||
// The last server-known prefs. The same states are ALSO written when we adopt server prefs on
|
||||
// login/account-switch, so the auto-save effect compares against this baseline to avoid echoing
|
||||
// a server-adopted value straight back. A ref (drives no UI); updated on adopt + after each save.
|
||||
const baselineRef = useRef<EditablePrefs>({
|
||||
theme: loadLocalTheme(),
|
||||
performanceMode: getAccountRaw(PERF_KEY) === "1",
|
||||
hints: hintsEnabled(),
|
||||
notifications: getNotifSettings(),
|
||||
});
|
||||
const [prefsSaveState, setPrefsSaveState] = useState<PrefsController["saveState"]>("idle");
|
||||
const saveMsgTimer = useRef<number | undefined>(undefined);
|
||||
const saveTimer = useRef<number | undefined>(undefined);
|
||||
// Per-panel "customize" layouts (order/collapsed/hidden of each side panel's group cards),
|
||||
// persisted per-account. One record so feed/plex/playlists share the same mechanism.
|
||||
const [panelLayouts, setPanelLayouts] = useState<Record<PanelId, PanelLayout>>(() => ({
|
||||
@@ -171,70 +131,6 @@ export default function App() {
|
||||
writeAccount(LS.playlistsCollapsed, next);
|
||||
api.savePrefs({ playlistsCollapsed: next }).catch(() => {});
|
||||
}
|
||||
useEffect(() => applyTheme(theme), [theme]);
|
||||
|
||||
// Apply the draft prefs locally for instant preview (their localStorage mirrors update via
|
||||
// the stores too); persistence to the server is deferred to an explicit Save.
|
||||
useEffect(() => {
|
||||
document.documentElement.dataset.perf = perf ? "1" : "";
|
||||
setAccountRaw(PERF_KEY, perf ? "1" : "0");
|
||||
}, [perf]);
|
||||
|
||||
// The per-scheme background image (the "glass over image" look) is on when the user hasn't opted
|
||||
// out and we're not in perf mode. Drives `html[data-backdrop]` (see index.css), which paints the
|
||||
// theme-appropriate image on <body> (dark or /light/ set) and switches the glass to translucent.
|
||||
useEffect(() => {
|
||||
const on = theme.bgImage && !perf;
|
||||
document.documentElement.dataset.backdrop = on ? "on" : "off";
|
||||
}, [theme.bgImage, perf]);
|
||||
useEffect(() => setHintsEnabled(hints), [hints]);
|
||||
useEffect(() => configureNotifications(notif), [notif]);
|
||||
|
||||
// Auto-save the Settings prefs on change (debounced ~500ms so a slider drag coalesces into one
|
||||
// PUT). Live-apply is handled by the effects above; this is the only persistence path — there is
|
||||
// no draft or Save button. `baselineRef` suppresses the echo when the change came from adopting
|
||||
// server prefs on login (`/me` merges, so a full payload is fine).
|
||||
const editablePrefs = useMemo<EditablePrefs>(
|
||||
() => ({ theme, performanceMode: perf, hints, notifications: notif }),
|
||||
[theme, perf, hints, notif],
|
||||
);
|
||||
useEffect(() => {
|
||||
window.clearTimeout(saveTimer.current);
|
||||
if (JSON.stringify(editablePrefs) === JSON.stringify(baselineRef.current)) {
|
||||
// Back at the last-saved value (initial load, server adopt, or an edit undone) — nothing to
|
||||
// save; clear any "Saving…" left from a change that was reverted before it flushed.
|
||||
setPrefsSaveState((s) => (s === "saving" ? "idle" : s));
|
||||
return;
|
||||
}
|
||||
setPrefsSaveState("saving");
|
||||
window.clearTimeout(saveMsgTimer.current);
|
||||
const payload = editablePrefs;
|
||||
saveTimer.current = window.setTimeout(() => {
|
||||
api
|
||||
.savePrefs(payload)
|
||||
.then(() => {
|
||||
baselineRef.current = payload;
|
||||
setPrefsSaveState("saved");
|
||||
saveMsgTimer.current = window.setTimeout(() => setPrefsSaveState("idle"), 2500);
|
||||
})
|
||||
.catch(() => {
|
||||
// api.req() already surfaces the failure; flag the indicator so the user sees it didn't take.
|
||||
setPrefsSaveState("error");
|
||||
saveMsgTimer.current = window.setTimeout(() => setPrefsSaveState("idle"), 4000);
|
||||
});
|
||||
}, 500);
|
||||
return () => window.clearTimeout(saveTimer.current);
|
||||
}, [editablePrefs]);
|
||||
// Clear pending pref-save timers on unmount so a debounced PUT or a fade timer never fires
|
||||
// setState on a dead component.
|
||||
useEffect(
|
||||
() => () => {
|
||||
window.clearTimeout(saveTimer.current);
|
||||
window.clearTimeout(saveMsgTimer.current);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Filters live in localStorage, not the address bar. If we arrived via a "Share view"
|
||||
// link, its params have already hydrated the initial state — persist them and strip the
|
||||
// query so the URL stays clean from here on.
|
||||
@@ -330,31 +226,12 @@ export default function App() {
|
||||
stripUrlParams();
|
||||
}, [adminDeepLink, meQuery.data?.id]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// On login, adopt server-stored preferences.
|
||||
// On login, adopt the server-stored SHELL preferences (panel layouts, rail collapse flags,
|
||||
// language). The Settings-page prefs (theme/perf/hints/notifications) are adopted by PrefsProvider
|
||||
// and the feed view by FeedViewProvider — each via its own passive ["me"] observer.
|
||||
useEffect(() => {
|
||||
const prefs = meQuery.data?.preferences;
|
||||
if (!prefs) return;
|
||||
// Adopt the server prefs into the live states (the runtime effects above apply them for
|
||||
// display) AND into the auto-save baseline, so adopting them doesn't echo a save back.
|
||||
const adopted: EditablePrefs = {
|
||||
theme: prefs.theme ? { ...DEFAULT_THEME, ...prefs.theme } : loadLocalTheme(),
|
||||
performanceMode:
|
||||
typeof prefs.performanceMode === "boolean"
|
||||
? prefs.performanceMode
|
||||
: getAccountRaw(PERF_KEY) === "1",
|
||||
hints: typeof prefs.hints === "boolean" ? prefs.hints : hintsEnabled(),
|
||||
notifications: prefs.notifications
|
||||
? { ...getNotifSettings(), ...prefs.notifications }
|
||||
: getNotifSettings(),
|
||||
};
|
||||
setThemeState(adopted.theme);
|
||||
saveLocalTheme(adopted.theme);
|
||||
setPerf(adopted.performanceMode);
|
||||
setHints(adopted.hints);
|
||||
setNotif(adopted.notifications);
|
||||
baselineRef.current = adopted;
|
||||
// Out-of-scope prefs stay instant (edited outside the Settings page). The feed view is adopted
|
||||
// by FeedViewProvider (its own passive ["me"] observer), not here.
|
||||
const prefsRec = prefs as Record<string, unknown>;
|
||||
(["feed", "plex", "playlists"] as PanelId[]).forEach((p) => {
|
||||
const raw = prefsRec[PREF_KEY[p]];
|
||||
@@ -411,30 +288,12 @@ export default function App() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [meQuery.data?.role, meQuery.data?.pending_invites]);
|
||||
|
||||
// Theme applies live (and caches to localStorage); the auto-save effect persists it to the server.
|
||||
function setTheme(next: ThemePrefs) {
|
||||
setThemeState(next);
|
||||
saveLocalTheme(next);
|
||||
}
|
||||
// Language is edited outside the Settings page (sidebar/login), so it stays instant.
|
||||
function changeLanguage(code: LangCode) {
|
||||
setLanguage(code);
|
||||
api.savePrefs({ language: code }).catch(() => {});
|
||||
}
|
||||
|
||||
const prefsCtl: PrefsController = {
|
||||
theme,
|
||||
setTheme,
|
||||
perf,
|
||||
setPerf,
|
||||
hints,
|
||||
setHints,
|
||||
notif,
|
||||
setNotif,
|
||||
saveState: prefsSaveState,
|
||||
};
|
||||
|
||||
|
||||
// First-run: a fresh instance is in setup mode (all other routes are locked) → show the wizard.
|
||||
if (setupQuery.isLoading)
|
||||
return (
|
||||
@@ -581,7 +440,7 @@ export default function App() {
|
||||
) : page === "downloads" && !meQuery.data!.is_demo ? (
|
||||
<DownloadCenter me={meQuery.data!} />
|
||||
) : page === "settings" ? (
|
||||
<SettingsPanel me={meQuery.data!} prefs={prefsCtl} />
|
||||
<SettingsPanel me={meQuery.data!} />
|
||||
) : page === "plex" && meQuery.data!.plex_enabled ? (
|
||||
<PlexBrowse />
|
||||
) : (
|
||||
@@ -624,7 +483,7 @@ export default function App() {
|
||||
{/* Re-binds to the active page's scroll container on navigation (page or channel change). */}
|
||||
<BackToTop dep={channelView ? `chan:${channelView.id}` : page} />
|
||||
{/* Opt-in live appearance tuner (Settings → Appearance; off by default). */}
|
||||
<GlassTuner enabled={theme.showTuner} />
|
||||
<GlassTuner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Overlay from "./Overlay";
|
||||
import { usePrefs } from "./PrefsProvider";
|
||||
|
||||
/**
|
||||
* A floating live appearance tuner. Mounted at the app root so it's reachable on every page; it
|
||||
@@ -91,7 +92,9 @@ function loadSaved(): Saved {
|
||||
}
|
||||
}
|
||||
|
||||
export default function GlassTuner({ enabled }: { enabled: boolean }) {
|
||||
export default function GlassTuner() {
|
||||
// Opt-in via Settings → Appearance (theme.showTuner, off by default); the flag lives in PrefsProvider.
|
||||
const enabled = usePrefs().theme.showTuner;
|
||||
const initial = useMemo(loadSaved, []);
|
||||
const [vars, setVars] = useState<Record<string, number>>(() => {
|
||||
const base: Record<string, number> = {};
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api } from "../lib/api";
|
||||
import {
|
||||
applyTheme,
|
||||
DEFAULT_THEME,
|
||||
loadLocalTheme,
|
||||
saveLocalTheme,
|
||||
type ThemePrefs,
|
||||
} from "../lib/theme";
|
||||
import { configureNotifications, getNotifSettings, type NotifSettings } from "../lib/notifications";
|
||||
import { hintsEnabled, setHintsEnabled } from "../lib/hints";
|
||||
import { getAccountRaw, LS, setAccountRaw } from "../lib/storage";
|
||||
import type { PrefsController } from "./SettingsPanel";
|
||||
|
||||
const PERF_KEY = LS.perfMode;
|
||||
|
||||
// The four Settings-page preferences. They apply locally for instant preview and auto-save (debounced)
|
||||
// to the server — there is no draft or Save button. This provider owns them (lifted out of the App god
|
||||
// component) plus the last-saved "baseline" used to suppress the echo when a change came from adopting
|
||||
// server prefs on login. Consumers (SettingsPanel, GlassTuner) read the PrefsController via usePrefs().
|
||||
type EditablePrefs = {
|
||||
theme: ThemePrefs;
|
||||
performanceMode: boolean;
|
||||
hints: boolean;
|
||||
notifications: NotifSettings;
|
||||
};
|
||||
|
||||
const PrefsContext = createContext<PrefsController>({
|
||||
theme: DEFAULT_THEME,
|
||||
setTheme: () => {},
|
||||
perf: false,
|
||||
setPerf: () => {},
|
||||
hints: true,
|
||||
setHints: () => {},
|
||||
notif: getNotifSettings(),
|
||||
setNotif: () => {},
|
||||
saveState: "idle",
|
||||
});
|
||||
|
||||
export function usePrefs(): PrefsController {
|
||||
return useContext(PrefsContext);
|
||||
}
|
||||
|
||||
export function PrefsProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setThemeState] = useState<ThemePrefs>(() => loadLocalTheme());
|
||||
const [perf, setPerf] = useState(() => getAccountRaw(PERF_KEY) === "1");
|
||||
const [hints, setHints] = useState(() => hintsEnabled());
|
||||
const [notif, setNotif] = useState<NotifSettings>(() => getNotifSettings());
|
||||
// The last server-known prefs. Written on adopt + after each save so the auto-save effect can
|
||||
// compare against it and avoid echoing a server-adopted value straight back. A ref (drives no UI).
|
||||
const baselineRef = useRef<EditablePrefs>({
|
||||
theme: loadLocalTheme(),
|
||||
performanceMode: getAccountRaw(PERF_KEY) === "1",
|
||||
hints: hintsEnabled(),
|
||||
notifications: getNotifSettings(),
|
||||
});
|
||||
const [saveState, setSaveState] = useState<PrefsController["saveState"]>("idle");
|
||||
const saveMsgTimer = useRef<number | undefined>(undefined);
|
||||
const saveTimer = useRef<number | undefined>(undefined);
|
||||
// Passive observer of the account App fetches (enabled:false → reads cache, re-renders on update).
|
||||
const { data: me } = useQuery({ queryKey: ["me"], queryFn: api.me, enabled: false });
|
||||
|
||||
// Live-apply the prefs locally for instant preview (their localStorage mirrors update via the
|
||||
// stores too); persistence to the server is the auto-save effect below.
|
||||
useEffect(() => applyTheme(theme), [theme]);
|
||||
useEffect(() => {
|
||||
document.documentElement.dataset.perf = perf ? "1" : "";
|
||||
setAccountRaw(PERF_KEY, perf ? "1" : "0");
|
||||
}, [perf]);
|
||||
// The per-scheme background image (the "glass over image" look) is on when the user hasn't opted
|
||||
// out and we're not in perf mode. Drives `html[data-backdrop]` (see index.css), which paints the
|
||||
// theme-appropriate image on <body> and switches the glass to translucent.
|
||||
useEffect(() => {
|
||||
const on = theme.bgImage && !perf;
|
||||
document.documentElement.dataset.backdrop = on ? "on" : "off";
|
||||
}, [theme.bgImage, perf]);
|
||||
useEffect(() => setHintsEnabled(hints), [hints]);
|
||||
useEffect(() => configureNotifications(notif), [notif]);
|
||||
|
||||
// Auto-save on change (debounced ~500ms so a slider drag coalesces into one PUT). Live-apply is the
|
||||
// effects above; this is the only persistence path. `baselineRef` suppresses the echo when the
|
||||
// change came from adopting server prefs on login (`/me` merges, so a full payload is fine).
|
||||
const editablePrefs = useMemo<EditablePrefs>(
|
||||
() => ({ theme, performanceMode: perf, hints, notifications: notif }),
|
||||
[theme, perf, hints, notif],
|
||||
);
|
||||
useEffect(() => {
|
||||
window.clearTimeout(saveTimer.current);
|
||||
if (JSON.stringify(editablePrefs) === JSON.stringify(baselineRef.current)) {
|
||||
// Back at the last-saved value (initial load, server adopt, or an edit undone) — nothing to
|
||||
// save; clear any "Saving…" left from a change that was reverted before it flushed.
|
||||
setSaveState((s) => (s === "saving" ? "idle" : s));
|
||||
return;
|
||||
}
|
||||
setSaveState("saving");
|
||||
window.clearTimeout(saveMsgTimer.current);
|
||||
const payload = editablePrefs;
|
||||
saveTimer.current = window.setTimeout(() => {
|
||||
api
|
||||
.savePrefs(payload)
|
||||
.then(() => {
|
||||
baselineRef.current = payload;
|
||||
setSaveState("saved");
|
||||
saveMsgTimer.current = window.setTimeout(() => setSaveState("idle"), 2500);
|
||||
})
|
||||
.catch(() => {
|
||||
// api.req() already surfaces the failure; flag the indicator so the user sees it didn't take.
|
||||
setSaveState("error");
|
||||
saveMsgTimer.current = window.setTimeout(() => setSaveState("idle"), 4000);
|
||||
});
|
||||
}, 500);
|
||||
return () => window.clearTimeout(saveTimer.current);
|
||||
}, [editablePrefs]);
|
||||
// Clear pending timers on unmount so a debounced PUT or a fade timer never fires setState on a dead
|
||||
// component.
|
||||
useEffect(
|
||||
() => () => {
|
||||
window.clearTimeout(saveTimer.current);
|
||||
window.clearTimeout(saveMsgTimer.current);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// On login / account switch, adopt the server-stored prefs into the live states AND the auto-save
|
||||
// baseline (so adopting them doesn't echo a save back).
|
||||
useEffect(() => {
|
||||
const prefs = me?.preferences;
|
||||
if (!prefs) return;
|
||||
const adopted: EditablePrefs = {
|
||||
theme: prefs.theme ? { ...DEFAULT_THEME, ...prefs.theme } : loadLocalTheme(),
|
||||
performanceMode:
|
||||
typeof prefs.performanceMode === "boolean"
|
||||
? prefs.performanceMode
|
||||
: getAccountRaw(PERF_KEY) === "1",
|
||||
hints: typeof prefs.hints === "boolean" ? prefs.hints : hintsEnabled(),
|
||||
notifications: prefs.notifications
|
||||
? { ...getNotifSettings(), ...prefs.notifications }
|
||||
: getNotifSettings(),
|
||||
};
|
||||
setThemeState(adopted.theme);
|
||||
saveLocalTheme(adopted.theme);
|
||||
setPerf(adopted.performanceMode);
|
||||
setHints(adopted.hints);
|
||||
setNotif(adopted.notifications);
|
||||
baselineRef.current = adopted;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [me?.id]);
|
||||
|
||||
// Theme applies live (and caches to localStorage); the auto-save effect persists it to the server.
|
||||
const setTheme = useCallback((next: ThemePrefs) => {
|
||||
setThemeState(next);
|
||||
saveLocalTheme(next);
|
||||
}, []);
|
||||
|
||||
const value = useMemo<PrefsController>(
|
||||
() => ({ theme, setTheme, perf, setPerf, hints, setHints, notif, setNotif, saveState }),
|
||||
[theme, setTheme, perf, hints, notif, saveState],
|
||||
);
|
||||
|
||||
return <PrefsContext.Provider value={value}>{children}</PrefsContext.Provider>;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { Section, SettingRow, Switch } from "./ui/form";
|
||||
import { type SaveState } from "./ui/DraftSaveBar";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
import { useWizard } from "./WizardProvider";
|
||||
import { usePrefs } from "./PrefsProvider";
|
||||
|
||||
// The Settings page edits server-persisted preferences. Each change applies locally for instant
|
||||
// preview AND auto-saves (debounced) — no draft, no Save button; a small indicator flashes the
|
||||
@@ -40,8 +41,10 @@ const TABS: { id: TabId; icon: typeof Monitor }[] = [
|
||||
// Settings as a page (Design B): rendered in the main content area, not an overlay. It keeps
|
||||
// the frosted `.glass` surface so it still refracts the ambient backdrop. The page title is
|
||||
// shown by the Header; the tab rail stays as in-page sub-navigation.
|
||||
export default function SettingsPanel({ me, prefs }: { me: Me; prefs: PrefsController }) {
|
||||
export default function SettingsPanel({ me }: { me: Me }) {
|
||||
const { t } = useTranslation();
|
||||
// The editable prefs (theme/perf/hints/notifications + save state) live in PrefsProvider.
|
||||
const prefs = usePrefs();
|
||||
// "Connect YouTube" onboarding lives in WizardProvider (the Account tab's reconnect button).
|
||||
const { openWizard } = useWizard();
|
||||
const [tabRaw, setTab] = useAccountPersistedState(LS.settingsTab, "appearance");
|
||||
|
||||
@@ -9,6 +9,7 @@ import { PlaylistsProvider } from "./components/PlaylistsProvider";
|
||||
import { PlexProvider } from "./components/PlexProvider";
|
||||
import { ChannelsProvider } from "./components/ChannelsProvider";
|
||||
import { FeedViewProvider } from "./components/FeedViewProvider";
|
||||
import { PrefsProvider } from "./components/PrefsProvider";
|
||||
import { WizardProvider } from "./components/WizardProvider";
|
||||
import "./i18n";
|
||||
import "./index.css";
|
||||
@@ -48,9 +49,11 @@ const root =
|
||||
<PlexProvider>
|
||||
<ChannelsProvider>
|
||||
<FeedViewProvider>
|
||||
<WizardProvider>
|
||||
<App />
|
||||
</WizardProvider>
|
||||
<PrefsProvider>
|
||||
<WizardProvider>
|
||||
<App />
|
||||
</WizardProvider>
|
||||
</PrefsProvider>
|
||||
</FeedViewProvider>
|
||||
</ChannelsProvider>
|
||||
</PlexProvider>
|
||||
|
||||
Reference in New Issue
Block a user