Prevent settings from loading empty and rewriting to backend

- No saves happen until backend settings are loaded and applied
- Automatic syncers wait for settings to be loaded before syncing
This commit is contained in:
Pas 2025-10-25 12:07:15 -06:00
parent bd21f7d104
commit 3de8a35df4
8 changed files with 51 additions and 26 deletions

View file

@ -193,6 +193,7 @@ export function PlaybackSettingsView({ id }: { id: string }) {
const account = useAuthStore((s) => s.account);
const backendUrl = useBackendUrl();
const settingsLoaded = usePreferencesStore((s) => s._settingsLoaded);
const allowAutoplay = useMemo(() => isAutoplayAllowed(), []);
const canShowAutoplay =
!isInWatchParty && allowAutoplay && !enableLowPerformanceMode;
@ -200,7 +201,7 @@ export function PlaybackSettingsView({ id }: { id: string }) {
// Save settings to backend
const saveThumbnailSetting = useCallback(
async (value: boolean) => {
if (!account || !backendUrl) return;
if (!account || !backendUrl || !settingsLoaded) return;
try {
await updateSettings(backendUrl, account, {
@ -210,12 +211,12 @@ export function PlaybackSettingsView({ id }: { id: string }) {
console.error("Failed to save thumbnail setting:", error);
}
},
[account, backendUrl],
[account, backendUrl, settingsLoaded],
);
const saveAutoplaySetting = useCallback(
async (value: boolean) => {
if (!account || !backendUrl) return;
if (!account || !backendUrl || !settingsLoaded) return;
try {
await updateSettings(backendUrl, account, {
@ -225,7 +226,7 @@ export function PlaybackSettingsView({ id }: { id: string }) {
console.error("Failed to save autoplay setting:", error);
}
},
[account, backendUrl],
[account, backendUrl, settingsLoaded],
);
const setPlaybackRate = useCallback(

View file

@ -33,6 +33,7 @@ export function useAuthData() {
);
const setFebboxKey = usePreferencesStore((s) => s.setFebboxKey);
const setRealDebridKey = usePreferencesStore((s) => s.setRealDebridKey);
const setSettingsLoaded = usePreferencesStore((s) => s.setSettingsLoaded);
const replaceBookmarks = useBookmarkStore((s) => s.replaceBookmarks);
const replaceItems = useProgressStore((s) => s.replaceItems);
@ -109,12 +110,14 @@ export function useAuthData() {
clearProgress();
clearGroupOrder();
setFebboxKey(null);
setSettingsLoaded(false);
}, [
removeAccount,
clearBookmarks,
clearProgress,
clearGroupOrder,
setFebboxKey,
setSettingsLoaded,
]);
const syncData = useCallback(
@ -215,6 +218,10 @@ export function useAuthData() {
if (settings.febboxKey !== undefined) {
setFebboxKey(settings.febboxKey);
} else {
// Only set to null if backend explicitly returns null/undefined
// Don't overwrite with defaults if backend doesn't have the setting
setFebboxKey(null);
}
if (settings.realDebridKey !== undefined) {
@ -246,6 +253,9 @@ export function useAuthData() {
if (settings.enableDoubleClickToSeek !== undefined) {
setEnableDoubleClickToSeek(settings.enableDoubleClickToSeek);
}
// Mark settings as loaded to prevent saving defaults
setSettingsLoaded(true);
},
[
replaceBookmarks,
@ -278,6 +288,7 @@ export function useAuthData() {
setHomeSectionOrder,
setManualSourceSelection,
setEnableDoubleClickToSeek,
setSettingsLoaded,
],
);

View file

@ -9,6 +9,7 @@ const AUTH_CHECK_INTERVAL = 12 * 60 * 60 * 1000;
export function useAuthRestore() {
const { account } = useAuthStore();
const { restore } = useAuth();
const setSettingsLoading = useAuthStore((s) => s.setSettingsLoading);
const hasRestored = useRef(false);
useInterval(() => {
@ -17,9 +18,13 @@ export function useAuthRestore() {
const result = useAsync(async () => {
if (hasRestored.current || !account) return;
await restore(account).finally(() => {
setSettingsLoading(true);
try {
await restore(account);
} finally {
setSettingsLoading(false);
hasRestored.current = true;
});
}
}, []); // no deps because we don't want to it ever rerun after the first time
return result;

View file

@ -8,6 +8,7 @@ import { usePreferencesStore } from "@/stores/preferences";
export function useEmbedOrderState() {
const account = useAuthStore((s) => s.account);
const backendUrl = useBackendUrl();
const settingsLoaded = usePreferencesStore((s) => s._settingsLoaded);
// Get current values from store
const embedOrder = usePreferencesStore((s) => s.embedOrder);
@ -51,7 +52,7 @@ export function useEmbedOrderState() {
// Save changes to backend and update store
const saveChanges = useCallback(async () => {
if (!account || !backendUrl) return;
if (!account || !backendUrl || !settingsLoaded) return;
try {
await updateSettings(backendUrl, account, {
@ -71,6 +72,7 @@ export function useEmbedOrderState() {
}, [
account,
backendUrl,
settingsLoaded,
localEmbedOrder,
localEnableEmbedOrder,
localDisabledEmbeds,

View file

@ -9,7 +9,7 @@ import {
encryptData,
} from "@/backend/accounts/crypto";
import { getSessions, updateSession } from "@/backend/accounts/sessions";
import { getSettings, updateSettings } from "@/backend/accounts/settings";
import { updateSettings } from "@/backend/accounts/settings";
import { editUser } from "@/backend/accounts/user";
import { getAllProviders } from "@/backend/providers/providers";
import { Button } from "@/components/buttons/Button";
@ -364,6 +364,7 @@ export function SettingsPage() {
const account = useAuthStore((s) => s.account);
const updateProfile = useAuthStore((s) => s.setAccountProfile);
const updateDeviceName = useAuthStore((s) => s.updateDeviceName);
const settingsLoaded = usePreferencesStore((s) => s._settingsLoaded);
const decryptedName = useMemo(() => {
if (!account) return "";
return decryptData(account.deviceName, base64ToBuffer(account.seed));
@ -374,21 +375,6 @@ export function SettingsPage() {
const { logout } = useAuth();
const user = useAuthStore();
useEffect(() => {
const loadSettings = async () => {
if (account && backendUrl) {
const settings = await getSettings(backendUrl, account);
if (settings.febboxKey) {
setFebboxKey(settings.febboxKey);
}
if (settings.realDebridKey) {
setRealDebridKey(settings.realDebridKey);
}
}
};
loadSettings();
}, [account, backendUrl, setFebboxKey, setRealDebridKey]);
const state = useSettingsState(
activeTheme,
appLanguage,
@ -459,7 +445,7 @@ export function SettingsPage() {
);
const saveChanges = useCallback(async () => {
if (account && backendUrl) {
if (account && backendUrl && settingsLoaded) {
if (
state.appLanguage.changed ||
state.theme.changed ||
@ -570,6 +556,7 @@ export function SettingsPage() {
}, [
account,
backendUrl,
settingsLoaded,
setEnableThumbnails,
setFebboxKey,
setRealDebridKey,
@ -705,7 +692,7 @@ export function SettingsPage() {
</SettingsLayout>
<Transition
animation="fade"
show={state.changed}
show={state.changed && settingsLoaded}
className="bg-settings-saveBar-background border-t border-settings-card-border/50 py-4 transition-opacity w-full fixed bottom-0 flex justify-between flex-col md:flex-row px-8 items-start md:items-center gap-3 z-[999]"
>
<p className="text-type-danger">{t("settings.unsaved")}</p>

View file

@ -22,6 +22,7 @@ interface AuthStore {
account: null | AccountWithToken;
backendUrl: null | string;
proxySet: null | string[];
settingsLoading: boolean;
removeAccount(): void;
setAccount(acc: AccountWithToken): void;
updateDeviceName(deviceName: string): void;
@ -29,6 +30,7 @@ interface AuthStore {
setAccountProfile(acc: Account["profile"]): void;
setBackendUrl(url: null | string): void;
setProxySet(urls: null | string[]): void;
setSettingsLoading(loading: boolean): void;
}
export const useAuthStore = create(
@ -37,6 +39,7 @@ export const useAuthStore = create(
account: null,
backendUrl: null,
proxySet: null,
settingsLoading: false,
setAccount(acc) {
set((s) => {
s.account = acc;
@ -57,6 +60,11 @@ export const useAuthStore = create(
s.proxySet = urls;
});
},
setSettingsLoading(loading) {
set((s) => {
s.settingsLoading = loading;
});
},
setAccountProfile(profile) {
set((s) => {
if (s.account) {

View file

@ -27,6 +27,7 @@ export interface PreferencesStore {
homeSectionOrder: string[];
manualSourceSelection: boolean;
enableDoubleClickToSeek: boolean;
_settingsLoaded: boolean;
setEnableThumbnails(v: boolean): void;
setEnableAutoplay(v: boolean): void;
@ -52,6 +53,7 @@ export interface PreferencesStore {
setHomeSectionOrder(v: string[]): void;
setManualSourceSelection(v: boolean): void;
setEnableDoubleClickToSeek(v: boolean): void;
setSettingsLoaded(loaded: boolean): void;
}
export const usePreferencesStore = create(
@ -75,6 +77,7 @@ export const usePreferencesStore = create(
proxyTmdb: false,
febboxKey: null,
realDebridKey: null,
_settingsLoaded: false,
enableLowPerformanceMode: false,
enableNativeSubtitles: false,
enableHoldToBoost: true,
@ -206,6 +209,11 @@ export const usePreferencesStore = create(
s.enableDoubleClickToSeek = v;
});
},
setSettingsLoaded(loaded) {
set((s) => {
s._settingsLoaded = loaded;
});
},
})),
{
name: "__MW::preferences",

View file

@ -4,6 +4,7 @@ import { updateSettings } from "@/backend/accounts/settings";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { useAuthStore } from "@/stores/auth";
import { useSubtitleStore } from "@/stores/subtitles";
import { usePreferencesStore } from "@/stores/preferences";
const syncIntervalMs = 5 * 1000;
@ -12,11 +13,13 @@ export function SettingsSyncer() {
(s) => s.importSubtitleLanguage,
);
const url = useBackendUrl();
const settingsLoading = useAuthStore((s) => s.settingsLoading);
useEffect(() => {
const interval = setInterval(() => {
(async () => {
if (!url) return;
if (settingsLoading) return; // Don't sync while settings are loading from backend
const state = useSubtitleStore.getState();
const user = useAuthStore.getState();
if (state.lastSync.lastSelectedLanguage === state.lastSelectedLanguage)
@ -33,7 +36,7 @@ export function SettingsSyncer() {
return () => {
clearInterval(interval);
};
}, [importSubtitleLanguage, url]);
}, [importSubtitleLanguage, url, settingsLoading]);
return null;
}