mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-20 23:22:05 +08:00
feat(preferences): enhance preference subscription and notification system
- Updated `PreferenceService` to notify change listeners immediately upon fetching uncached values. - Refactored subscription logic to simplify auto-subscribing to preference keys. - Modified `usePreference` and `useMultiplePreferences` hooks to improve handling of default values and subscription behavior. - Enhanced `SelectionAssistantSettings` to include detailed logging of preference states for better debugging.
This commit is contained in:
parent
9bde833419
commit
3dd2bc1a40
@ -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)
|
||||
}
|
||||
|
||||
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,7 +344,8 @@ export class PreferenceService {
|
||||
* Subscribe to a specific key for change notifications
|
||||
*/
|
||||
private async subscribeToKeyInternal(key: PreferenceKeyType): Promise<void> {
|
||||
if (!this.subscribedKeys.has(key)) {
|
||||
if (this.subscribedKeys.has(key)) return
|
||||
|
||||
try {
|
||||
await window.api.preference.subscribe([key])
|
||||
this.subscribedKeys.add(key)
|
||||
@ -355,7 +354,6 @@ export class PreferenceService {
|
||||
logger.error(`Failed to subscribe to preference key ${key}:`, error as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to global preference changes (for useSyncExternalStore)
|
||||
@ -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,11 +415,12 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
this.fullCacheLoaded = true
|
||||
logger.info(`Loaded all ${Object.keys(allPreferences).length} preferences into cache`)
|
||||
|
||||
@ -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<K extends PreferenceKeyType>(
|
||||
key: K,
|
||||
options: PreferenceUpdateOptions = { optimistic: true }
|
||||
): [PreferenceDefaultScopeType[K] | undefined, (value: PreferenceDefaultScopeType[K]) => Promise<void>] {
|
||||
// Subscribe to changes for this specific preference
|
||||
const value = useSyncExternalStore(
|
||||
useCallback((callback) => preferenceService.subscribeKeyChange(key)(callback), [key]),
|
||||
): [PreferenceDefaultScopeType[K], (value: PreferenceDefaultScopeType[K]) => Promise<void>] {
|
||||
// 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<K extends PreferenceKeyType>(
|
||||
[key, options]
|
||||
)
|
||||
|
||||
return [value, setValue]
|
||||
return [exposedValue, setValue]
|
||||
}
|
||||
|
||||
/**
|
||||
@ -247,7 +252,7 @@ export function useMultiplePreferences<T extends Record<string, PreferenceKeyTyp
|
||||
keys: T,
|
||||
options: PreferenceUpdateOptions = { optimistic: true }
|
||||
): [
|
||||
{ [P in keyof T]: PreferenceDefaultScopeType[T[P]] | undefined },
|
||||
{ [P in keyof T]: PreferenceDefaultScopeType[T[P]] },
|
||||
(updates: Partial<{ [P in keyof T]: PreferenceDefaultScopeType[T[P]] }>) => Promise<void>
|
||||
] {
|
||||
// Create stable key dependencies
|
||||
@ -256,11 +261,11 @@ export function useMultiplePreferences<T extends Record<string, PreferenceKeyTyp
|
||||
// Cache the last snapshot to avoid infinite loops
|
||||
const lastSnapshotRef = useRef<Record<string, any>>({})
|
||||
|
||||
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<T extends Record<string, PreferenceKeyTyp
|
||||
|
||||
// Load initial values asynchronously if not cached
|
||||
useEffect(() => {
|
||||
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<string, any> = {}
|
||||
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<T extends Record<string, PreferenceKeyTyp
|
||||
)
|
||||
|
||||
// Type-cast the values to the expected shape
|
||||
const typedValues = allValues as { [P in keyof T]: PreferenceDefaultScopeType[T[P]] | undefined }
|
||||
const typedValues = exposedValues as { [P in keyof T]: PreferenceDefaultScopeType[T[P]] }
|
||||
|
||||
return [typedValues, updateValues]
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { usePreference } from '@data/hooks/usePreference'
|
||||
import { loggerService } from '@logger'
|
||||
import { isMac, isWin } from '@renderer/config/constant'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { getSelectionDescriptionLabel } from '@renderer/i18n/label'
|
||||
@ -25,6 +26,8 @@ import MacProcessTrustHintModal from './components/MacProcessTrustHintModal'
|
||||
import SelectionActionsList from './components/SelectionActionsList'
|
||||
import SelectionFilterListModal from './components/SelectionFilterListModal'
|
||||
|
||||
const logger = loggerService.withContext('Settings:SelectionAssistant')
|
||||
|
||||
const SelectionAssistantSettings: FC = () => {
|
||||
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)
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user