fix: 优化数据库启动与设置页响应性能

(cherry picked from commit f6614548b0e742bc703b2a5aaa5060720985ed27)
This commit is contained in:
电摇小子
2026-08-03 10:04:19 +08:00
committed by Wxw-Gu
parent 90bf1aed90
commit ee7dc11e92
14 changed files with 202 additions and 94 deletions
+5 -1
View File
@@ -486,6 +486,7 @@ app.whenReady().then(async () => {
if (dbInitInFlight) return dbInitInFlight if (dbInitInFlight) return dbInitInFlight
dbInitInFlight = (async () => { dbInitInFlight = (async () => {
const startedAt = Date.now()
try { try {
if (wcdbBootstrapPromise) await wcdbBootstrapPromise if (wcdbBootstrapPromise) await wcdbBootstrapPromise
const trimmedKey = String(key || '').trim() const trimmedKey = String(key || '').trim()
@@ -511,7 +512,7 @@ app.whenReady().then(async () => {
} }
chat.setChatDb(nextWechatDb) chat.setChatDb(nextWechatDb)
const wcdb4Client = nextWechatDb.getWcdb4Client() const wcdb4Client = nextWechatDb.getWcdb4Client()
const sessions = await wcdb4Client.getSessionsAsync() const sessions = await wcdb4Client.getSessionsAsync({ hydrateDisplayNames: false })
configureRecallProtection(wcdb4Client, resolvedRoot, settings.recallProtectionEnabled) configureRecallProtection(wcdb4Client, resolvedRoot, settings.recallProtectionEnabled)
voiceService = new VoiceService(wcdb4Client) voiceService = new VoiceService(wcdb4Client)
stickerService = new StickerService(wcdb4Client) stickerService = new StickerService(wcdb4Client)
@@ -530,6 +531,9 @@ app.whenReady().then(async () => {
.catch((error) => console.warn('[WCDB4] message cursor warmup failed:', error)) .catch((error) => console.warn('[WCDB4] message cursor warmup failed:', error))
} }
imageDecryptService = null imageDecryptService = null
console.log(
`[WCDB4] db:init ready sessions=${sessions.length} monitoring=${monitoring} cost=${Date.now() - startedAt}ms`
)
return { success: true, monitoring } return { success: true, monitoring }
} catch (error) { } catch (error) {
console.error('Failed to init DB:', error) console.error('Failed to init DB:', error)
+1 -1
View File
@@ -174,7 +174,7 @@ export function listContacts(filter?: string): FormattedContact[] {
export async function listContactsAsync(filter?: string): Promise<FormattedContact[]> { export async function listContactsAsync(filter?: string): Promise<FormattedContact[]> {
if (!dbRef) return [] if (!dbRef) return []
await dbRef.getWcdb4Client().getSessionsAsync() await dbRef.getWcdb4Client().getSessionsAsync({ hydrateDisplayNames: false })
return listContacts(filter) return listContacts(filter)
} }
+53 -16
View File
@@ -32,6 +32,10 @@ export interface Wcdb4MessageQueryOptions {
limit?: number limit?: number
} }
export interface Wcdb4SessionQueryOptions {
hydrateDisplayNames?: boolean
}
type Wcdb4MessageStore = { type Wcdb4MessageStore = {
tableName: string tableName: string
dbPath: string dbPath: string
@@ -242,6 +246,8 @@ export class Wcdb4Client {
private cachedSessions: Wcdb4Session[] | null = null private cachedSessions: Wcdb4Session[] | null = null
private cachedChatTables: { name: string; db_number: string }[] | null = null private cachedChatTables: { name: string; db_number: string }[] | null = null
private sessionsInFlight: Promise<Wcdb4Session[]> | null = null private sessionsInFlight: Promise<Wcdb4Session[]> | null = null
private sessionDisplayNamesInFlight: Promise<void> | null = null
private sessionDisplayNamesHydrated = false
private sessionCacheGeneration = 0 private sessionCacheGeneration = 0
private wcdbShutdown: (() => number) | null = null private wcdbShutdown: (() => number) | null = null
@@ -558,6 +564,7 @@ export class Wcdb4Client {
this.handle = null this.handle = null
this.cachedSessions = null this.cachedSessions = null
this.sessionDisplayNamesHydrated = false
this.displayNameCache.clear() this.displayNameCache.clear()
this.avatarCache.clear() this.avatarCache.clear()
this.groupNicknameCache.clear() this.groupNicknameCache.clear()
@@ -722,13 +729,22 @@ export class Wcdb4Client {
...session, ...session,
nickname: this.displayNameCache.get(session.username) || session.nickname || session.username nickname: this.displayNameCache.get(session.username) || session.nickname || session.username
})) }))
this.sessionDisplayNamesHydrated = true
return this.cachedSessions return this.cachedSessions
} }
async getSessionsAsync(): Promise<Wcdb4Session[]> { async getSessionsAsync(options: Wcdb4SessionQueryOptions = {}): Promise<Wcdb4Session[]> {
if (this.cachedSessions) return this.cachedSessions const hydrateDisplayNames = options.hydrateDisplayNames !== false
if (this.sessionsInFlight) return this.sessionsInFlight if (this.cachedSessions) {
if (hydrateDisplayNames) await this.ensureSessionDisplayNamesAsync()
return this.cachedSessions
}
if (this.sessionsInFlight) {
await this.sessionsInFlight
if (hydrateDisplayNames) await this.ensureSessionDisplayNamesAsync()
return this.cachedSessions || []
}
if (!this.wcdbGetSessions) return [] if (!this.wcdbGetSessions) return []
const generation = this.sessionCacheGeneration const generation = this.sessionCacheGeneration
@@ -739,25 +755,17 @@ export class Wcdb4Client {
const sessions = (Array.isArray(rows) ? rows : []) const sessions = (Array.isArray(rows) ? rows : [])
.map((row) => this.normalizeSession(row)) .map((row) => this.normalizeSession(row))
.filter((session) => session.username) .filter((session) => session.username)
await this.hydrateDisplayNamesAsync( if (generation === this.sessionCacheGeneration) this.cachedSessions = sessions
sessions return sessions
.filter((session) => this.shouldHydrateSessionDisplayName(session))
.map((session) => session.username)
)
const hydrated = sessions.map((session) => ({
...session,
nickname:
this.displayNameCache.get(session.username) || session.nickname || session.username
}))
if (generation === this.sessionCacheGeneration) this.cachedSessions = hydrated
return hydrated
})() })()
this.sessionsInFlight = request this.sessionsInFlight = request
try { try {
return await request await request
} finally { } finally {
if (this.sessionsInFlight === request) this.sessionsInFlight = null if (this.sessionsInFlight === request) this.sessionsInFlight = null
} }
if (hydrateDisplayNames) await this.ensureSessionDisplayNamesAsync()
return this.cachedSessions || []
} }
invalidateSessionCache(): void { invalidateSessionCache(): void {
@@ -765,6 +773,7 @@ export class Wcdb4Client {
this.cachedSessions = null this.cachedSessions = null
this.cachedChatTables = null this.cachedChatTables = null
this.sessionsInFlight = null this.sessionsInFlight = null
this.sessionDisplayNamesHydrated = false
} }
getChatTables(): { name: string; db_number: string }[] { getChatTables(): { name: string; db_number: string }[] {
@@ -2214,6 +2223,34 @@ export class Wcdb4Client {
} }
} }
private async ensureSessionDisplayNamesAsync(): Promise<void> {
if (this.sessionDisplayNamesHydrated || !this.cachedSessions) return
if (this.sessionDisplayNamesInFlight) return this.sessionDisplayNamesInFlight
const generation = this.sessionCacheGeneration
const sessions = this.cachedSessions
const request = (async (): Promise<void> => {
await this.hydrateDisplayNamesAsync(
sessions
.filter((session) => this.shouldHydrateSessionDisplayName(session))
.map((session) => session.username)
)
if (generation !== this.sessionCacheGeneration || this.cachedSessions !== sessions) return
this.cachedSessions = sessions.map((session) => ({
...session,
nickname:
this.displayNameCache.get(session.username) || session.nickname || session.username
}))
this.sessionDisplayNamesHydrated = true
})()
this.sessionDisplayNamesInFlight = request
try {
await request
} finally {
if (this.sessionDisplayNamesInFlight === request) this.sessionDisplayNamesInFlight = null
}
}
private hydrateAvatarUrls(usernames: string[]): void { private hydrateAvatarUrls(usernames: string[]): void {
const missing = this.uniq(usernames).filter((username) => !this.avatarCache.has(username)) const missing = this.uniq(usernames).filter((username) => !this.avatarCache.has(username))
if (missing.length === 0) return if (missing.length === 0) return
+16
View File
@@ -210,6 +210,7 @@ const mergeMessagePages = (older: Message[], current: Message[]): Message[] => {
function App(): React.ReactElement { function App(): React.ReactElement {
const [isAuthenticated, setIsAuthenticated] = useState(false) const [isAuthenticated, setIsAuthenticated] = useState(false)
const [isDatabaseConnected, setIsDatabaseConnected] = useState(false) const [isDatabaseConnected, setIsDatabaseConnected] = useState(false)
const [isDatabaseConnecting, setIsDatabaseConnecting] = useState(false)
const [dbKey, setDbKey] = useState(getDevelopmentDatabaseKey) const [dbKey, setDbKey] = useState(getDevelopmentDatabaseKey)
const [contacts, setContacts] = useState<Contact[]>([]) const [contacts, setContacts] = useState<Contact[]>([])
const [selectedContact, setSelectedContact] = useState<Contact | null>(null) const [selectedContact, setSelectedContact] = useState<Contact | null>(null)
@@ -582,6 +583,7 @@ function App(): React.ReactElement {
if (!autoLoginEnabled) return if (!autoLoginEnabled) return
try { try {
const startupCacheReady = await loadStartupCache() const startupCacheReady = await loadStartupCache()
setIsDatabaseConnecting(true)
const initPromise = window.api.initDb(key) const initPromise = window.api.initDb(key)
if (startupCacheReady) { if (startupCacheReady) {
setIsAuthenticated(true) setIsAuthenticated(true)
@@ -606,6 +608,9 @@ function App(): React.ReactElement {
console.warn('[Startup] background database init failed:', error) console.warn('[Startup] background database init failed:', error)
setDbKeyStatusKind('error') setDbKeyStatusKind('error')
}) })
.finally(() => {
if (active) setIsDatabaseConnecting(false)
})
return return
} }
const result = await initPromise const result = await initPromise
@@ -638,6 +643,8 @@ function App(): React.ReactElement {
setDbKeyStatus(`自动连接失败: ${message}`) setDbKeyStatus(`自动连接失败: ${message}`)
setDbKeyStatusKind('error') setDbKeyStatusKind('error')
setBootState('login') setBootState('login')
} finally {
if (active) setIsDatabaseConnecting(false)
} }
} }
void attemptAutoConnect() void attemptAutoConnect()
@@ -663,6 +670,7 @@ function App(): React.ReactElement {
const keyToUse = keyInput || dbKey const keyToUse = keyInput || dbKey
if (!keyToUse) return if (!keyToUse) return
setBootState('connecting') setBootState('connecting')
setIsDatabaseConnecting(true)
// 持久化用户手动指定的微信聊天文件路径,供 db:init 读取 settings.dbRoot // 持久化用户手动指定的微信聊天文件路径,供 db:init 读取 settings.dbRoot
const trimmedRoot = dbRootInput.trim() const trimmedRoot = dbRootInput.trim()
if (trimmedRoot) { if (trimmedRoot) {
@@ -746,6 +754,8 @@ function App(): React.ReactElement {
setBootState('login') setBootState('login')
setStartupProgress(null) setStartupProgress(null)
alert('Error connecting to database') alert('Error connecting to database')
} finally {
setIsDatabaseConnecting(false)
} }
} }
@@ -918,6 +928,7 @@ function App(): React.ReactElement {
const handleReturnToLogin = (): void => { const handleReturnToLogin = (): void => {
setIsAuthenticated(false) setIsAuthenticated(false)
setIsDatabaseConnected(false) setIsDatabaseConnected(false)
setIsDatabaseConnecting(false)
setBootState('login') setBootState('login')
setDatabaseConnectionMode(dbKey ? 'manual' : 'automatic') setDatabaseConnectionMode(dbKey ? 'manual' : 'automatic')
setActivePage('archive') setActivePage('archive')
@@ -1425,6 +1436,7 @@ function App(): React.ReactElement {
width={sidebarWidth} width={sidebarWidth}
selfInfo={selfInfo} selfInfo={selfInfo}
dbReady={isDatabaseConnected} dbReady={isDatabaseConnected}
dbConnecting={isDatabaseConnecting}
onOpenSettings={openSettings} onOpenSettings={openSettings}
/> />
<div className="resizer" onMouseDown={startResizing} /> <div className="resizer" onMouseDown={startResizing} />
@@ -1454,6 +1466,7 @@ function App(): React.ReactElement {
selectedReportId={selectedReportId} selectedReportId={selectedReportId}
selfInfo={selfInfo} selfInfo={selfInfo}
dbReady={isDatabaseConnected} dbReady={isDatabaseConnected}
dbConnecting={isDatabaseConnecting}
onSelectReport={openReport} onSelectReport={openReport}
onCreateReport={openReportConfigure} onCreateReport={openReportConfigure}
onDeleteReport={handleDeleteReport} onDeleteReport={handleDeleteReport}
@@ -1476,6 +1489,7 @@ function App(): React.ReactElement {
selectedContact={reportSourceContact} selectedContact={reportSourceContact}
selfInfo={selfInfo} selfInfo={selfInfo}
dbReady={isDatabaseConnected} dbReady={isDatabaseConnected}
dbConnecting={isDatabaseConnecting}
onSelectContact={handleSelectReportSource} onSelectContact={handleSelectReportSource}
onOpenSettings={openSettings} onOpenSettings={openSettings}
/> />
@@ -1546,6 +1560,7 @@ function App(): React.ReactElement {
onCategoryChange={setSettingsCategory} onCategoryChange={setSettingsCategory}
selfInfo={selfInfo} selfInfo={selfInfo}
dbReady={isDatabaseConnected} dbReady={isDatabaseConnected}
dbConnecting={isDatabaseConnecting}
dbKey={dbKey} dbKey={dbKey}
onDbKeyChange={setDbKey} onDbKeyChange={setDbKey}
onDatabaseConnectionChange={setIsDatabaseConnected} onDatabaseConnectionChange={setIsDatabaseConnected}
@@ -1692,6 +1707,7 @@ function App(): React.ReactElement {
activePage={activePage} activePage={activePage}
selfInfo={selfInfo} selfInfo={selfInfo}
dbReady={isDatabaseConnected} dbReady={isDatabaseConnected}
dbConnecting={isDatabaseConnecting}
onPageChange={handlePageChange} onPageChange={handlePageChange}
onOpenSettings={openSettings} onOpenSettings={openSettings}
onOpenGuide={openFirstUseGuide} onOpenGuide={openFirstUseGuide}
@@ -10,6 +10,7 @@ interface SelfInfo {
interface AccountSummaryProps { interface AccountSummaryProps {
selfInfo: SelfInfo | null selfInfo: SelfInfo | null
dbReady: boolean dbReady: boolean
dbConnecting?: boolean
compact?: boolean compact?: boolean
onClick?: () => void onClick?: () => void
} }
@@ -17,13 +18,20 @@ interface AccountSummaryProps {
export function AccountSummary({ export function AccountSummary({
selfInfo, selfInfo,
dbReady, dbReady,
dbConnecting = false,
compact = false, compact = false,
onClick onClick
}: AccountSummaryProps): React.ReactElement { }: AccountSummaryProps): React.ReactElement {
const showAccount = Boolean(selfInfo && (dbReady || dbConnecting))
const displayName = const displayName =
dbReady && selfInfo ? selfInfo.nickname || selfInfo.wxid || '当前账号' : '未连接' showAccount && selfInfo ? selfInfo.nickname || selfInfo.wxid || '当前账号' : '未连接'
const subtitle = dbReady && selfInfo ? selfInfo.wxid : '打开设置' const subtitle = showAccount && selfInfo ? selfInfo.wxid : '打开设置'
const statusText = dbReady ? '数据库已连接' : '数据库未连接' const statusText = dbReady
? '数据库已连接'
: dbConnecting
? '正在连接数据库'
: '数据库未连接'
const statusClass = dbReady ? 'ready' : dbConnecting ? 'connecting' : ''
const initial = (displayName || '?').charAt(0) const initial = (displayName || '?').charAt(0)
const title = `${displayName}\n${subtitle}` const title = `${displayName}\n${subtitle}`
const avatar = ( const avatar = (
@@ -33,7 +41,7 @@ export function AccountSummary({
) : ( ) : (
initial initial
)} )}
<span className={`account-summary-status ${dbReady ? 'ready' : ''}`} aria-hidden /> <span className={`account-summary-status ${statusClass}`} aria-hidden />
</span> </span>
) )
@@ -52,7 +60,7 @@ export function AccountSummary({
<span className="account-summary-name">{displayName}</span> <span className="account-summary-name">{displayName}</span>
<span className="account-summary-meta">{subtitle}</span> <span className="account-summary-meta">{subtitle}</span>
<span className="account-summary-state"> <span className="account-summary-state">
<span className={`account-summary-state-dot ${dbReady ? 'ready' : ''}`} aria-hidden /> <span className={`account-summary-state-dot ${statusClass}`} aria-hidden />
{statusText} {statusText}
</span> </span>
</span> </span>
@@ -21,6 +21,7 @@ export interface ConversationSidebarProps {
width: number width: number
selfInfo: SelfInfo | null selfInfo: SelfInfo | null
dbReady: boolean dbReady: boolean
dbConnecting?: boolean
onOpenSettings: () => void onOpenSettings: () => void
} }
@@ -37,6 +38,7 @@ export function ConversationSidebar({
width, width,
selfInfo, selfInfo,
dbReady, dbReady,
dbConnecting = false,
onOpenSettings onOpenSettings
}: ConversationSidebarProps): React.ReactElement { }: ConversationSidebarProps): React.ReactElement {
const [searchTerm, setSearchTerm] = useState('') const [searchTerm, setSearchTerm] = useState('')
@@ -124,7 +126,12 @@ export function ConversationSidebar({
</div> </div>
</div> </div>
<div className="conversation-sidebar-account"> <div className="conversation-sidebar-account">
<AccountSummary selfInfo={selfInfo} dbReady={dbReady} onClick={onOpenSettings} /> <AccountSummary
selfInfo={selfInfo}
dbReady={dbReady}
dbConnecting={dbConnecting}
onClick={onOpenSettings}
/>
</div> </div>
</aside> </aside>
) )
@@ -15,6 +15,7 @@ interface AppShellProps {
activePage: AppPage activePage: AppPage
selfInfo: SelfInfo | null selfInfo: SelfInfo | null
dbReady: boolean dbReady: boolean
dbConnecting?: boolean
onPageChange: (page: AppPage) => void onPageChange: (page: AppPage) => void
onOpenSettings: () => void onOpenSettings: () => void
onOpenGuide: () => void onOpenGuide: () => void
@@ -35,6 +36,7 @@ export function AppShell({
activePage, activePage,
selfInfo, selfInfo,
dbReady, dbReady,
dbConnecting = false,
onPageChange, onPageChange,
onOpenSettings, onOpenSettings,
onOpenGuide, onOpenGuide,
@@ -63,7 +65,13 @@ export function AppShell({
<span className="app-guide-launcher-label"></span> <span className="app-guide-launcher-label"></span>
</button> </button>
<div className="app-rail-account"> <div className="app-rail-account">
<AccountSummary selfInfo={selfInfo} dbReady={dbReady} compact onClick={onOpenSettings} /> <AccountSummary
selfInfo={selfInfo}
dbReady={dbReady}
dbConnecting={dbConnecting}
compact
onClick={onOpenSettings}
/>
</div> </div>
</aside> </aside>
<main className="app-shell-main" aria-label={activeItem?.label || '工作区'}> <main className="app-shell-main" aria-label={activeItem?.label || '工作区'}>
@@ -14,6 +14,7 @@ interface ReportHistorySidebarProps {
selectedReportId: string | null selectedReportId: string | null
selfInfo: SelfInfo | null selfInfo: SelfInfo | null
dbReady: boolean dbReady: boolean
dbConnecting?: boolean
onSelectReport: (reportId: string) => void onSelectReport: (reportId: string) => void
onCreateReport: () => void onCreateReport: () => void
onDeleteReport: (reportId: string) => Promise<{ success: boolean; error?: string }> onDeleteReport: (reportId: string) => Promise<{ success: boolean; error?: string }>
@@ -80,6 +81,7 @@ export function ReportHistorySidebar({
selectedReportId, selectedReportId,
selfInfo, selfInfo,
dbReady, dbReady,
dbConnecting = false,
onSelectReport, onSelectReport,
onCreateReport, onCreateReport,
onDeleteReport, onDeleteReport,
@@ -202,7 +204,12 @@ export function ReportHistorySidebar({
)} )}
</div> </div>
<div className="report-history-account"> <div className="report-history-account">
<AccountSummary selfInfo={selfInfo} dbReady={dbReady} onClick={onOpenSettings} /> <AccountSummary
selfInfo={selfInfo}
dbReady={dbReady}
dbConnecting={dbConnecting}
onClick={onOpenSettings}
/>
</div> </div>
{pendingDelete && ( {pendingDelete && (
<div className="report-delete-confirm" role="dialog" aria-modal="true"> <div className="report-delete-confirm" role="dialog" aria-modal="true">
@@ -14,6 +14,7 @@ interface ReportSourceSidebarProps {
selectedContact: Contact | null selectedContact: Contact | null
selfInfo: SelfInfo | null selfInfo: SelfInfo | null
dbReady: boolean dbReady: boolean
dbConnecting?: boolean
onSelectContact: (contact: Contact) => void onSelectContact: (contact: Contact) => void
onOpenSettings: () => void onOpenSettings: () => void
} }
@@ -23,6 +24,7 @@ export function ReportSourceSidebar({
selectedContact, selectedContact,
selfInfo, selfInfo,
dbReady, dbReady,
dbConnecting = false,
onSelectContact, onSelectContact,
onOpenSettings onOpenSettings
}: ReportSourceSidebarProps): React.ReactElement { }: ReportSourceSidebarProps): React.ReactElement {
@@ -95,6 +97,7 @@ export function ReportSourceSidebar({
<AccountSummary <AccountSummary
selfInfo={selfInfo} selfInfo={selfInfo}
dbReady={dbReady} dbReady={dbReady}
dbConnecting={dbConnecting}
onClick={onOpenSettings} onClick={onOpenSettings}
/> />
</div> </div>
@@ -19,6 +19,7 @@ export function SettingsWorkspace({
onCategoryChange, onCategoryChange,
selfInfo, selfInfo,
dbReady, dbReady,
dbConnecting = false,
dbKey, dbKey,
onDbKeyChange, onDbKeyChange,
onDatabaseConnectionChange, onDatabaseConnectionChange,
@@ -35,6 +36,7 @@ export function SettingsWorkspace({
onCategoryChange: (id: SettingsCategoryId) => void onCategoryChange: (id: SettingsCategoryId) => void
selfInfo: SettingsSelfInfo | null selfInfo: SettingsSelfInfo | null
dbReady: boolean dbReady: boolean
dbConnecting?: boolean
dbKey: string dbKey: string
onDbKeyChange: (key: string) => void onDbKeyChange: (key: string) => void
onDatabaseConnectionChange: (connected: boolean) => void onDatabaseConnectionChange: (connected: boolean) => void
@@ -47,6 +49,54 @@ export function SettingsWorkspace({
onOpenSettings: () => void onOpenSettings: () => void
onAppearanceChange: (settings: { theme: 'system' | 'light' | 'dark'; compactMode: boolean }) => void onAppearanceChange: (settings: { theme: 'system' | 'light' | 'dark'; compactMode: boolean }) => void
}): React.ReactElement { }): React.ReactElement {
const renderSelectedPage = (): React.ReactElement => {
switch (selectedCategory) {
case 'account-database':
return (
<AccountDatabasePage
dbKey={dbKey}
dbReady={dbReady}
dbConnecting={dbConnecting}
selfInfo={selfInfo}
onNotice={onNotice}
/>
)
case 'database-key':
return (
<DatabaseKeyPage
dbKey={dbKey}
dbReady={dbReady}
selfInfo={selfInfo}
onDbKeyChange={onDbKeyChange}
onDatabaseConnectionChange={onDatabaseConnectionChange}
onSelfInfoChange={onSelfInfoChange}
onContactsChange={onContactsChange}
onFilteredContactsChange={onFilteredContactsChange}
onReturnToLogin={onReturnToLogin}
onNotice={onNotice}
/>
)
case 'image-key':
return <ImageDecryptionPage selfInfo={selfInfo} onNotice={onNotice} />
case 'ai-model':
return <AIModelPage onRuntimeChange={onAIRuntimeChange} onNotice={onNotice} />
case 'recall-protection':
return <RecallProtectionPage onNotice={onNotice} />
case 'advanced':
return <AdvancedPage onNotice={onNotice} />
case 'cache-cleanup':
return <CacheCleanupPage onNotice={onNotice} />
case 'appearance':
return (
<AppearancePage onNotice={onNotice} onAppearanceChange={onAppearanceChange} />
)
case 'about':
return <AboutPage onNotice={onNotice} />
default:
return <SettingsEmptyState label={SETTINGS_CATEGORY_LABELS[selectedCategory]} />
}
}
return ( return (
<div className="settings-workspace"> <div className="settings-workspace">
<SettingsSidebar <SettingsSidebar
@@ -54,70 +104,10 @@ export function SettingsWorkspace({
onSelect={onCategoryChange} onSelect={onCategoryChange}
selfInfo={selfInfo} selfInfo={selfInfo}
dbReady={dbReady} dbReady={dbReady}
dbConnecting={dbConnecting}
onOpenSettings={onOpenSettings} onOpenSettings={onOpenSettings}
/> />
<div <div className="settings-page-panel active">{renderSelectedPage()}</div>
className={`settings-page-panel ${selectedCategory === 'account-database' ? 'active' : ''}`}
>
<AccountDatabasePage
dbKey={dbKey}
dbReady={dbReady}
selfInfo={selfInfo}
onNotice={onNotice}
/>
</div>
<div className={`settings-page-panel ${selectedCategory === 'database-key' ? 'active' : ''}`}>
<DatabaseKeyPage
dbKey={dbKey}
dbReady={dbReady}
selfInfo={selfInfo}
onDbKeyChange={onDbKeyChange}
onDatabaseConnectionChange={onDatabaseConnectionChange}
onSelfInfoChange={onSelfInfoChange}
onContactsChange={onContactsChange}
onFilteredContactsChange={onFilteredContactsChange}
onReturnToLogin={onReturnToLogin}
onNotice={onNotice}
/>
</div>
<div className={`settings-page-panel ${selectedCategory === 'image-key' ? 'active' : ''}`}>
<ImageDecryptionPage selfInfo={selfInfo} onNotice={onNotice} />
</div>
<div className={`settings-page-panel ${selectedCategory === 'ai-model' ? 'active' : ''}`}>
<AIModelPage onRuntimeChange={onAIRuntimeChange} onNotice={onNotice} />
</div>
<div
className={`settings-page-panel ${selectedCategory === 'recall-protection' ? 'active' : ''}`}
>
<RecallProtectionPage onNotice={onNotice} />
</div>
<div className={`settings-page-panel ${selectedCategory === 'advanced' ? 'active' : ''}`}>
<AdvancedPage onNotice={onNotice} />
</div>
<div className={`settings-page-panel ${selectedCategory === 'cache-cleanup' ? 'active' : ''}`}>
<CacheCleanupPage onNotice={onNotice} />
</div>
<div className={`settings-page-panel ${selectedCategory === 'appearance' ? 'active' : ''}`}>
<AppearancePage onNotice={onNotice} onAppearanceChange={onAppearanceChange} />
</div>
<div className={`settings-page-panel ${selectedCategory === 'about' ? 'active' : ''}`}>
<AboutPage onNotice={onNotice} />
</div>
{![
'account-database',
'database-key',
'image-key',
'ai-model',
'recall-protection',
'advanced',
'cache-cleanup',
'appearance',
'about'
].includes(selectedCategory) && (
<div className="settings-page-panel active">
<SettingsEmptyState label={SETTINGS_CATEGORY_LABELS[selectedCategory]} />
</div>
)}
</div> </div>
) )
} }
@@ -13,11 +13,13 @@ import type { ConnectionCheckState } from './types'
export function useAccountDatabaseController({ export function useAccountDatabaseController({
dbKey, dbKey,
dbReady, dbReady,
dbConnecting = false,
selfInfo, selfInfo,
onNotice onNotice
}: { }: {
dbKey: string dbKey: string
dbReady: boolean dbReady: boolean
dbConnecting?: boolean
selfInfo: SettingsSelfInfo | null selfInfo: SettingsSelfInfo | null
onNotice: (message: string) => void onNotice: (message: string) => void
}) { }) {
@@ -28,7 +30,7 @@ export function useAccountDatabaseController({
useEffect(() => { useEffect(() => {
let active = true let active = true
void window.api void window.api
.getImageDecryptionStatus() .getImageKeyConfig()
.then((result) => { .then((result) => {
if (active) setHasImageKey(result.configured) if (active) setHasImageKey(result.configured)
}) })
@@ -58,8 +60,11 @@ export function useAccountDatabaseController({
[checkState, dbKey, dbReady, hasImageKey, selfInfo] [checkState, dbKey, dbReady, hasImageKey, selfInfo]
) )
const connectionStatus = useMemo( const connectionStatus = useMemo(
() => getConnectionOverviewStatus({ dbReady, checkState, diagnostics }), () =>
[checkState, dbReady, diagnostics] dbConnecting
? ('checking' as const)
: getConnectionOverviewStatus({ dbReady, checkState, diagnostics }),
[checkState, dbConnecting, dbReady, diagnostics]
) )
const lastCheckedLabel = formatConnectionCheckedAt(checkState, clock) const lastCheckedLabel = formatConnectionCheckedAt(checkState, clock)
@@ -121,7 +126,7 @@ export function useAccountDatabaseController({
diagnostics, diagnostics,
connectionStatus, connectionStatus,
checkState, checkState,
isChecking: checkState.status === 'checking', isChecking: dbConnecting || checkState.status === 'checking',
lastCheckedLabel, lastCheckedLabel,
testConnection, testConnection,
openAccountDirectory, openAccountDirectory,
@@ -8,12 +8,14 @@ export function SettingsSidebar({
onSelect, onSelect,
selfInfo, selfInfo,
dbReady, dbReady,
dbConnecting = false,
onOpenSettings onOpenSettings
}: { }: {
selectedId: SettingsCategoryId selectedId: SettingsCategoryId
onSelect: (id: SettingsCategoryId) => void onSelect: (id: SettingsCategoryId) => void
selfInfo: SettingsSelfInfo | null selfInfo: SettingsSelfInfo | null
dbReady: boolean dbReady: boolean
dbConnecting?: boolean
onOpenSettings: () => void onOpenSettings: () => void
}): React.ReactElement { }): React.ReactElement {
const [keyword, setKeyword] = useState('') const [keyword, setKeyword] = useState('')
@@ -57,7 +59,12 @@ export function SettingsSidebar({
))} ))}
</div> </div>
<div className="settings-sidebar-account"> <div className="settings-sidebar-account">
<AccountSummary selfInfo={selfInfo} dbReady={dbReady} onClick={onOpenSettings} /> <AccountSummary
selfInfo={selfInfo}
dbReady={dbReady}
dbConnecting={dbConnecting}
onClick={onOpenSettings}
/>
</div> </div>
</aside> </aside>
) )
@@ -17,15 +17,23 @@ const STATUS_LABELS: Record<ConnectionOverviewStatus, string> = {
export function AccountDatabasePage({ export function AccountDatabasePage({
dbKey, dbKey,
dbReady, dbReady,
dbConnecting = false,
selfInfo, selfInfo,
onNotice onNotice
}: { }: {
dbKey: string dbKey: string
dbReady: boolean dbReady: boolean
dbConnecting?: boolean
selfInfo: SettingsSelfInfo | null selfInfo: SettingsSelfInfo | null
onNotice: (message: string) => void onNotice: (message: string) => void
}): React.ReactElement { }): React.ReactElement {
const controller = useAccountDatabaseController({ dbKey, dbReady, selfInfo, onNotice }) const controller = useAccountDatabaseController({
dbKey,
dbReady,
dbConnecting,
selfInfo,
onNotice
})
const [autoLogin, setAutoLogin] = useState(false) const [autoLogin, setAutoLogin] = useState(false)
useEffect(() => { useEffect(() => {
+8
View File
@@ -253,6 +253,10 @@ body {
&.ready { &.ready {
background: var(--wxex-success); background: var(--wxex-success);
} }
&.connecting {
background: var(--wxex-brand);
}
} }
.account-summary:not(.compact) .account-summary-avatar .account-summary-status { .account-summary:not(.compact) .account-summary-avatar .account-summary-status {
@@ -306,6 +310,10 @@ body {
&.ready { &.ready {
background: var(--wxex-success); background: var(--wxex-success);
} }
&.connecting {
background: var(--wxex-brand);
}
} }
.account-summary-settings { .account-summary-settings {