This commit is contained in:
2026-04-19 20:33:18 +08:00
parent 692643136f
commit 67c584b4df
123 changed files with 11898 additions and 4082 deletions

View File

@@ -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,