refactor(frontend): every stored key declares its kind, and preferences follow the account

storage.ts's registry gives each key a kind (pref, device, view, cache,
public); a key without one does not compile. A contract test refuses a key
spelled out elsewhere, an account preference read or written straight from
storage instead of through usePref, and an e2e seed of a key nobody reads.
The keys the next account's wipe keeps are the public ones.

The seven preferences kept only in the browser move onto usePref: start on
Home, the onboarding dismissal, the release banner's last version, the
channel manager's layout, the Plex playlist layout, the playlist rail's sort
and channel discovery's sort. usePref reads an older bare-string copy, and a
value only this browser held is saved to the server once, so the account's
other devices get it.

Performance mode is the device's: it is neither read from the account nor
saved to it.
This commit is contained in:
2026-10-20 23:04:48 +02:00
parent d6dada7fe5
commit f21cdfa156
29 changed files with 749 additions and 220 deletions
+7 -8
View File
@@ -24,7 +24,7 @@ import { SCHEMES, type Scheme, type ThemePrefs } from "../lib/theme";
import { api, HttpError, type Me } from "../lib/api";
import { displayName, roleKey } from "../lib/api/account";
import { notifyActionError } from "../lib/actionErrors";
import { LS, useAccountPersistedState, useTabPersistedState } from "../lib/storage";
import { LS, useTabPersistedState } from "../lib/storage";
import Avatar from "./ui/Avatar";
import { notify, type NotifSettings } from "../lib/notifications";
import Tooltip from "./ui/Tooltip";
@@ -35,7 +35,8 @@ import { type SaveState } from "./ui/DraftSaveBar";
import { useConfirm } from "./confirmContext";
import { StateMessage } from "./ui/QueryState";
import { useWizard } from "./wizardContext";
import { START_ON_HOME } from "./navigationInit";
import { START_ON_HOME_OPTIONS, START_ON_HOME_PREF } from "./navigationInit";
import { usePref } from "../lib/usePref";
import { usePrefs } from "./prefsContext";
import Tabs from "./ui/Tabs";
import { useTabPanel } from "./ui/tabPanel";
@@ -164,10 +165,8 @@ function Appearance({ prefs }: { prefs: PrefsController }) {
const { t } = useTranslation();
// Controlled by PrefsProvider: each change applies locally at once and auto-saves (debounced).
const { theme, setTheme, perf, setPerf, hints, setHints, railLabels, setRailLabels } = prefs;
// The landing page. Per-account localStorage, not a server preference like the rest of this
// tab: `loadInitialPage` reads it synchronously in NavigationProvider's initializer. The two stored
// values come from navigationInit, which is what reads them back at boot.
const [startOnHome, setStartOnHome] = useAccountPersistedState(LS.startOnHome, START_ON_HOME.on);
// The landing page: read at boot (NavigationProvider), so a change applies from the next start.
const [startOnHome, setStartOnHome] = usePref(START_ON_HOME_PREF, true, START_ON_HOME_OPTIONS);
return (
<>
@@ -242,8 +241,8 @@ function Appearance({ prefs }: { prefs: PrefsController }) {
<SwitchRow
label={t("settings.appearance.startOnHome")}
hint={t("settings.appearance.startOnHomeHint")}
checked={startOnHome !== START_ON_HOME.off}
onChange={(v) => setStartOnHome(v ? START_ON_HOME.on : START_ON_HOME.off)}
checked={startOnHome}
onChange={setStartOnHome}
/>
</Section>