mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 11:44:28 +08:00
refactor(preferences): enhance PreferenceService with public access modifiers and improve caching logic
This commit updates the PreferenceService by adding public access modifiers to several methods, improving code clarity and consistency. It also refines the caching logic to eliminate unnecessary type assertions and streamline the handling of preference values. Additionally, minor formatting adjustments are made for better readability across the service and related hooks.
This commit is contained in:
parent
b219e96544
commit
2860935e5b
@ -97,9 +97,8 @@ export class PreferenceService {
|
||||
throw new Error(`Preference ${key} not found in cache`)
|
||||
}
|
||||
|
||||
const db = dbService.getDb()
|
||||
|
||||
await db
|
||||
await dbService
|
||||
.getDb()
|
||||
.update(preferenceTable)
|
||||
.set({
|
||||
value: value as any
|
||||
|
||||
@ -10,7 +10,7 @@ const logger = loggerService.withContext('PreferenceService')
|
||||
*/
|
||||
export class PreferenceService {
|
||||
private static instance: PreferenceService
|
||||
private cache: Partial<PreferenceDefaultScopeType> = {}
|
||||
private cache: Record<string, any> = {}
|
||||
private listeners = new Set<() => void>()
|
||||
private keyListeners = new Map<string, Set<() => void>>()
|
||||
private changeListenerCleanup: (() => void) | null = null
|
||||
@ -24,7 +24,7 @@ export class PreferenceService {
|
||||
/**
|
||||
* Get the singleton instance of PreferenceService
|
||||
*/
|
||||
static getInstance(): PreferenceService {
|
||||
public static getInstance(): PreferenceService {
|
||||
if (!PreferenceService.instance) {
|
||||
PreferenceService.instance = new PreferenceService()
|
||||
}
|
||||
@ -68,7 +68,7 @@ export class PreferenceService {
|
||||
/**
|
||||
* Get a single preference value with caching
|
||||
*/
|
||||
async get<K extends PreferenceKeyType>(key: K): Promise<PreferenceDefaultScopeType[K]> {
|
||||
public async get<K extends PreferenceKeyType>(key: K): Promise<PreferenceDefaultScopeType[K]> {
|
||||
// Check cache first
|
||||
if (key in this.cache && this.cache[key] !== undefined) {
|
||||
return this.cache[key] as PreferenceDefaultScopeType[K]
|
||||
@ -95,7 +95,7 @@ export class PreferenceService {
|
||||
/**
|
||||
* Set a single preference value
|
||||
*/
|
||||
async set<K extends PreferenceKeyType>(key: K, value: PreferenceDefaultScopeType[K]): Promise<void> {
|
||||
public async set<K extends PreferenceKeyType>(key: K, value: PreferenceDefaultScopeType[K]): Promise<void> {
|
||||
try {
|
||||
await window.api.preference.set(key, value)
|
||||
|
||||
@ -113,14 +113,14 @@ export class PreferenceService {
|
||||
/**
|
||||
* Get multiple preferences at once
|
||||
*/
|
||||
async getMultiple(keys: PreferenceKeyType[]): Promise<Record<string, any>> {
|
||||
public async getMultiple(keys: PreferenceKeyType[]): Promise<Record<string, any>> {
|
||||
// Check which keys are already cached
|
||||
const cachedResults: Partial<PreferenceDefaultScopeType> = {}
|
||||
const uncachedKeys: PreferenceKeyType[] = []
|
||||
|
||||
for (const key of keys) {
|
||||
if (key in this.cache && this.cache[key] !== undefined) {
|
||||
;(cachedResults as any)[key] = this.cache[key]
|
||||
cachedResults[key] = this.cache[key]
|
||||
} else {
|
||||
uncachedKeys.push(key)
|
||||
}
|
||||
@ -133,7 +133,7 @@ export class PreferenceService {
|
||||
|
||||
// Update cache with new results
|
||||
for (const [key, value] of Object.entries(uncachedResults)) {
|
||||
;(this.cache as any)[key] = value
|
||||
this.cache[key as PreferenceKeyType] = value
|
||||
}
|
||||
|
||||
// Auto-subscribe to new keys
|
||||
@ -165,13 +165,13 @@ export class PreferenceService {
|
||||
/**
|
||||
* Set multiple preferences at once
|
||||
*/
|
||||
async setMultiple(updates: Partial<PreferenceDefaultScopeType>): Promise<void> {
|
||||
public async setMultiple(updates: Partial<PreferenceDefaultScopeType>): Promise<void> {
|
||||
try {
|
||||
await window.api.preference.setMultiple(updates)
|
||||
|
||||
// Update local cache for all updated values
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
;(this.cache as any)[key] = value
|
||||
this.cache[key as PreferenceKeyType] = value
|
||||
this.notifyListeners(key)
|
||||
}
|
||||
|
||||
@ -200,7 +200,7 @@ export class PreferenceService {
|
||||
/**
|
||||
* Subscribe to global preference changes (for useSyncExternalStore)
|
||||
*/
|
||||
subscribe = (callback: () => void): (() => void) => {
|
||||
public subscribe = (callback: () => void): (() => void) => {
|
||||
this.listeners.add(callback)
|
||||
return () => {
|
||||
this.listeners.delete(callback)
|
||||
@ -210,7 +210,7 @@ export class PreferenceService {
|
||||
/**
|
||||
* Subscribe to specific key changes (for useSyncExternalStore)
|
||||
*/
|
||||
subscribeToKey =
|
||||
public subscribeToKey =
|
||||
(key: PreferenceKeyType) =>
|
||||
(callback: () => void): (() => void) => {
|
||||
if (!this.keyListeners.has(key)) {
|
||||
@ -234,7 +234,7 @@ export class PreferenceService {
|
||||
/**
|
||||
* Get snapshot for useSyncExternalStore
|
||||
*/
|
||||
getSnapshot =
|
||||
public getSnapshot =
|
||||
<K extends PreferenceKeyType>(key: K) =>
|
||||
(): PreferenceDefaultScopeType[K] | undefined => {
|
||||
return this.cache[key]
|
||||
@ -243,14 +243,14 @@ export class PreferenceService {
|
||||
/**
|
||||
* Get cached value without async fetch
|
||||
*/
|
||||
getCachedValue<K extends PreferenceKeyType>(key: K): PreferenceDefaultScopeType[K] | undefined {
|
||||
public getCachedValue<K extends PreferenceKeyType>(key: K): PreferenceDefaultScopeType[K] | undefined {
|
||||
return this.cache[key]
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a preference is cached
|
||||
*/
|
||||
isCached(key: PreferenceKeyType): boolean {
|
||||
public isCached(key: PreferenceKeyType): boolean {
|
||||
return key in this.cache && this.cache[key] !== undefined
|
||||
}
|
||||
|
||||
@ -258,13 +258,13 @@ export class PreferenceService {
|
||||
* Load all preferences from main process at once
|
||||
* Provides optimal performance by loading complete preference set into memory
|
||||
*/
|
||||
async loadAll(): Promise<PreferenceDefaultScopeType> {
|
||||
public async loadAll(): Promise<PreferenceDefaultScopeType> {
|
||||
try {
|
||||
const allPreferences = await window.api.preference.getAll()
|
||||
|
||||
// Update local cache with all preferences
|
||||
for (const [key, value] of Object.entries(allPreferences)) {
|
||||
;(this.cache as any)[key] = value
|
||||
this.cache[key as PreferenceKeyType] = value
|
||||
|
||||
// Auto-subscribe to this key if not already subscribed
|
||||
if (!this.subscribedKeys.has(key)) {
|
||||
@ -285,7 +285,7 @@ export class PreferenceService {
|
||||
/**
|
||||
* Check if all preferences are loaded in cache
|
||||
*/
|
||||
isFullyCached(): boolean {
|
||||
public isFullyCached(): boolean {
|
||||
return this.fullCacheLoaded
|
||||
}
|
||||
|
||||
@ -308,7 +308,7 @@ export class PreferenceService {
|
||||
/**
|
||||
* Clear all cached preferences (for testing/debugging)
|
||||
*/
|
||||
clearCache(): void {
|
||||
public clearCache(): void {
|
||||
this.cache = {}
|
||||
this.fullCacheLoaded = false
|
||||
logger.debug('Preference cache cleared')
|
||||
@ -317,7 +317,7 @@ export class PreferenceService {
|
||||
/**
|
||||
* Cleanup service (call when shutting down)
|
||||
*/
|
||||
cleanup(): void {
|
||||
public cleanup(): void {
|
||||
if (this.changeListenerCleanup) {
|
||||
this.changeListenerCleanup()
|
||||
this.changeListenerCleanup = null
|
||||
|
||||
@ -1,17 +1,69 @@
|
||||
import { preferenceService } from '@data/PreferenceService'
|
||||
import { loggerService } from '@logger'
|
||||
import type { PreferenceDefaultScopeType, PreferenceKeyType } from '@shared/data/types'
|
||||
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
|
||||
|
||||
import { preferenceService } from '../PreferenceService'
|
||||
|
||||
const logger = loggerService.withContext('usePreference')
|
||||
|
||||
/**
|
||||
* React hook for managing a single preference value
|
||||
* Uses useSyncExternalStore for optimal React 18 integration
|
||||
* React hook for managing a single preference value with automatic synchronization
|
||||
* Uses useSyncExternalStore for optimal React 18 integration and real-time updates
|
||||
*
|
||||
* @param key - The preference key to manage
|
||||
* @returns [value, setValue] - Current value and setter function
|
||||
* @param key - The preference key to manage (must be a valid PreferenceKeyType)
|
||||
* @returns A tuple [value, setValue] where:
|
||||
* - value: Current preference value or undefined if not loaded/cached
|
||||
* - setValue: Async function to update the preference value
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic usage - managing theme preference
|
||||
* const [theme, setTheme] = usePreference('app.theme.mode')
|
||||
*
|
||||
* // Conditional rendering based on preference value
|
||||
* if (theme === undefined) {
|
||||
* return <LoadingSpinner />
|
||||
* }
|
||||
*
|
||||
* // Updating preference value
|
||||
* const handleThemeChange = async (newTheme: string) => {
|
||||
* try {
|
||||
* await setTheme(newTheme)
|
||||
* } catch (error) {
|
||||
* console.error('Failed to update theme:', error)
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* return (
|
||||
* <select value={theme} onChange={(e) => handleThemeChange(e.target.value)}>
|
||||
* <option value="ThemeMode.light">Light</option>
|
||||
* <option value="ThemeMode.dark">Dark</option>
|
||||
* <option value="ThemeMode.system">System</option>
|
||||
* </select>
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Advanced usage with form handling for message font size
|
||||
* const [fontSize, setFontSize] = usePreference('chat.message.font_size')
|
||||
*
|
||||
* const handleFontSizeChange = useCallback(async (size: number) => {
|
||||
* if (size < 8 || size > 72) {
|
||||
* throw new Error('Font size must be between 8 and 72')
|
||||
* }
|
||||
* await setFontSize(size)
|
||||
* }, [setFontSize])
|
||||
*
|
||||
* return (
|
||||
* <input
|
||||
* type="number"
|
||||
* value={fontSize ?? 14}
|
||||
* onChange={(e) => handleFontSizeChange(Number(e.target.value))}
|
||||
* min={8}
|
||||
* max={72}
|
||||
* />
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export function usePreference<K extends PreferenceKeyType>(
|
||||
key: K
|
||||
@ -49,13 +101,126 @@ export function usePreference<K extends PreferenceKeyType>(
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook for managing multiple preference values
|
||||
* Efficiently batches operations and provides type-safe interface
|
||||
* React hook for managing multiple preference values with efficient batch operations
|
||||
* Automatically synchronizes all specified preferences and provides type-safe access
|
||||
*
|
||||
* @param keys - Object mapping local names to preference keys
|
||||
* @returns [values, updateValues] - Current values and batch update function
|
||||
* @param keys - Object mapping local names to preference keys. Keys are your custom names,
|
||||
* values must be valid PreferenceKeyType identifiers
|
||||
* @returns A tuple [values, updateValues] where:
|
||||
* - values: Object with your local keys mapped to current preference values (undefined if not loaded)
|
||||
* - updateValues: Async function to batch update multiple preferences at once
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic usage - managing related UI preferences
|
||||
* const [uiSettings, setUISettings] = useMultiplePreferences({
|
||||
* theme: 'app.theme.mode',
|
||||
* fontSize: 'chat.message.font_size',
|
||||
* showLineNumbers: 'chat.code.show_line_numbers'
|
||||
* })
|
||||
*
|
||||
* // Accessing individual values with type safety
|
||||
* const currentTheme = uiSettings.theme // string | undefined
|
||||
* const currentFontSize = uiSettings.fontSize // number | undefined
|
||||
* const showLines = uiSettings.showLineNumbers // boolean | undefined
|
||||
*
|
||||
* // Batch updating multiple preferences
|
||||
* const resetToDefaults = async () => {
|
||||
* await setUISettings({
|
||||
* theme: 'ThemeMode.light',
|
||||
* fontSize: 14,
|
||||
* showLineNumbers: true
|
||||
* })
|
||||
* }
|
||||
*
|
||||
* // Partial updates (only specified keys will be updated)
|
||||
* const toggleTheme = async () => {
|
||||
* await setUISettings({
|
||||
* theme: currentTheme === 'ThemeMode.light' ? 'ThemeMode.dark' : 'ThemeMode.light'
|
||||
* })
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Advanced usage - backup settings form with validation
|
||||
* const [settings, updateSettings] = useMultiplePreferences({
|
||||
* autoSync: 'data.backup.local.auto_sync',
|
||||
* backupDir: 'data.backup.local.dir',
|
||||
* maxBackups: 'data.backup.local.max_backups',
|
||||
* syncInterval: 'data.backup.local.sync_interval'
|
||||
* })
|
||||
*
|
||||
* // Form submission with error handling
|
||||
* const handleSubmit = async (formData: Partial<typeof settings>) => {
|
||||
* try {
|
||||
* // Validate before saving
|
||||
* if (formData.maxBackups && formData.maxBackups < 0) {
|
||||
* throw new Error('Max backups must be non-negative')
|
||||
* }
|
||||
*
|
||||
* await updateSettings(formData)
|
||||
* showSuccessMessage('Backup settings saved successfully')
|
||||
* } catch (error) {
|
||||
* showErrorMessage(`Failed to save settings: ${error.message}`)
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // Conditional rendering based on loading state
|
||||
* if (Object.values(settings).every(val => val === undefined)) {
|
||||
* return <SettingsSkeletonLoader />
|
||||
* }
|
||||
*
|
||||
* return (
|
||||
* <form onSubmit={(e) => {
|
||||
* e.preventDefault()
|
||||
* handleSubmit({
|
||||
* maxBackups: parseInt(e.target.maxBackups.value),
|
||||
* syncInterval: parseInt(e.target.syncInterval.value)
|
||||
* })
|
||||
* }}>
|
||||
* <input
|
||||
* name="maxBackups"
|
||||
* type="number"
|
||||
* defaultValue={settings.maxBackups ?? 10}
|
||||
* min="0"
|
||||
* />
|
||||
* <input
|
||||
* name="syncInterval"
|
||||
* type="number"
|
||||
* defaultValue={settings.syncInterval ?? 3600}
|
||||
* min="60"
|
||||
* />
|
||||
* <button type="submit">Save Backup Settings</button>
|
||||
* </form>
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Performance optimization - grouping related chat code preferences
|
||||
* const [codePrefs] = useMultiplePreferences({
|
||||
* showLineNumbers: 'chat.code.show_line_numbers',
|
||||
* wrappable: 'chat.code.wrappable',
|
||||
* collapsible: 'chat.code.collapsible',
|
||||
* autocompletion: 'chat.code.editor.autocompletion',
|
||||
* foldGutter: 'chat.code.editor.fold_gutter'
|
||||
* })
|
||||
*
|
||||
* // Single subscription handles all code preferences
|
||||
* // More efficient than 5 separate usePreference calls
|
||||
* const codeConfig = useMemo(() => ({
|
||||
* showLineNumbers: codePrefs.showLineNumbers ?? false,
|
||||
* wrappable: codePrefs.wrappable ?? false,
|
||||
* collapsible: codePrefs.collapsible ?? false,
|
||||
* autocompletion: codePrefs.autocompletion ?? true,
|
||||
* foldGutter: codePrefs.foldGutter ?? false
|
||||
* }), [codePrefs])
|
||||
*
|
||||
* return <CodeBlock config={codeConfig} />
|
||||
* ```
|
||||
*/
|
||||
export function usePreferences<T extends Record<string, PreferenceKeyType>>(
|
||||
export function useMultiplePreferences<T extends Record<string, PreferenceKeyType>>(
|
||||
keys: T
|
||||
): [
|
||||
{ [P in keyof T]: PreferenceDefaultScopeType[T[P]] | undefined },
|
||||
@ -63,7 +228,6 @@ export function usePreferences<T extends Record<string, PreferenceKeyType>>(
|
||||
] {
|
||||
// Create stable key dependencies
|
||||
const keyList = useMemo(() => Object.values(keys), [keys])
|
||||
const keysStringified = useMemo(() => JSON.stringify(keys), [keys])
|
||||
|
||||
// Cache the last snapshot to avoid infinite loops
|
||||
const lastSnapshotRef = useRef<Record<string, any>>({})
|
||||
@ -78,7 +242,7 @@ export function usePreferences<T extends Record<string, PreferenceKeyType>>(
|
||||
unsubscribeFunctions.forEach((unsubscribe) => unsubscribe())
|
||||
}
|
||||
},
|
||||
[keysStringified]
|
||||
[keyList]
|
||||
),
|
||||
|
||||
useCallback(() => {
|
||||
@ -101,9 +265,9 @@ export function usePreferences<T extends Record<string, PreferenceKeyType>>(
|
||||
}
|
||||
|
||||
return lastSnapshotRef.current
|
||||
}, [keysStringified]),
|
||||
}, [keys]),
|
||||
|
||||
() => ({}) // SSR snapshot
|
||||
() => ({}) // No SSR snapshot
|
||||
)
|
||||
|
||||
// Load initial values asynchronously if not cached
|
||||
@ -115,7 +279,7 @@ export function usePreferences<T extends Record<string, PreferenceKeyType>>(
|
||||
logger.error('Failed to load initial preferences:', error as Error)
|
||||
})
|
||||
}
|
||||
}, [keysStringified])
|
||||
}, [keyList])
|
||||
|
||||
// Memoized batch update function
|
||||
const updateValues = useCallback(
|
||||
@ -136,7 +300,7 @@ export function usePreferences<T extends Record<string, PreferenceKeyType>>(
|
||||
throw error
|
||||
}
|
||||
},
|
||||
[keysStringified]
|
||||
[keys]
|
||||
)
|
||||
|
||||
// Type-cast the values to the expected shape
|
||||
@ -144,26 +308,3 @@ export function usePreferences<T extends Record<string, PreferenceKeyType>>(
|
||||
|
||||
return [typedValues, updateValues]
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for preloading preferences to improve performance
|
||||
* Useful for components that will use many preferences
|
||||
*
|
||||
* @param keys - Array of preference keys to preload
|
||||
*/
|
||||
export function usePreferencePreload(keys: PreferenceKeyType[]): void {
|
||||
const keysString = useMemo(() => keys.join(','), [keys])
|
||||
useEffect(() => {
|
||||
preferenceService.preload(keys).catch((error) => {
|
||||
logger.error('Failed to preload preferences:', error as Error)
|
||||
})
|
||||
}, [keysString])
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for getting the preference service instance
|
||||
* Useful for non-reactive operations or advanced usage
|
||||
*/
|
||||
export function usePreferenceService() {
|
||||
return preferenceService
|
||||
}
|
||||
|
||||
@ -29,11 +29,11 @@ const PreferenceHookTests: React.FC = () => {
|
||||
const testSubscriptions = () => {
|
||||
// Test subscription behavior
|
||||
const unsubscribe = preferenceService.subscribeToKey('app.theme.mode')(() => {
|
||||
setSubscriptionCount(prev => prev + 1)
|
||||
setSubscriptionCount((prev) => prev + 1)
|
||||
})
|
||||
|
||||
message.info('已添加订阅,修改app.theme.mode将触发计数')
|
||||
|
||||
|
||||
// Clean up after 10 seconds
|
||||
setTimeout(() => {
|
||||
unsubscribe()
|
||||
@ -45,14 +45,14 @@ const PreferenceHookTests: React.FC = () => {
|
||||
try {
|
||||
const keys: PreferenceKeyType[] = ['app.theme.mode', 'app.language', 'app.zoom_factor', 'app.spell_check.enabled']
|
||||
await preferenceService.preload(keys)
|
||||
|
||||
const cachedStates = keys.map(key => ({
|
||||
|
||||
const cachedStates = keys.map((key) => ({
|
||||
key,
|
||||
isCached: preferenceService.isCached(key),
|
||||
value: preferenceService.getCachedValue(key)
|
||||
}))
|
||||
|
||||
message.success(`预加载完成。缓存状态: ${cachedStates.filter(s => s.isCached).length}/${keys.length}`)
|
||||
|
||||
message.success(`预加载完成。缓存状态: ${cachedStates.filter((s) => s.isCached).length}/${keys.length}`)
|
||||
console.log('Cache states:', cachedStates)
|
||||
} catch (error) {
|
||||
message.error(`预加载失败: ${(error as Error).message}`)
|
||||
@ -63,16 +63,16 @@ const PreferenceHookTests: React.FC = () => {
|
||||
try {
|
||||
const keys: PreferenceKeyType[] = ['app.theme.mode', 'app.language']
|
||||
const result = await preferenceService.getMultiple(keys)
|
||||
|
||||
|
||||
message.success(`批量获取成功: ${Object.keys(result).length} 项`)
|
||||
console.log('Batch get result:', result)
|
||||
|
||||
|
||||
// Test batch set
|
||||
await preferenceService.setMultiple({
|
||||
'app.theme.mode': theme1 === 'ThemeMode.dark' ? 'ThemeMode.light' : 'ThemeMode.dark',
|
||||
'app.language': language === 'zh-CN' ? 'en-US' : 'zh-CN'
|
||||
})
|
||||
|
||||
|
||||
message.success('批量设置成功')
|
||||
} catch (error) {
|
||||
message.error(`批量操作失败: ${(error as Error).message}`)
|
||||
@ -82,23 +82,25 @@ const PreferenceHookTests: React.FC = () => {
|
||||
const performanceTest = async () => {
|
||||
const start = performance.now()
|
||||
const iterations = 100
|
||||
|
||||
|
||||
try {
|
||||
// Test rapid reads
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
preferenceService.getCachedValue('app.theme.mode')
|
||||
}
|
||||
|
||||
|
||||
const readTime = performance.now() - start
|
||||
|
||||
|
||||
// Test rapid writes
|
||||
const writeStart = performance.now()
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await preferenceService.set('app.theme.mode', `ThemeMode.test_${i}`)
|
||||
}
|
||||
const writeTime = performance.now() - writeStart
|
||||
|
||||
message.success(`性能测试完成: 读取${iterations}次耗时${readTime.toFixed(2)}ms, 写入10次耗时${writeTime.toFixed(2)}ms`)
|
||||
|
||||
message.success(
|
||||
`性能测试完成: 读取${iterations}次耗时${readTime.toFixed(2)}ms, 写入10次耗时${writeTime.toFixed(2)}ms`
|
||||
)
|
||||
} catch (error) {
|
||||
message.error(`性能测试失败: ${(error as Error).message}`)
|
||||
}
|
||||
@ -110,11 +112,21 @@ const PreferenceHookTests: React.FC = () => {
|
||||
{/* Hook State Display */}
|
||||
<Card size="small" title="Hook 状态监控">
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Text>组件渲染次数: <Text strong>{renderCountRef.current}</Text></Text>
|
||||
<Text>订阅触发次数: <Text strong>{subscriptionCount}</Text></Text>
|
||||
<Text>Theme Hook 1: <Text code>{String(theme1)}</Text></Text>
|
||||
<Text>Theme Hook 2: <Text code>{String(theme2)}</Text></Text>
|
||||
<Text>Language Hook: <Text code>{String(language)}</Text></Text>
|
||||
<Text>
|
||||
组件渲染次数: <Text strong>{renderCountRef.current}</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
订阅触发次数: <Text strong>{subscriptionCount}</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
Theme Hook 1: <Text code>{String(theme1)}</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
Theme Hook 2: <Text code>{String(theme2)}</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
Language Hook: <Text code>{String(language)}</Text>
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
注意: 相同key的多个hook应该返回相同值
|
||||
</Text>
|
||||
@ -123,24 +135,18 @@ const PreferenceHookTests: React.FC = () => {
|
||||
|
||||
{/* Test Actions */}
|
||||
<Space wrap>
|
||||
<Button onClick={testSubscriptions}>
|
||||
测试订阅机制
|
||||
</Button>
|
||||
<Button onClick={testCacheWarming}>
|
||||
测试缓存预热
|
||||
</Button>
|
||||
<Button onClick={testBatchOperations}>
|
||||
测试批量操作
|
||||
</Button>
|
||||
<Button onClick={performanceTest}>
|
||||
性能测试
|
||||
</Button>
|
||||
<Button onClick={testSubscriptions}>测试订阅机制</Button>
|
||||
<Button onClick={testCacheWarming}>测试缓存预热</Button>
|
||||
<Button onClick={testBatchOperations}>测试批量操作</Button>
|
||||
<Button onClick={performanceTest}>性能测试</Button>
|
||||
</Space>
|
||||
|
||||
{/* Service Information */}
|
||||
<Card size="small" title="Service 信息">
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Text>Service实例: <Text code>{preferenceService ? '已连接' : '未连接'}</Text></Text>
|
||||
<Text>
|
||||
Service实例: <Text code>{preferenceService ? '已连接' : '未连接'}</Text>
|
||||
</Text>
|
||||
<Text>预加载Keys: app.theme.mode, app.language, app.zoom_factor</Text>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
usePreferenceService() 返回的是同一个单例实例
|
||||
@ -152,15 +158,9 @@ const PreferenceHookTests: React.FC = () => {
|
||||
<Card size="small" title="Hook 行为测试">
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Text strong>实时同步测试:</Text>
|
||||
<Text type="secondary">
|
||||
1. 在其他测试组件中修改 app.theme.mode 或 app.language
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
2. 观察此组件中的值是否实时更新
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
3. 检查订阅触发次数是否增加
|
||||
</Text>
|
||||
<Text type="secondary">1. 在其他测试组件中修改 app.theme.mode 或 app.language</Text>
|
||||
<Text type="secondary">2. 观察此组件中的值是否实时更新</Text>
|
||||
<Text type="secondary">3. 检查订阅触发次数是否增加</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
</Space>
|
||||
@ -174,4 +174,4 @@ const TestContainer = styled.div`
|
||||
border-radius: 8px;
|
||||
`
|
||||
|
||||
export default PreferenceHookTests
|
||||
export default PreferenceHookTests
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { usePreferences } from '@renderer/data/hooks/usePreference'
|
||||
import { useMultiplePreferences } from '@renderer/data/hooks/usePreference'
|
||||
import { Button, Card, Input, message, Select, Space, Table, Typography } from 'antd'
|
||||
import { ColumnType } from 'antd/es/table'
|
||||
import React, { useState } from 'react'
|
||||
@ -40,7 +40,7 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
} as const
|
||||
|
||||
const currentKeys = scenarios[scenario as keyof typeof scenarios]
|
||||
const [values, updateValues] = usePreferences(currentKeys)
|
||||
const [values, updateValues] = useMultiplePreferences(currentKeys)
|
||||
|
||||
const [batchInput, setBatchInput] = useState<string>('')
|
||||
|
||||
@ -72,7 +72,7 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
const sampleUpdates: Record<string, any> = {}
|
||||
Object.keys(currentKeys).forEach((localKey, index) => {
|
||||
const prefKey = currentKeys[localKey as keyof typeof currentKeys]
|
||||
|
||||
|
||||
switch (prefKey) {
|
||||
case 'app.theme.mode':
|
||||
sampleUpdates[localKey] = values[localKey] === 'ThemeMode.dark' ? 'ThemeMode.light' : 'ThemeMode.dark'
|
||||
@ -81,7 +81,7 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
sampleUpdates[localKey] = values[localKey] === 'zh-CN' ? 'en-US' : 'zh-CN'
|
||||
break
|
||||
case 'app.zoom_factor':
|
||||
sampleUpdates[localKey] = 1.0 + (index * 0.1)
|
||||
sampleUpdates[localKey] = 1.0 + index * 0.1
|
||||
break
|
||||
case 'app.spell_check.enabled':
|
||||
sampleUpdates[localKey] = !values[localKey]
|
||||
@ -90,7 +90,7 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
sampleUpdates[localKey] = `sample_value_${index}`
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
setBatchInput(JSON.stringify(sampleUpdates, null, 2))
|
||||
}
|
||||
|
||||
@ -115,7 +115,11 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
render: (value: any) => (
|
||||
<ValueDisplay>
|
||||
{value !== undefined ? (
|
||||
typeof value === 'object' ? JSON.stringify(value) : String(value)
|
||||
typeof value === 'object' ? (
|
||||
JSON.stringify(value)
|
||||
) : (
|
||||
String(value)
|
||||
)
|
||||
) : (
|
||||
<Text type="secondary">undefined</Text>
|
||||
)}
|
||||
@ -136,7 +140,14 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
{record.prefKey === 'app.theme.mode' && (
|
||||
<Button size="small" onClick={() => handleQuickUpdate(record.localKey, record.value === 'ThemeMode.dark' ? 'ThemeMode.light' : 'ThemeMode.dark')}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() =>
|
||||
handleQuickUpdate(
|
||||
record.localKey,
|
||||
record.value === 'ThemeMode.dark' ? 'ThemeMode.light' : 'ThemeMode.dark'
|
||||
)
|
||||
}>
|
||||
切换
|
||||
</Button>
|
||||
)}
|
||||
@ -146,7 +157,9 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
</Button>
|
||||
)}
|
||||
{record.prefKey === 'app.language' && (
|
||||
<Button size="small" onClick={() => handleQuickUpdate(record.localKey, record.value === 'zh-CN' ? 'en-US' : 'zh-CN')}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => handleQuickUpdate(record.localKey, record.value === 'zh-CN' ? 'en-US' : 'zh-CN')}>
|
||||
切换
|
||||
</Button>
|
||||
)}
|
||||
@ -183,35 +196,27 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
|
||||
{/* Current Values Table */}
|
||||
<Card size="small" title="当前值状态">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
pagination={false}
|
||||
size="small"
|
||||
bordered
|
||||
/>
|
||||
<Table columns={columns} dataSource={tableData} pagination={false} size="small" bordered />
|
||||
</Card>
|
||||
|
||||
{/* Batch Update */}
|
||||
<Card size="small" title="批量更新">
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<Button onClick={generateSampleBatch}>
|
||||
生成示例更新
|
||||
</Button>
|
||||
<Button onClick={generateSampleBatch}>生成示例更新</Button>
|
||||
<Button type="primary" onClick={handleBatchUpdate} disabled={!batchInput.trim()}>
|
||||
执行批量更新
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
|
||||
<Input.TextArea
|
||||
value={batchInput}
|
||||
onChange={(e) => setBatchInput(e.target.value)}
|
||||
placeholder="输入JSON格式的批量更新数据,例如: {"theme": "dark", "language": "en-US"}"
|
||||
placeholder='输入JSON格式的批量更新数据,例如: {"theme": "dark", "language": "en-US"}'
|
||||
rows={6}
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
/>
|
||||
|
||||
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
格式: {"localKey": "newValue", ...} - 使用本地键名,不是完整的偏好设置键名
|
||||
</Text>
|
||||
@ -221,15 +226,17 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
{/* Quick Actions */}
|
||||
<Card size="small" title="快速操作">
|
||||
<Space wrap>
|
||||
<Button
|
||||
onClick={() => updateValues(Object.fromEntries(Object.keys(currentKeys).map(key => [key, 'test_value'])))}>
|
||||
<Button
|
||||
onClick={() =>
|
||||
updateValues(Object.fromEntries(Object.keys(currentKeys).map((key) => [key, 'test_value'])))
|
||||
}>
|
||||
设置所有为测试值
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => updateValues(Object.fromEntries(Object.keys(currentKeys).map(key => [key, undefined])))}>
|
||||
<Button
|
||||
onClick={() => updateValues(Object.fromEntries(Object.keys(currentKeys).map((key) => [key, undefined])))}>
|
||||
清空所有值
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const toggles: Record<string, any> = {}
|
||||
Object.entries(currentKeys).forEach(([localKey, prefKey]) => {
|
||||
@ -259,7 +266,7 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
返回值数量: <Text strong>{Object.keys(values).length}</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
已定义值: <Text strong>{Object.values(values).filter(v => v !== undefined).length}</Text>
|
||||
已定义值: <Text strong>{Object.values(values).filter((v) => v !== undefined).length}</Text>
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
usePreferences 返回的值对象使用本地键名,内部自动映射到实际的偏好设置键
|
||||
@ -283,4 +290,4 @@ const ValueDisplay = styled.span`
|
||||
word-break: break-all;
|
||||
`
|
||||
|
||||
export default PreferenceMultipleTests
|
||||
export default PreferenceMultipleTests
|
||||
|
||||
@ -34,16 +34,22 @@ const PreferenceServiceTests: React.FC = () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
let parsedValue: any = testValue
|
||||
|
||||
|
||||
// Try to parse as JSON if it looks like an object/array/boolean/number
|
||||
if (testValue.startsWith('{') || testValue.startsWith('[') || testValue === 'true' || testValue === 'false' || !isNaN(Number(testValue))) {
|
||||
if (
|
||||
testValue.startsWith('{') ||
|
||||
testValue.startsWith('[') ||
|
||||
testValue === 'true' ||
|
||||
testValue === 'false' ||
|
||||
!isNaN(Number(testValue))
|
||||
) {
|
||||
try {
|
||||
parsedValue = JSON.parse(testValue)
|
||||
} catch {
|
||||
// Keep as string if JSON parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await preferenceService.set(testKey as PreferenceKeyType, parsedValue)
|
||||
message.success('设置成功')
|
||||
// Automatically get the updated value
|
||||
@ -93,7 +99,13 @@ const PreferenceServiceTests: React.FC = () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// Get multiple keys to simulate getAll functionality
|
||||
const sampleKeys = ['app.theme.mode', 'app.language', 'app.zoom_factor', 'app.spell_check.enabled', 'app.user.name'] as PreferenceKeyType[]
|
||||
const sampleKeys = [
|
||||
'app.theme.mode',
|
||||
'app.language',
|
||||
'app.zoom_factor',
|
||||
'app.spell_check.enabled',
|
||||
'app.user.name'
|
||||
] as PreferenceKeyType[]
|
||||
const result = await preferenceService.getMultiple(sampleKeys)
|
||||
setGetResult(`Sample preferences (${Object.keys(result).length} keys):\n${JSON.stringify(result, null, 2)}`)
|
||||
message.success('获取示例偏好设置成功')
|
||||
@ -138,12 +150,8 @@ const PreferenceServiceTests: React.FC = () => {
|
||||
<Button onClick={handleSet} loading={loading}>
|
||||
Set
|
||||
</Button>
|
||||
<Button onClick={handleGetCached}>
|
||||
Get Cached
|
||||
</Button>
|
||||
<Button onClick={handleIsCached}>
|
||||
Is Cached
|
||||
</Button>
|
||||
<Button onClick={handleGetCached}>Get Cached</Button>
|
||||
<Button onClick={handleIsCached}>Is Cached</Button>
|
||||
<Button onClick={handlePreload} loading={loading}>
|
||||
Preload
|
||||
</Button>
|
||||
@ -156,7 +164,9 @@ const PreferenceServiceTests: React.FC = () => {
|
||||
{getResult !== null && (
|
||||
<ResultContainer>
|
||||
<Text strong>Result:</Text>
|
||||
<ResultText>{typeof getResult === 'object' ? JSON.stringify(getResult, null, 2) : String(getResult)}</ResultText>
|
||||
<ResultText>
|
||||
{typeof getResult === 'object' ? JSON.stringify(getResult, null, 2) : String(getResult)}
|
||||
</ResultText>
|
||||
</ResultContainer>
|
||||
)}
|
||||
|
||||
@ -215,4 +225,4 @@ const ResultText = styled.pre`
|
||||
word-break: break-all;
|
||||
`
|
||||
|
||||
export default PreferenceServiceTests
|
||||
export default PreferenceServiceTests
|
||||
|
||||
@ -10,4 +10,4 @@ loggerService.initWindowSource('DataRefactorTestWindow')
|
||||
|
||||
const root = createRoot(document.getElementById('root') as HTMLElement)
|
||||
|
||||
root.render(<TestApp />)
|
||||
root.render(<TestApp />)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user