diff --git a/src/renderer/src/data/PreferenceService.ts b/src/renderer/src/data/PreferenceService.ts index b9a12d8a73..2ad2db135a 100644 --- a/src/renderer/src/data/PreferenceService.ts +++ b/src/renderer/src/data/PreferenceService.ts @@ -105,10 +105,11 @@ export class PreferenceService { const value = await window.api.preference.get(key) this.cache[key] = value + // since not cached, notify change listeners to receive the value + this.notifyChangeListeners(key) + // Auto-subscribe to this key for future updates - if (!this.subscribedKeys.has(key)) { - await this.subscribeToKeyInternal(key) - } + await this.subscribeToKeyInternal(key) return value } catch (error) { @@ -226,13 +227,10 @@ export class PreferenceService { // Update cache with new results for (const [key, value] of Object.entries(uncachedResults)) { this.cache[key as PreferenceKeyType] = value - } - // Auto-subscribe to new keys - for (const key of uncachedKeys) { - if (!this.subscribedKeys.has(key)) { - await this.subscribeToKeyInternal(key) - } + this.notifyChangeListeners(key) + + await this.subscribeToKeyInternal(key as PreferenceKeyType) } return { ...cachedResults, ...uncachedResults } @@ -346,14 +344,14 @@ export class PreferenceService { * Subscribe to a specific key for change notifications */ private async subscribeToKeyInternal(key: PreferenceKeyType): Promise { - if (!this.subscribedKeys.has(key)) { - try { - await window.api.preference.subscribe([key]) - this.subscribedKeys.add(key) - logger.debug(`Subscribed to preference key: ${key}`) - } catch (error) { - logger.error(`Failed to subscribe to preference key ${key}:`, error as Error) - } + if (this.subscribedKeys.has(key)) return + + try { + await window.api.preference.subscribe([key]) + this.subscribedKeys.add(key) + logger.debug(`Subscribed to preference key: ${key}`) + } catch (error) { + logger.error(`Failed to subscribe to preference key ${key}:`, error as Error) } } @@ -370,7 +368,7 @@ export class PreferenceService { /** * Subscribe to specific key changes (for useSyncExternalStore) */ - public subscribeKeyChange = + public subscribeChange = (key: PreferenceKeyType) => (callback: () => void): (() => void) => { if (!this.keyChangeListeners.has(key)) { @@ -417,10 +415,11 @@ export class PreferenceService { for (const [key, value] of Object.entries(allPreferences)) { this.cache[key as PreferenceKeyType] = value + // Notify change listeners for the loaded value + this.notifyChangeListeners(key) + // Auto-subscribe to this key if not already subscribed - if (!this.subscribedKeys.has(key)) { - await this.subscribeToKeyInternal(key as PreferenceKeyType) - } + await this.subscribeToKeyInternal(key as PreferenceKeyType) } this.fullCacheLoaded = true diff --git a/src/renderer/src/data/hooks/usePreference.ts b/src/renderer/src/data/hooks/usePreference.ts index ca2620ad15..de161a5a26 100644 --- a/src/renderer/src/data/hooks/usePreference.ts +++ b/src/renderer/src/data/hooks/usePreference.ts @@ -1,5 +1,6 @@ import { preferenceService } from '@data/PreferenceService' import { loggerService } from '@logger' +import { DefaultPreferences } from '@shared/data/preferences' import type { PreferenceDefaultScopeType, PreferenceKeyType, PreferenceUpdateOptions } from '@shared/data/types' import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react' @@ -81,22 +82,26 @@ const logger = loggerService.withContext('usePreference') export function usePreference( key: K, options: PreferenceUpdateOptions = { optimistic: true } -): [PreferenceDefaultScopeType[K] | undefined, (value: PreferenceDefaultScopeType[K]) => Promise] { - // Subscribe to changes for this specific preference - const value = useSyncExternalStore( - useCallback((callback) => preferenceService.subscribeKeyChange(key)(callback), [key]), +): [PreferenceDefaultScopeType[K], (value: PreferenceDefaultScopeType[K]) => Promise] { + // Subscribe to changes for this specific preference (raw value including undefined) + const rawValue = useSyncExternalStore( + useCallback((callback) => preferenceService.subscribeChange(key)(callback), [key]), useCallback(() => preferenceService.getCachedValue(key), [key]), () => undefined // SSR snapshot (not used in Electron context) ) // Load initial value asynchronously if not cached useEffect(() => { - if (value === undefined && !preferenceService.isCached(key)) { + if (rawValue === undefined) { preferenceService.get(key).catch((error) => { logger.error(`Failed to load initial preference ${key}:`, error as Error) }) } - }, [key, value]) + }, [key, rawValue]) + + // Convert undefined to default value for external consumption + const exposedValue = + rawValue !== undefined ? rawValue : (DefaultPreferences.default[key] as PreferenceDefaultScopeType[K]) // Memoized setter function const setValue = useCallback( @@ -111,7 +116,7 @@ export function usePreference( [key, options] ) - return [value, setValue] + return [exposedValue, setValue] } /** @@ -247,7 +252,7 @@ export function useMultiplePreferences) => Promise ] { // Create stable key dependencies @@ -256,11 +261,11 @@ export function useMultiplePreferences>({}) - const allValues = useSyncExternalStore( + const rawValues = useSyncExternalStore( useCallback( (callback: () => void) => { // Subscribe to all keys and aggregate the unsubscribe functions - const unsubscribeFunctions = keyList.map((key) => preferenceService.subscribeKeyChange(key)(callback)) + const unsubscribeFunctions = keyList.map((key) => preferenceService.subscribeChange(key)(callback)) return () => { unsubscribeFunctions.forEach((unsubscribe) => unsubscribe()) @@ -296,14 +301,31 @@ export function useMultiplePreferences { - const uncachedKeys = keyList.filter((key) => !preferenceService.isCached(key)) + // Find keys that need loading (either not cached or rawValue is undefined) + const uncachedKeys = keyList.filter((key) => { + // Find the local key for this preference key + const localKey = Object.keys(keys).find((k) => keys[k] === key) + const rawValue = localKey ? rawValues[localKey] : undefined + + return rawValue === undefined && !preferenceService.isCached(key) + }) if (uncachedKeys.length > 0) { preferenceService.getMultiple(uncachedKeys).catch((error) => { logger.error('Failed to load initial preferences:', error as Error) }) } - }, [keyList]) + }, [keyList, rawValues, keys]) + + // Convert raw values (including undefined) to exposed values (with defaults) + const exposedValues = useMemo(() => { + const result: Record = {} + for (const [localKey, prefKey] of Object.entries(keys)) { + const rawValue = rawValues[localKey] + result[localKey] = rawValue !== undefined ? rawValue : DefaultPreferences.default[prefKey] + } + return result + }, [keys, rawValues]) // Memoized batch update function const updateValues = useCallback( @@ -328,7 +350,7 @@ export function useMultiplePreferences { const { theme } = useTheme() const { t } = useTranslation() @@ -65,6 +68,18 @@ const SelectionAssistantSettings: FC = () => { const [filterList, setFilterList] = usePreference('feature.selection.filter_list') const [actionItems, setActionItems] = usePreference('feature.selection.action_items') + logger.debug(`selectionEnabled: ${selectionEnabled}`) + logger.debug(`triggerMode: ${triggerMode}`) + logger.debug(`isCompact: ${isCompact}`) + logger.debug(`isAutoClose: ${isAutoClose}`) + logger.debug(`isAutoPin: ${isAutoPin}`) + logger.debug(`isFollowToolbar: ${isFollowToolbar}`) + logger.debug(`isRemeberWinSize: ${isRemeberWinSize}`) + logger.debug(`actionWindowOpacity: ${actionWindowOpacity}`) + logger.debug(`filterMode: ${filterMode}`) + logger.debug(`filterList: ${filterList}`) + logger.debug(`actionItems: ${actionItems}`) + const isSupportedOS = isWin || isMac const [isFilterListModalOpen, setIsFilterListModalOpen] = useState(false) diff --git a/src/renderer/src/windows/dataRefactorTest/components/PreferenceHookTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/PreferenceHookTests.tsx index f622a9fad2..7620565004 100644 --- a/src/renderer/src/windows/dataRefactorTest/components/PreferenceHookTests.tsx +++ b/src/renderer/src/windows/dataRefactorTest/components/PreferenceHookTests.tsx @@ -36,7 +36,7 @@ const PreferenceHookTests: React.FC = () => { const testSubscriptions = () => { // Test subscription behavior - const unsubscribe = preferenceService.subscribeKeyChange('app.theme.mode')(() => { + const unsubscribe = preferenceService.subscribeChange('app.theme.mode')(() => { setSubscriptionCount((prev) => prev + 1) })