import { create } from 'zustand'; import { DEFAULT_DEDUP_CONFIG, type DedupConfig } from '../domain/dedup'; import { DEFAULT_TRANSFER_CONFIG, type TransferConfig } from '../domain/transferRecognizer'; /** * 设置状态管理(plan.md「0.1 状态管理」settingsStore)。 * * 持久化策略:内存状态 + 持久化钩子(persistTo)。生产环境传入 AsyncStorage 持久化, * 测试环境不传(纯内存)。这样避免直接依赖 @react-native-async-storage/async-storage, * 使本 store 可单测。 */ export type ThemeMode = 'light' | 'dark' | 'system'; export type Locale = 'zh' | 'en'; export interface SettingsState { // 去重/转账配置(影响 BillPipeline) dedupConfig: DedupConfig; transferConfig: TransferConfig; /** 是否启用多通道联合去重。 */ dedupEnabled: boolean; /** 是否启用转账智能识别。 */ transferRecognitionEnabled: boolean; // UI 偏好 themeMode: ThemeMode; locale: Locale; /** 应用锁启用。 */ appLockEnabled: boolean; /** 引导是否完成。 */ onboardingCompleted: boolean; setOnboardingCompleted: (v: boolean) => void; // Sync Configurations webdavUrl?: string; webdavUsername?: string; webdavPassword?: string; webdavRemotePath?: string; gitRemoteUrl?: string; gitBranch?: string; gitUsername?: string; gitPassword?: string; // AI Configurations aiEnabled: boolean; aiProviderId: 'openai' | 'gemini' | 'deepseek'; aiApiKey?: string; aiBaseUrl?: string; aiModel?: string; // Reminder Configurations reminderEnabled: boolean; reminderHour: number; reminderMinute: number; // 悬浮球 floatingBallEnabled: boolean; setFloatingBallEnabled: (enabled: boolean) => void; // 记账面板 /** +按钮是否打开全局记账面板(P5 起默认 true;已持久化 false 的用户保持关闭)。 */ numpadGlobalEntry: boolean; setNumpadGlobalEntry: (enabled: boolean) => void; /** 上次记账的资金账户(NumpadSheet 默认选中)。 */ lastUsedAccount: string | null; /** 上次使用的分类 id。 */ lastUsedCategoryId: string | null; setLastUsedEntry: (account: string | null, categoryId: string | null) => void; // actions updateDedupConfig: (patch: Partial) => void; updateTransferConfig: (patch: Partial) => void; setDedupEnabled: (enabled: boolean) => void; setTransferRecognitionEnabled: (enabled: boolean) => void; setThemeMode: (mode: ThemeMode) => void; setLocale: (locale: Locale) => void; setAppLockEnabled: (enabled: boolean) => void; updateSyncConfig: (patch: Partial>) => void; updateAiConfig: (patch: Partial>) => void; updateReminderConfig: (patch: Partial>) => void; /** 持久化钩子(每次状态变更后调用,生产环境传 AsyncStorage 写入)。 */ persistTo?: (state: PersistableSettings) => void; /** 从持久化源恢复(启动时调用)。 */ hydrate: (data: Partial) => void; /** 跳过下次持久化(hydrate 时使用)。 */ skipPersist: boolean; } /** 可持久化的设置子集(排除函数)。 */ export interface PersistableSettings { dedupConfig: DedupConfig; transferConfig: TransferConfig; dedupEnabled: boolean; transferRecognitionEnabled: boolean; themeMode: ThemeMode; locale: Locale; appLockEnabled: boolean; webdavUrl?: string; webdavUsername?: string; webdavPassword?: string; webdavRemotePath?: string; gitRemoteUrl?: string; gitBranch?: string; gitUsername?: string; gitPassword?: string; onboardingCompleted: boolean; aiEnabled: boolean; aiProviderId: 'openai' | 'gemini' | 'deepseek'; aiApiKey?: string; aiBaseUrl?: string; aiModel?: string; reminderEnabled: boolean; reminderHour: number; reminderMinute: number; floatingBallEnabled: boolean; numpadGlobalEntry: boolean; lastUsedAccount: string | null; lastUsedCategoryId: string | null; } /** * 敏感字段:这些值不写入明文 settings.json,由 storePersistence 层 * 通过 expo-secure-store(Android Keystore / iOS Keychain)加密存储。 */ export const SECURE_KEYS = ['aiApiKey', 'webdavPassword', 'gitPassword'] as const; export type SecureKey = (typeof SECURE_KEYS)[number]; const DEFAULTS: PersistableSettings & { skipPersist: boolean; floatingBallEnabled: boolean } = { dedupConfig: DEFAULT_DEDUP_CONFIG, transferConfig: DEFAULT_TRANSFER_CONFIG, dedupEnabled: true, transferRecognitionEnabled: true, themeMode: 'system', locale: 'zh', appLockEnabled: false, webdavUrl: '', webdavUsername: '', webdavPassword: '', webdavRemotePath: 'main.bean', gitRemoteUrl: '', gitBranch: 'main', gitUsername: '', gitPassword: '', onboardingCompleted: false, aiEnabled: false, aiProviderId: 'openai', aiApiKey: '', aiBaseUrl: 'https://api.openai.com/v1', aiModel: 'gpt-4o-mini', reminderEnabled: false, reminderHour: 20, reminderMinute: 0, floatingBallEnabled: false, // P5 翻默认:+按钮默认打开全局记账面板。 // 注意:hydrate 以持久化值覆盖 DEFAULTS,已持久化 false 的老用户不受影响,仅新安装生效。 numpadGlobalEntry: true, lastUsedAccount: null, lastUsedCategoryId: null, skipPersist: false, }; export function toPersistable(state: SettingsState): PersistableSettings { return { dedupConfig: state.dedupConfig, transferConfig: state.transferConfig, dedupEnabled: state.dedupEnabled, transferRecognitionEnabled: state.transferRecognitionEnabled, themeMode: state.themeMode, locale: state.locale, appLockEnabled: state.appLockEnabled, webdavUrl: state.webdavUrl, webdavUsername: state.webdavUsername, webdavPassword: state.webdavPassword, webdavRemotePath: state.webdavRemotePath, gitRemoteUrl: state.gitRemoteUrl, gitBranch: state.gitBranch, gitUsername: state.gitUsername, gitPassword: state.gitPassword, onboardingCompleted: state.onboardingCompleted, aiEnabled: state.aiEnabled, aiProviderId: state.aiProviderId, aiApiKey: state.aiApiKey, aiBaseUrl: state.aiBaseUrl, aiModel: state.aiModel, reminderEnabled: state.reminderEnabled, reminderHour: state.reminderHour, reminderMinute: state.reminderMinute, floatingBallEnabled: state.floatingBallEnabled, numpadGlobalEntry: state.numpadGlobalEntry, lastUsedAccount: state.lastUsedAccount, lastUsedCategoryId: state.lastUsedCategoryId, }; } export const useSettingsStore = create((set) => ({ ...DEFAULTS, updateDedupConfig: (patch) => set((s) => ({ dedupConfig: { ...s.dedupConfig, ...patch } })), updateTransferConfig: (patch) => set((s) => ({ transferConfig: { ...s.transferConfig, ...patch } })), setDedupEnabled: (enabled) => set({ dedupEnabled: enabled }), setTransferRecognitionEnabled: (enabled) => set({ transferRecognitionEnabled: enabled }), setThemeMode: (mode) => set({ themeMode: mode }), setLocale: (locale) => set({ locale }), setAppLockEnabled: (enabled) => set({ appLockEnabled: enabled }), updateSyncConfig: (patch) => set((s) => ({ ...s, ...patch })), updateAiConfig: (patch) => set((s) => ({ ...s, ...patch })), updateReminderConfig: (patch) => set((s) => ({ ...s, ...patch })), setOnboardingCompleted: (completed) => set({ onboardingCompleted: completed }), setFloatingBallEnabled: (enabled) => set({ floatingBallEnabled: enabled }), setNumpadGlobalEntry: (enabled) => set({ numpadGlobalEntry: enabled }), setLastUsedEntry: (account, categoryId) => set({ lastUsedAccount: account, lastUsedCategoryId: categoryId }), hydrate: (data) => { // hydrate 期间跳过自动持久化 // 使用单次 setState 合并所有数据,避免多次触发 subscriber useSettingsStore.setState({ ...DEFAULTS, ...data, skipPersist: true }); useSettingsStore.setState({ skipPersist: false }); }, }));