feat(sync): 按 Vault 持久化可移植侧栏布局
This commit is contained in:
@@ -4,6 +4,7 @@ import { flushPromises } from '@vue/test-utils'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { useWorkspaceStore } from '@/stores/workspace'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { useLayoutPreferencesStore } from '@/stores/layoutPreferences'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { hostInvoke } from './desktop'
|
||||
import { installPreferenceSync, seedCurrentPreferences } from './preferenceSync'
|
||||
@@ -14,15 +15,18 @@ it('applies remote appearance without echoing it and only exports approved local
|
||||
const workspace = useWorkspaceStore(), theme = useThemeStore(), settings = useSettingsStore()
|
||||
settings.permissionPolicy = { plantedSecretPermission: 'allow' }
|
||||
const data = { themeId: 'dark', fontEditorSize: 18, fontEditorFamily: 'system-ui', lineHeight: 1.7, codeBlockTheme: 'auto', headings: { custom: false, family: 'inherit', levels: [32,28,24,21,18,16].map(size => ({ size, weight: 700 })) } }
|
||||
const layout = useLayoutPreferencesStore()
|
||||
const remoteLayout = { record: { schema: 1, kind: 'layout', id: 'sidebars', data: { primaryExpanded: true, workspaceWidth: 400, chatWidth: 320 } }, hash: '3'.repeat(64), file_id: 'layout-file' }
|
||||
let remote: Record<string, unknown> = { record: { schema: 1, kind: 'theme_settings', id: 'appearance', data }, hash: '1'.repeat(64), file_id: 'theme-file' }
|
||||
vi.mocked(hostInvoke).mockImplementation(async (command, args) => {
|
||||
const request = args!.request as { kind: string; record?: Record<string, unknown> }
|
||||
if (command === 'record_get') return request.kind === 'theme_settings' ? remote : null
|
||||
if (command === 'record_get') return request.kind === 'theme_settings' ? remote : request.kind === 'layout' ? remoteLayout : null
|
||||
const result = { record: request.record, hash: '2'.repeat(64), file_id: 'theme-file' }
|
||||
if (request.record?.kind === 'theme_settings') remote = result
|
||||
return result
|
||||
})
|
||||
workspace.vaultId = 'one'; installPreferenceSync(); await flushPromises()
|
||||
expect(layout.workspaceWidth).toBe(400); expect(layout.chatWidth).toBe(320); expect(layout.primaryExpanded).toBe(true)
|
||||
expect(theme.fontEditorSize).toBe(18); expect(theme.currentThemeId).toBe('dark')
|
||||
expect(vi.mocked(hostInvoke).mock.calls.filter(([command]) => command === 'record_write')).toHaveLength(0)
|
||||
theme.fontEditorSize = 24; await vi.advanceTimersByTimeAsync(1500); await flushPromises()
|
||||
@@ -33,5 +37,7 @@ it('applies remote appearance without echoing it and only exports approved local
|
||||
writes = vi.mocked(hostInvoke).mock.calls.filter(([command]) => command === 'record_write')
|
||||
expect(writes.some(([, args]) => (args!.request as { record: { kind: string } }).record.kind === 'preferences')).toBe(true)
|
||||
expect(JSON.stringify(writes)).not.toContain('permissionPolicy')
|
||||
layout.workspaceWidth = 440; await vi.advanceTimersByTimeAsync(1500); await flushPromises()
|
||||
expect(vi.mocked(hostInvoke).mock.calls.some(([command, args]) => command === 'record_write' && (args!.request as { record: { kind: string } }).record.kind === 'layout')).toBe(true)
|
||||
await expect(seedCurrentPreferences('other')).rejects.toThrow('PREFERENCE_BINDING_NOT_READY')
|
||||
})
|
||||
|
||||
@@ -7,12 +7,13 @@ import { useHeadingAppearanceStore, normalizeHeadingAppearance } from '@/stores/
|
||||
import { useMarkdownPreferencesStore, normalizeMarkdownPreferences } from '@/stores/markdownPreferences'
|
||||
import { hostInvoke, isDesktop } from './desktop'
|
||||
import { RecordBinding } from './recordBinding'
|
||||
import { useLayoutPreferencesStore } from '@/stores/layoutPreferences'
|
||||
interface Controller { label: string; binding: Pick<RecordBinding<never>, 'capture' | 'poll' | 'seed' | 'stop' | 'keepLocal' | 'useRemote' | 'error' | 'hasDraft'> }
|
||||
export const preferenceSyncIssues = ref<Array<{ kind: string; label: string; error: string; hasDraft: boolean }>>([])
|
||||
let controllers = new Map<string, Controller>(), activeVault = '', installed = false
|
||||
export async function seedCurrentPreferences(vaultId: string) {
|
||||
if (!isDesktop()) return
|
||||
if (vaultId !== activeVault || controllers.size !== 2) throw new Error('PREFERENCE_BINDING_NOT_READY')
|
||||
if (vaultId !== activeVault || controllers.size !== 3) throw new Error('PREFERENCE_BINDING_NOT_READY')
|
||||
for (const { binding } of controllers.values()) {
|
||||
await binding.seed()
|
||||
if (binding.error) throw new Error(binding.error)
|
||||
@@ -29,6 +30,8 @@ export function installPreferenceSync() {
|
||||
installed = true
|
||||
const workspace = useWorkspaceStore(), theme = useThemeStore(), settings = useSettingsStore()
|
||||
const headings = useHeadingAppearanceStore(), markdown = useMarkdownPreferencesStore()
|
||||
const layout = useLayoutPreferencesStore()
|
||||
const readLayout = () => ({ primaryExpanded: layout.primaryExpanded, workspaceWidth: layout.workspaceWidth, chatWidth: layout.chatWidth })
|
||||
let applying = 0
|
||||
const readTheme = () => ({ themeId: theme.currentThemeId, fontEditorSize: theme.fontEditorSize, fontEditorFamily: theme.fontEditorFamily, lineHeight: theme.lineHeight, codeBlockTheme: theme.codeBlockTheme, headings: normalizeHeadingAppearance(headings.preferences) })
|
||||
const readPreferences = () => ({ restoreLastVault: settings.restoreLastVault, autoSaveInterval: settings.autoSaveInterval, language: settings.language, defaultEditorMode: settings.defaultEditorMode, editorLineWidth: settings.editorLineWidth, spellCheck: settings.spellCheck, markdown: normalizeMarkdownPreferences(markdown.preferences), presets: markdown.customPresets.map(preset => ({ name: preset.name, preferences: normalizeMarkdownPreferences(preset.preferences) })) })
|
||||
@@ -49,9 +52,13 @@ export function installPreferenceSync() {
|
||||
settings.defaultEditorMode = data.defaultEditorMode; settings.editorLineWidth = data.editorLineWidth; settings.spellCheck = data.spellCheck
|
||||
markdown.apply(data.markdown); markdown.customPresets = data.presets.map(preset => ({ name: preset.name, preferences: normalizeMarkdownPreferences(preset.preferences) }))
|
||||
}) }) })
|
||||
controllers.set('layout', { label: '侧栏布局', binding: new RecordBinding({ ...common, kind: 'layout', id: 'sidebars', read: readLayout, apply: data => apply(() => {
|
||||
layout.primaryExpanded = data.primaryExpanded; layout.workspaceWidth = data.workspaceWidth; layout.chatWidth = data.chatWidth
|
||||
}) }) })
|
||||
changed()
|
||||
for (const value of controllers.values()) void value.binding.poll()
|
||||
}, { immediate: true, flush: 'sync' })
|
||||
watch(readLayout, () => { if (!applying) controllers.get('layout')?.binding.capture() }, { deep: true, flush: 'sync' })
|
||||
watch(readTheme, () => { if (!applying) controllers.get('theme_settings')?.binding.capture() }, { deep: true, flush: 'sync' })
|
||||
watch(readPreferences, () => { if (!applying) controllers.get('preferences')?.binding.capture() }, { deep: true, flush: 'sync' })
|
||||
setInterval(() => { for (const value of controllers.values()) void value.binding.poll() }, 1500)
|
||||
|
||||
Reference in New Issue
Block a user