import {useCallback, useEffect, useRef, useState} from 'react'; import { clampVolume, DEFAULT_MUSIC_VOLUME, normalizePlatformTheme, readSavedSettings, writeSavedSettings, } from '../persistence/gameSettingsStorage'; import { isAbortError } from '../services/apiClient'; import { getSettings, putSettings } from '../services/storageService'; const SETTINGS_SYNC_DELAY_MS = 180; export function useGameSettings(authenticatedUserId: string | null = null) { const [musicVolume, setMusicVolumeState] = useState( () => readSavedSettings().musicVolume, ); const [platformTheme, setPlatformThemeState] = useState( () => readSavedSettings().platformTheme, ); const [hasHydratedSettings, setHasHydratedSettings] = useState(false); const [isHydratingSettings, setIsHydratingSettings] = useState(true); const [isPersistingSettings, setIsPersistingSettings] = useState(false); const [settingsError, setSettingsError] = useState(null); const lastSyncedVolumeRef = useRef(DEFAULT_MUSIC_VOLUME); const currentVolumeRef = useRef(readSavedSettings().musicVolume); const lastSyncedThemeRef = useRef(readSavedSettings().platformTheme); const currentThemeRef = useRef(readSavedSettings().platformTheme); const hydrateControllerRef = useRef(null); const persistControllerRef = useRef(null); const persistRequestIdRef = useRef(0); const [isRemoteSyncReady, setIsRemoteSyncReady] = useState(false); const abortActivePersist = useCallback(() => { persistControllerRef.current?.abort(); persistControllerRef.current = null; setIsPersistingSettings(false); }, []); useEffect(() => { currentVolumeRef.current = musicVolume; currentThemeRef.current = platformTheme; writeSavedSettings({ musicVolume, platformTheme }); }, [musicVolume, platformTheme]); useEffect(() => { hydrateControllerRef.current?.abort(); hydrateControllerRef.current = null; abortActivePersist(); if (!authenticatedUserId) { lastSyncedVolumeRef.current = currentVolumeRef.current; lastSyncedThemeRef.current = currentThemeRef.current; setSettingsError(null); setIsHydratingSettings(false); setHasHydratedSettings(true); setIsRemoteSyncReady(true); return; } const controller = new AbortController(); hydrateControllerRef.current = controller; setIsRemoteSyncReady(false); setHasHydratedSettings(false); setIsHydratingSettings(true); void getSettings({ signal: controller.signal }) .then((settings) => { const nextVolume = clampVolume(settings.musicVolume); const nextPlatformTheme = normalizePlatformTheme(settings.platformTheme); lastSyncedVolumeRef.current = nextVolume; lastSyncedThemeRef.current = nextPlatformTheme; setMusicVolumeState(nextVolume); setPlatformThemeState(nextPlatformTheme); setSettingsError(null); }) .catch((error) => { if (isAbortError(error)) { return; } lastSyncedVolumeRef.current = currentVolumeRef.current; const message = error instanceof Error ? error.message : '读取远端设置失败'; setSettingsError(message); console.warn('[useGameSettings] failed to load remote settings', error); }) .finally(() => { if (hydrateControllerRef.current === controller) { hydrateControllerRef.current = null; setIsHydratingSettings(false); setHasHydratedSettings(true); setIsRemoteSyncReady(true); } }); return () => { controller.abort(); if (hydrateControllerRef.current === controller) { hydrateControllerRef.current = null; } }; }, [abortActivePersist, authenticatedUserId]); useEffect(() => () => { hydrateControllerRef.current?.abort(); persistControllerRef.current?.abort(); persistControllerRef.current = null; }, []); useEffect(() => { if (!authenticatedUserId || !hasHydratedSettings || !isRemoteSyncReady) { return; } if ( lastSyncedVolumeRef.current === musicVolume && lastSyncedThemeRef.current === platformTheme ) { return; } const timeoutId = window.setTimeout(() => { abortActivePersist(); const requestId = persistRequestIdRef.current + 1; persistRequestIdRef.current = requestId; const controller = new AbortController(); persistControllerRef.current = controller; setIsPersistingSettings(true); setSettingsError(null); void putSettings( { musicVolume, platformTheme, }, { signal: controller.signal }, ) .then((settings) => { if (persistRequestIdRef.current !== requestId) { return; } const nextVolume = clampVolume(settings.musicVolume); const nextPlatformTheme = normalizePlatformTheme( settings.platformTheme, ); lastSyncedVolumeRef.current = nextVolume; lastSyncedThemeRef.current = nextPlatformTheme; setMusicVolumeState((currentValue) => currentValue === nextVolume ? currentValue : nextVolume, ); setPlatformThemeState((currentValue) => currentValue === nextPlatformTheme ? currentValue : nextPlatformTheme, ); }) .catch((error) => { if (isAbortError(error)) { return; } const message = error instanceof Error ? error.message : '保存远端设置失败'; if (persistRequestIdRef.current === requestId) { setSettingsError(message); } console.warn('[useGameSettings] failed to persist remote settings', error); }) .finally(() => { if (persistControllerRef.current === controller) { persistControllerRef.current = null; setIsPersistingSettings(false); } }); }, SETTINGS_SYNC_DELAY_MS); return () => window.clearTimeout(timeoutId); }, [ abortActivePersist, authenticatedUserId, hasHydratedSettings, isRemoteSyncReady, musicVolume, platformTheme, ]); const setMusicVolume = useCallback((value: number) => { setMusicVolumeState(clampVolume(value)); }, []); const setPlatformTheme = useCallback((value: 'light' | 'dark') => { setPlatformThemeState(normalizePlatformTheme(value)); }, []); return { musicVolume, setMusicVolume, platformTheme, setPlatformTheme, hasHydratedSettings, isHydratingSettings, isPersistingSettings, settingsError, }; }