1
This commit is contained in:
@@ -3,22 +3,34 @@ 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() {
|
||||
const [musicVolume, setMusicVolumeState] = useState(DEFAULT_MUSIC_VOLUME);
|
||||
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<string | null>(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<AbortController | null>(null);
|
||||
const persistControllerRef = useRef<AbortController | null>(null);
|
||||
const persistRequestIdRef = useRef(0);
|
||||
const [isRemoteSyncReady, setIsRemoteSyncReady] = useState(false);
|
||||
|
||||
const abortActivePersist = useCallback(() => {
|
||||
persistControllerRef.current?.abort();
|
||||
@@ -27,21 +39,47 @@ export function useGameSettings() {
|
||||
}, []);
|
||||
|
||||
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);
|
||||
@@ -52,6 +90,7 @@ export function useGameSettings() {
|
||||
hydrateControllerRef.current = null;
|
||||
setIsHydratingSettings(false);
|
||||
setHasHydratedSettings(true);
|
||||
setIsRemoteSyncReady(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -61,7 +100,7 @@ export function useGameSettings() {
|
||||
hydrateControllerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [abortActivePersist, authenticatedUserId]);
|
||||
|
||||
useEffect(() => () => {
|
||||
hydrateControllerRef.current?.abort();
|
||||
@@ -70,11 +109,14 @@ export function useGameSettings() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasHydratedSettings) {
|
||||
if (!authenticatedUserId || !hasHydratedSettings || !isRemoteSyncReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastSyncedVolumeRef.current === musicVolume) {
|
||||
if (
|
||||
lastSyncedVolumeRef.current === musicVolume
|
||||
&& lastSyncedThemeRef.current === platformTheme
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -88,17 +130,32 @@ export function useGameSettings() {
|
||||
setIsPersistingSettings(true);
|
||||
setSettingsError(null);
|
||||
|
||||
void putSettings({ musicVolume }, { signal: controller.signal })
|
||||
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)) {
|
||||
@@ -120,15 +177,28 @@ export function useGameSettings() {
|
||||
}, SETTINGS_SYNC_DELAY_MS);
|
||||
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [abortActivePersist, hasHydratedSettings, musicVolume]);
|
||||
}, [
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user