From 3de8a35df4410bd485f9655458d7f53c2503ae63 Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Sat, 25 Oct 2025 12:07:15 -0600 Subject: [PATCH] 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 --- .../atoms/settings/PlaybackSettingsView.tsx | 9 ++++---- src/hooks/auth/useAuthData.ts | 11 +++++++++ src/hooks/auth/useAuthRestore.ts | 9 ++++++-- src/hooks/useEmbedOrderState.ts | 4 +++- src/pages/Settings.tsx | 23 ++++--------------- src/stores/auth/index.ts | 8 +++++++ src/stores/preferences/index.tsx | 8 +++++++ src/stores/subtitles/SettingsSyncer.tsx | 5 +++- 8 files changed, 51 insertions(+), 26 deletions(-) diff --git a/src/components/player/atoms/settings/PlaybackSettingsView.tsx b/src/components/player/atoms/settings/PlaybackSettingsView.tsx index 0d38840b..17ec1d82 100644 --- a/src/components/player/atoms/settings/PlaybackSettingsView.tsx +++ b/src/components/player/atoms/settings/PlaybackSettingsView.tsx @@ -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( diff --git a/src/hooks/auth/useAuthData.ts b/src/hooks/auth/useAuthData.ts index 38339071..07d022fb 100644 --- a/src/hooks/auth/useAuthData.ts +++ b/src/hooks/auth/useAuthData.ts @@ -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, ], ); diff --git a/src/hooks/auth/useAuthRestore.ts b/src/hooks/auth/useAuthRestore.ts index f0c8bb02..4c113403 100644 --- a/src/hooks/auth/useAuthRestore.ts +++ b/src/hooks/auth/useAuthRestore.ts @@ -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; diff --git a/src/hooks/useEmbedOrderState.ts b/src/hooks/useEmbedOrderState.ts index 11897f03..c37e5d45 100644 --- a/src/hooks/useEmbedOrderState.ts +++ b/src/hooks/useEmbedOrderState.ts @@ -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, diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index af65bdab..5be12736 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -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() {

{t("settings.unsaved")}

diff --git a/src/stores/auth/index.ts b/src/stores/auth/index.ts index 0a3c007a..28a5eae1 100644 --- a/src/stores/auth/index.ts +++ b/src/stores/auth/index.ts @@ -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) { diff --git a/src/stores/preferences/index.tsx b/src/stores/preferences/index.tsx index 256227ed..fdcdc791 100644 --- a/src/stores/preferences/index.tsx +++ b/src/stores/preferences/index.tsx @@ -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", diff --git a/src/stores/subtitles/SettingsSyncer.tsx b/src/stores/subtitles/SettingsSyncer.tsx index ca3c0817..0505ba76 100644 --- a/src/stores/subtitles/SettingsSyncer.tsx +++ b/src/stores/subtitles/SettingsSyncer.tsx @@ -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; }