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)
|
const value = await window.api.preference.get(key)
|
||||||
this.cache[key] = value
|
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
|
// Auto-subscribe to this key for future updates
|
||||||
if (!this.subscribedKeys.has(key)) {
|
await this.subscribeToKeyInternal(key)
|
||||||
await this.subscribeToKeyInternal(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
return value
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -226,13 +227,10 @@ export class PreferenceService {
|
|||||||
// Update cache with new results
|
// Update cache with new results
|
||||||
for (const [key, value] of Object.entries(uncachedResults)) {
|
for (const [key, value] of Object.entries(uncachedResults)) {
|
||||||
this.cache[key as PreferenceKeyType] = value
|
this.cache[key as PreferenceKeyType] = value
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-subscribe to new keys
|
this.notifyChangeListeners(key)
|
||||||
for (const key of uncachedKeys) {
|
|
||||||
if (!this.subscribedKeys.has(key)) {
|
await this.subscribeToKeyInternal(key as PreferenceKeyType)
|
||||||
await this.subscribeToKeyInternal(key)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...cachedResults, ...uncachedResults }
|
return { ...cachedResults, ...uncachedResults }
|
||||||
@ -346,14 +344,14 @@ export class PreferenceService {
|
|||||||
* Subscribe to a specific key for change notifications
|
* Subscribe to a specific key for change notifications
|
||||||
*/
|
*/
|
||||||
private async subscribeToKeyInternal(key: PreferenceKeyType): Promise<void> {
|
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])
|
try {
|
||||||
this.subscribedKeys.add(key)
|
await window.api.preference.subscribe([key])
|
||||||
logger.debug(`Subscribed to preference key: ${key}`)
|
this.subscribedKeys.add(key)
|
||||||
} catch (error) {
|
logger.debug(`Subscribed to preference key: ${key}`)
|
||||||
logger.error(`Failed to subscribe to preference key ${key}:`, error as Error)
|
} 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)
|
* Subscribe to specific key changes (for useSyncExternalStore)
|
||||||
*/
|
*/
|
||||||
public subscribeKeyChange =
|
public subscribeChange =
|
||||||
(key: PreferenceKeyType) =>
|
(key: PreferenceKeyType) =>
|
||||||
(callback: () => void): (() => void) => {
|
(callback: () => void): (() => void) => {
|
||||||
if (!this.keyChangeListeners.has(key)) {
|
if (!this.keyChangeListeners.has(key)) {
|
||||||
@ -417,10 +415,11 @@ export class PreferenceService {
|
|||||||
for (const [key, value] of Object.entries(allPreferences)) {
|
for (const [key, value] of Object.entries(allPreferences)) {
|
||||||
this.cache[key as PreferenceKeyType] = value
|
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
|
// 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
|
this.fullCacheLoaded = true
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { preferenceService } from '@data/PreferenceService'
|
import { preferenceService } from '@data/PreferenceService'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
|
import { DefaultPreferences } from '@shared/data/preferences'
|
||||||
import type { PreferenceDefaultScopeType, PreferenceKeyType, PreferenceUpdateOptions } from '@shared/data/types'
|
import type { PreferenceDefaultScopeType, PreferenceKeyType, PreferenceUpdateOptions } from '@shared/data/types'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
|
||||||
|
|
||||||
@ -81,22 +82,26 @@ const logger = loggerService.withContext('usePreference')
|
|||||||
export function usePreference<K extends PreferenceKeyType>(
|
export function usePreference<K extends PreferenceKeyType>(
|
||||||
key: K,
|
key: K,
|
||||||
options: PreferenceUpdateOptions = { optimistic: true }
|
options: PreferenceUpdateOptions = { optimistic: true }
|
||||||
): [PreferenceDefaultScopeType[K] | undefined, (value: PreferenceDefaultScopeType[K]) => Promise<void>] {
|
): [PreferenceDefaultScopeType[K], (value: PreferenceDefaultScopeType[K]) => Promise<void>] {
|
||||||
// Subscribe to changes for this specific preference
|
// Subscribe to changes for this specific preference (raw value including undefined)
|
||||||
const value = useSyncExternalStore(
|
const rawValue = useSyncExternalStore(
|
||||||
useCallback((callback) => preferenceService.subscribeKeyChange(key)(callback), [key]),
|
useCallback((callback) => preferenceService.subscribeChange(key)(callback), [key]),
|
||||||
useCallback(() => preferenceService.getCachedValue(key), [key]),
|
useCallback(() => preferenceService.getCachedValue(key), [key]),
|
||||||
() => undefined // SSR snapshot (not used in Electron context)
|
() => undefined // SSR snapshot (not used in Electron context)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Load initial value asynchronously if not cached
|
// Load initial value asynchronously if not cached
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value === undefined && !preferenceService.isCached(key)) {
|
if (rawValue === undefined) {
|
||||||
preferenceService.get(key).catch((error) => {
|
preferenceService.get(key).catch((error) => {
|
||||||
logger.error(`Failed to load initial preference ${key}:`, error as 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
|
// Memoized setter function
|
||||||
const setValue = useCallback(
|
const setValue = useCallback(
|
||||||
@ -111,7 +116,7 @@ export function usePreference<K extends PreferenceKeyType>(
|
|||||||
[key, options]
|
[key, options]
|
||||||
)
|
)
|
||||||
|
|
||||||
return [value, setValue]
|
return [exposedValue, setValue]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -247,7 +252,7 @@ export function useMultiplePreferences<T extends Record<string, PreferenceKeyTyp
|
|||||||
keys: T,
|
keys: T,
|
||||||
options: PreferenceUpdateOptions = { optimistic: true }
|
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>
|
(updates: Partial<{ [P in keyof T]: PreferenceDefaultScopeType[T[P]] }>) => Promise<void>
|
||||||
] {
|
] {
|
||||||
// Create stable key dependencies
|
// Create stable key dependencies
|
||||||
@ -256,11 +261,11 @@ export function useMultiplePreferences<T extends Record<string, PreferenceKeyTyp
|
|||||||
// Cache the last snapshot to avoid infinite loops
|
// Cache the last snapshot to avoid infinite loops
|
||||||
const lastSnapshotRef = useRef<Record<string, any>>({})
|
const lastSnapshotRef = useRef<Record<string, any>>({})
|
||||||
|
|
||||||
const allValues = useSyncExternalStore(
|
const rawValues = useSyncExternalStore(
|
||||||
useCallback(
|
useCallback(
|
||||||
(callback: () => void) => {
|
(callback: () => void) => {
|
||||||
// Subscribe to all keys and aggregate the unsubscribe functions
|
// 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 () => {
|
return () => {
|
||||||
unsubscribeFunctions.forEach((unsubscribe) => unsubscribe())
|
unsubscribeFunctions.forEach((unsubscribe) => unsubscribe())
|
||||||
@ -296,14 +301,31 @@ export function useMultiplePreferences<T extends Record<string, PreferenceKeyTyp
|
|||||||
|
|
||||||
// Load initial values asynchronously if not cached
|
// Load initial values asynchronously if not cached
|
||||||
useEffect(() => {
|
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) {
|
if (uncachedKeys.length > 0) {
|
||||||
preferenceService.getMultiple(uncachedKeys).catch((error) => {
|
preferenceService.getMultiple(uncachedKeys).catch((error) => {
|
||||||
logger.error('Failed to load initial preferences:', error as 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
|
// Memoized batch update function
|
||||||
const updateValues = useCallback(
|
const updateValues = useCallback(
|
||||||
@ -328,7 +350,7 @@ export function useMultiplePreferences<T extends Record<string, PreferenceKeyTyp
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Type-cast the values to the expected shape
|
// 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]
|
return [typedValues, updateValues]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { usePreference } from '@data/hooks/usePreference'
|
import { usePreference } from '@data/hooks/usePreference'
|
||||||
|
import { loggerService } from '@logger'
|
||||||
import { isMac, isWin } from '@renderer/config/constant'
|
import { isMac, isWin } from '@renderer/config/constant'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { getSelectionDescriptionLabel } from '@renderer/i18n/label'
|
import { getSelectionDescriptionLabel } from '@renderer/i18n/label'
|
||||||
@ -25,6 +26,8 @@ import MacProcessTrustHintModal from './components/MacProcessTrustHintModal'
|
|||||||
import SelectionActionsList from './components/SelectionActionsList'
|
import SelectionActionsList from './components/SelectionActionsList'
|
||||||
import SelectionFilterListModal from './components/SelectionFilterListModal'
|
import SelectionFilterListModal from './components/SelectionFilterListModal'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('Settings:SelectionAssistant')
|
||||||
|
|
||||||
const SelectionAssistantSettings: FC = () => {
|
const SelectionAssistantSettings: FC = () => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -65,6 +68,18 @@ const SelectionAssistantSettings: FC = () => {
|
|||||||
const [filterList, setFilterList] = usePreference('feature.selection.filter_list')
|
const [filterList, setFilterList] = usePreference('feature.selection.filter_list')
|
||||||
const [actionItems, setActionItems] = usePreference('feature.selection.action_items')
|
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 isSupportedOS = isWin || isMac
|
||||||
|
|
||||||
const [isFilterListModalOpen, setIsFilterListModalOpen] = useState(false)
|
const [isFilterListModalOpen, setIsFilterListModalOpen] = useState(false)
|
||||||
|
|||||||
@ -36,7 +36,7 @@ const PreferenceHookTests: React.FC = () => {
|
|||||||
|
|
||||||
const testSubscriptions = () => {
|
const testSubscriptions = () => {
|
||||||
// Test subscription behavior
|
// Test subscription behavior
|
||||||
const unsubscribe = preferenceService.subscribeKeyChange('app.theme.mode')(() => {
|
const unsubscribe = preferenceService.subscribeChange('app.theme.mode')(() => {
|
||||||
setSubscriptionCount((prev) => prev + 1)
|
setSubscriptionCount((prev) => prev + 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user