From 70d88dabba396b48429f94b93508a1c41f63c3be Mon Sep 17 00:00:00 2001 From: landaiqing Date: Sun, 29 Jun 2025 23:41:34 +0800 Subject: [PATCH] :sparkles: Use SQLite instead of JSON storage --- .../voidraft/internal/models/models.ts | 109 ++-- .../internal/services/configservice.ts | 8 - .../internal/services/documentservice.ts | 71 ++- .../internal/services/extensionservice.ts | 8 - .../internal/services/hotkeyservice.ts | 2 +- .../internal/services/keybindingservice.ts | 8 - .../internal/services/migrationservice.ts | 8 - frontend/src/stores/documentStore.ts | 204 ++++---- frontend/src/stores/editorStore.ts | 37 +- .../views/editor/basic/autoSaveExtension.ts | 145 +++--- go.mod | 16 + go.sum | 45 ++ internal/models/config.go | 2 +- internal/models/document.go | 43 +- internal/services/config_migration_service.go | 10 +- .../services/config_notification_service.go | 206 ++++++-- internal/services/config_service.go | 10 +- internal/services/document_service.go | 479 ++++++++++-------- internal/services/extension_service.go | 4 +- internal/services/hotkey_service.go | 2 +- internal/services/hotkey_service_darwin.go | 4 +- internal/services/hotkey_service_linux.go | 4 +- internal/services/keybinding_service.go | 4 +- internal/services/path_migration_service.go | 4 +- internal/services/service_manager.go | 10 +- 25 files changed, 807 insertions(+), 636 deletions(-) diff --git a/frontend/bindings/voidraft/internal/models/models.ts b/frontend/bindings/voidraft/internal/models/models.ts index 270c4b7..3f8a0c0 100644 --- a/frontend/bindings/voidraft/internal/models/models.ts +++ b/frontend/bindings/voidraft/internal/models/models.ts @@ -159,27 +159,32 @@ export class ConfigMetadata { } /** - * Document 表示一个文档 + * Document 表示一个文档(使用自增主键) */ export class Document { - /** - * 元数据 - */ - "meta": DocumentMeta; - - /** - * 文档内容 - */ + "id": number; + "title": string; "content": string; + "createdAt": time$0.Time; + "updatedAt": time$0.Time; /** Creates a new Document instance. */ constructor($$source: Partial = {}) { - if (!("meta" in $$source)) { - this["meta"] = (new DocumentMeta()); + if (!("id" in $$source)) { + this["id"] = 0; + } + if (!("title" in $$source)) { + this["title"] = ""; } if (!("content" in $$source)) { this["content"] = ""; } + if (!("createdAt" in $$source)) { + this["createdAt"] = null; + } + if (!("updatedAt" in $$source)) { + this["updatedAt"] = null; + } Object.assign(this, $$source); } @@ -188,66 +193,11 @@ export class Document { * Creates a new Document instance from a string or object. */ static createFrom($$source: any = {}): Document { - const $$createField0_0 = $$createType5; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - if ("meta" in $$parsedSource) { - $$parsedSource["meta"] = $$createField0_0($$parsedSource["meta"]); - } return new Document($$parsedSource as Partial); } } -/** - * DocumentMeta 文档元数据 - */ -export class DocumentMeta { - /** - * 文档唯一标识 - */ - "id": string; - - /** - * 文档标题 - */ - "title": string; - - /** - * 最后更新时间 - */ - "lastUpdated": time$0.Time; - - /** - * 创建时间 - */ - "createdAt": time$0.Time; - - /** Creates a new DocumentMeta instance. */ - constructor($$source: Partial = {}) { - if (!("id" in $$source)) { - this["id"] = ""; - } - if (!("title" in $$source)) { - this["title"] = ""; - } - if (!("lastUpdated" in $$source)) { - this["lastUpdated"] = null; - } - if (!("createdAt" in $$source)) { - this["createdAt"] = null; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new DocumentMeta instance from a string or object. - */ - static createFrom($$source: any = {}): DocumentMeta { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new DocumentMeta($$parsedSource as Partial); - } -} - /** * EditingConfig 编辑设置配置 */ @@ -380,7 +330,7 @@ export class Extension { * Creates a new Extension instance from a string or object. */ static createFrom($$source: any = {}): Extension { - const $$createField3_0 = $$createType6; + const $$createField3_0 = $$createType5; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("config" in $$parsedSource) { $$parsedSource["config"] = $$createField3_0($$parsedSource["config"]); @@ -508,7 +458,7 @@ export class GeneralConfig { * Creates a new GeneralConfig instance from a string or object. */ static createFrom($$source: any = {}): GeneralConfig { - const $$createField5_0 = $$createType8; + const $$createField5_0 = $$createType7; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("globalHotkey" in $$parsedSource) { $$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]); @@ -956,8 +906,8 @@ export class KeyBindingConfig { * Creates a new KeyBindingConfig instance from a string or object. */ static createFrom($$source: any = {}): KeyBindingConfig { - const $$createField0_0 = $$createType10; - const $$createField1_0 = $$createType11; + const $$createField0_0 = $$createType9; + const $$createField1_0 = $$createType10; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("keyBindings" in $$parsedSource) { $$parsedSource["keyBindings"] = $$createField0_0($$parsedSource["keyBindings"]); @@ -1110,15 +1060,14 @@ const $$createType1 = EditingConfig.createFrom; const $$createType2 = AppearanceConfig.createFrom; const $$createType3 = UpdatesConfig.createFrom; const $$createType4 = ConfigMetadata.createFrom; -const $$createType5 = DocumentMeta.createFrom; -var $$createType6 = (function $$initCreateType6(...args): any { - if ($$createType6 === $$initCreateType6) { - $$createType6 = $$createType7; +var $$createType5 = (function $$initCreateType5(...args): any { + if ($$createType5 === $$initCreateType5) { + $$createType5 = $$createType6; } - return $$createType6(...args); + return $$createType5(...args); }); -const $$createType7 = $Create.Map($Create.Any, $Create.Any); -const $$createType8 = HotkeyCombo.createFrom; -const $$createType9 = KeyBinding.createFrom; -const $$createType10 = $Create.Array($$createType9); -const $$createType11 = KeyBindingMetadata.createFrom; +const $$createType6 = $Create.Map($Create.Any, $Create.Any); +const $$createType7 = HotkeyCombo.createFrom; +const $$createType8 = KeyBinding.createFrom; +const $$createType9 = $Create.Array($$createType8); +const $$createType10 = KeyBindingMetadata.createFrom; diff --git a/frontend/bindings/voidraft/internal/services/configservice.ts b/frontend/bindings/voidraft/internal/services/configservice.ts index 1067bd3..a5d6756 100644 --- a/frontend/bindings/voidraft/internal/services/configservice.ts +++ b/frontend/bindings/voidraft/internal/services/configservice.ts @@ -42,14 +42,6 @@ export function ResetConfig(): Promise & { cancel(): void } { return $resultPromise; } -/** - * ServiceShutdown 关闭服务 - */ -export function ServiceShutdown(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3963562361) as any; - return $resultPromise; -} - /** * Set 设置配置项 */ diff --git a/frontend/bindings/voidraft/internal/services/documentservice.ts b/frontend/bindings/voidraft/internal/services/documentservice.ts index 55a6cac..10fafd5 100644 --- a/frontend/bindings/voidraft/internal/services/documentservice.ts +++ b/frontend/bindings/voidraft/internal/services/documentservice.ts @@ -2,7 +2,7 @@ // This file is automatically generated. DO NOT EDIT /** - * DocumentService 提供文档管理功能 + * DocumentService provides document management functionality * @module */ @@ -15,18 +15,10 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime"; import * as models$0 from "../models/models.js"; /** - * ForceSave 强制保存 + * CreateDocument creates a new document and returns the created document with ID */ -export function ForceSave(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2767091023) as any; - return $resultPromise; -} - -/** - * GetActiveDocument 获取活动文档 - */ -export function GetActiveDocument(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1785823398) as any; +export function CreateDocument(title: string): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(3360680842, title) as any; let $typingPromise = $resultPromise.then(($result: any) => { return $$createType1($result); }) as any; @@ -35,45 +27,70 @@ export function GetActiveDocument(): Promise & { cance } /** - * Initialize 初始化服务 + * DeleteDocument deletes a document (not allowed if it's the only document) */ -export function Initialize(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3418008221) as any; +export function DeleteDocument(id: number): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(412287269, id) as any; return $resultPromise; } /** - * OnDataPathChanged 处理数据路径变更 + * GetDocumentByID gets a document by ID */ -export function OnDataPathChanged(oldPath: string, newPath: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(269349439, oldPath, newPath) as any; +export function GetDocumentByID(id: number): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(3468193232, id) as any; + let $typingPromise = $resultPromise.then(($result: any) => { + return $$createType1($result); + }) as any; + $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); + return $typingPromise; +} + +/** + * GetFirstDocumentID gets the first document's ID for frontend initialization + */ +export function GetFirstDocumentID(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(2970773833) as any; return $resultPromise; } /** - * ReloadDocument 重新加载文档 + * ListAllDocumentsMeta lists all document metadata */ -export function ReloadDocument(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3093415283) as any; +export function ListAllDocumentsMeta(): Promise<(models$0.Document | null)[]> & { cancel(): void } { + let $resultPromise = $Call.ByID(3073950297) as any; + let $typingPromise = $resultPromise.then(($result: any) => { + return $$createType2($result); + }) as any; + $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); + return $typingPromise; +} + +/** + * OnDataPathChanged handles data path changes + */ +export function OnDataPathChanged(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(269349439) as any; return $resultPromise; } /** - * ServiceShutdown 关闭服务 + * UpdateDocumentContent updates the content of a document */ -export function ServiceShutdown(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(638578044) as any; +export function UpdateDocumentContent(id: number, content: string): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(3251897116, id, content) as any; return $resultPromise; } /** - * UpdateActiveDocumentContent 更新文档内容 + * UpdateDocumentTitle updates the title of a document */ -export function UpdateActiveDocumentContent(content: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1486276638, content) as any; +export function UpdateDocumentTitle(id: number, title: string): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(2045530459, id, title) as any; return $resultPromise; } // Private type creation functions const $$createType0 = models$0.Document.createFrom; const $$createType1 = $Create.Nullable($$createType0); +const $$createType2 = $Create.Array($$createType1); diff --git a/frontend/bindings/voidraft/internal/services/extensionservice.ts b/frontend/bindings/voidraft/internal/services/extensionservice.ts index cd086ac..16cd260 100644 --- a/frontend/bindings/voidraft/internal/services/extensionservice.ts +++ b/frontend/bindings/voidraft/internal/services/extensionservice.ts @@ -42,14 +42,6 @@ export function ResetExtensionToDefault(id: models$0.ExtensionID): Promise return $resultPromise; } -/** - * ServiceShutdown 关闭服务 - */ -export function ServiceShutdown(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(4127635746) as any; - return $resultPromise; -} - /** * UpdateExtensionEnabled 更新扩展启用状态 */ diff --git a/frontend/bindings/voidraft/internal/services/hotkeyservice.ts b/frontend/bindings/voidraft/internal/services/hotkeyservice.ts index 4a2fcbd..78da475 100644 --- a/frontend/bindings/voidraft/internal/services/hotkeyservice.ts +++ b/frontend/bindings/voidraft/internal/services/hotkeyservice.ts @@ -54,7 +54,7 @@ export function RegisterHotkey(hotkey: models$0.HotkeyCombo | null): Promise & { cancel(): void } { let $resultPromise = $Call.ByID(157291181) as any; diff --git a/frontend/bindings/voidraft/internal/services/keybindingservice.ts b/frontend/bindings/voidraft/internal/services/keybindingservice.ts index 924ec75..31aa3f5 100644 --- a/frontend/bindings/voidraft/internal/services/keybindingservice.ts +++ b/frontend/bindings/voidraft/internal/services/keybindingservice.ts @@ -38,14 +38,6 @@ export function GetKeyBindingConfig(): Promise return $typingPromise; } -/** - * ServiceShutdown 关闭服务 - */ -export function ServiceShutdown(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1610182855) as any; - return $resultPromise; -} - // Private type creation functions const $$createType0 = models$0.KeyBinding.createFrom; const $$createType1 = $Create.Array($$createType0); diff --git a/frontend/bindings/voidraft/internal/services/migrationservice.ts b/frontend/bindings/voidraft/internal/services/migrationservice.ts index 3c5dd7a..83b9bdd 100644 --- a/frontend/bindings/voidraft/internal/services/migrationservice.ts +++ b/frontend/bindings/voidraft/internal/services/migrationservice.ts @@ -42,13 +42,5 @@ export function MigrateDirectory(srcPath: string, dstPath: string): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3472042605) as any; - return $resultPromise; -} - // Private type creation functions const $$createType0 = $models.MigrationProgress.createFrom; diff --git a/frontend/src/stores/documentStore.ts b/frontend/src/stores/documentStore.ts index 9f71fe9..fd5424b 100644 --- a/frontend/src/stores/documentStore.ts +++ b/frontend/src/stores/documentStore.ts @@ -4,103 +4,113 @@ import {DocumentService} from '@/../bindings/voidraft/internal/services'; import {Document} from '@/../bindings/voidraft/internal/models/models'; export const useDocumentStore = defineStore('document', () => { - // 状态 - const activeDocument = ref(null); - const isLoading = ref(false); - const isSaving = ref(false); - const lastSaved = ref(null); - - // 计算属性 - const documentContent = computed(() => activeDocument.value?.content ?? ''); - const documentTitle = computed(() => activeDocument.value?.meta?.title ?? ''); - const hasActiveDocument = computed(() => !!activeDocument.value); - const isSaveInProgress = computed(() => isSaving.value); - const lastSavedTime = computed(() => lastSaved.value); - - // 加载文档 - const loadDocument = async (): Promise => { - if (isLoading.value) return null; - - isLoading.value = true; - try { - const doc = await DocumentService.GetActiveDocument(); - activeDocument.value = doc; - return doc; - } catch (error) { - return null; - } finally { - isLoading.value = false; - } - }; - - // 保存文档 - const saveDocument = async (content: string): Promise => { - if (isSaving.value) return false; - - isSaving.value = true; - try { - await DocumentService.UpdateActiveDocumentContent(content); - lastSaved.value = new Date(); - - // 更新本地副本 - if (activeDocument.value) { - activeDocument.value.content = content; - activeDocument.value.meta.lastUpdated = lastSaved.value; - } - - return true; - } catch (error) { - return false; - } finally { - isSaving.value = false; - } - }; - - // 强制保存文档到磁盘 - const forceSaveDocument = async (): Promise => { - if (isSaving.value) return false; - - isSaving.value = true; - try { - await DocumentService.ForceSave(); - lastSaved.value = new Date(); - - // 更新时间戳 - if (activeDocument.value) { - activeDocument.value.meta.lastUpdated = lastSaved.value; - } - - return true; - } catch (error) { - return false; - } finally { - isSaving.value = false; - } - }; - - // 初始化 - const initialize = async (): Promise => { - await loadDocument(); - }; - - return { // 状态 - activeDocument, - isLoading, - isSaving, - lastSaved, - + const currentDocument = ref(null); + const isLoading = ref(false); + const isSaving = ref(false); + const lastSaved = ref(null); + // 计算属性 - documentContent, - documentTitle, - hasActiveDocument, - isSaveInProgress, - lastSavedTime, - - // 方法 - loadDocument, - saveDocument, - forceSaveDocument, - initialize - }; + const documentContent = computed(() => currentDocument.value?.content ?? ''); + const documentTitle = computed(() => currentDocument.value?.title ?? ''); + const hasDocument = computed(() => !!currentDocument.value); + const isSaveInProgress = computed(() => isSaving.value); + const lastSavedTime = computed(() => lastSaved.value); + + // 加载文档 + const loadDocument = async (documentId = 1): Promise => { + if (isLoading.value) return currentDocument.value; + + isLoading.value = true; + try { + const doc = await DocumentService.GetDocumentByID(documentId); + if (doc) { + currentDocument.value = doc; + return doc; + } + return null; + } catch (error) { + return null; + } finally { + isLoading.value = false; + } + }; + + // 保存文档内容 + const saveDocumentContent = async (content: string): Promise => { + // 如果内容没有变化,直接返回成功 + if (currentDocument.value?.content === content) { + return true; + } + + // 如果正在保存中,直接返回 + if (isSaving.value) { + return false; + } + + isSaving.value = true; + try { + const documentId = currentDocument.value?.id || 1; + await DocumentService.UpdateDocumentContent(documentId, content); + + const now = new Date(); + lastSaved.value = now; + + // 更新本地副本 + if (currentDocument.value) { + currentDocument.value.content = content; + currentDocument.value.updatedAt = now; + } + + return true; + } catch (error) { + return false; + } finally { + isSaving.value = false; + } + }; + + // 保存文档标题 + const saveDocumentTitle = async (title: string): Promise => { + if (!currentDocument.value || currentDocument.value.title === title) { + return true; + } + + try { + await DocumentService.UpdateDocumentTitle(currentDocument.value.id, title); + const now = new Date(); + lastSaved.value = now; + currentDocument.value.title = title; + currentDocument.value.updatedAt = now; + return true; + } catch (error) { + return false; + } + }; + + // 初始化 + const initialize = async (): Promise => { + await loadDocument(); + }; + + return { + // 状态 + currentDocument, + isLoading, + isSaving, + lastSaved, + + // 计算属性 + documentContent, + documentTitle, + hasDocument, + isSaveInProgress, + lastSavedTime, + + // 方法 + loadDocument, + saveDocumentContent, + saveDocumentTitle, + initialize + }; }); \ No newline at end of file diff --git a/frontend/src/stores/editorStore.ts b/frontend/src/stores/editorStore.ts index a733172..f0be247 100644 --- a/frontend/src/stores/editorStore.ts +++ b/frontend/src/stores/editorStore.ts @@ -5,16 +5,15 @@ import {EditorState, Extension} from '@codemirror/state'; import {useConfigStore} from './configStore'; import {useDocumentStore} from './documentStore'; import {useThemeStore} from './themeStore'; -import {useKeybindingStore} from './keybindingStore'; import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models'; -import {DocumentService, ExtensionService} from '@/../bindings/voidraft/internal/services'; +import {ExtensionService} from '@/../bindings/voidraft/internal/services'; import {ensureSyntaxTree} from "@codemirror/language" import {createBasicSetup} from '@/views/editor/basic/basicSetup'; import {createThemeExtension, updateEditorTheme} from '@/views/editor/basic/themeExtension'; import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtension'; import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension'; import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension'; -import {createAutoSavePlugin, createSaveShortcutPlugin} from '@/views/editor/basic/autoSaveExtension'; +import {createAutoSavePlugin} from '@/views/editor/basic/autoSaveExtension'; import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap'; import {createDynamicExtensions, getExtensionManager, setExtensionManagerView} from '@/views/editor/manager'; import {useExtensionStore} from './extensionStore'; @@ -128,21 +127,10 @@ export const useEditorStore = defineStore('editor', () => { updateDocumentStats ); - // 创建保存快捷键插件 - const saveShortcutExtension = createSaveShortcutPlugin(() => { - if (editorView.value) { - handleManualSave(); - } - }); // 创建自动保存插件 const autoSaveExtension = createAutoSavePlugin({ - debounceDelay: 300, // 300毫秒的输入防抖 - onSave: (success) => { - if (success) { - documentStore.lastSaved = new Date(); - } - } + debounceDelay: configStore.config.editing.autoSaveDelay }); // 代码块功能 const codeBlockExtension = createCodeBlockExtension({ @@ -164,7 +152,6 @@ export const useEditorStore = defineStore('editor', () => { ...tabExtensions, fontExtension, statsExtension, - saveShortcutExtension, autoSaveExtension, codeBlockExtension, ...dynamicExtensions @@ -220,19 +207,6 @@ export const useEditorStore = defineStore('editor', () => { }); }; - // 手动保存文档 - const handleManualSave = async () => { - if (!editorView.value) return; - - const view = editorView.value as EditorView; - const content = view.state.doc.toString(); - - // 先更新内容 - await DocumentService.UpdateActiveDocumentContent(content); - // 然后调用强制保存方法 - await documentStore.forceSaveDocument(); - }; - // 销毁编辑器 const destroyEditor = () => { if (editorView.value) { @@ -283,7 +257,7 @@ export const useEditorStore = defineStore('editor', () => { // 如果需要更新配置 await ExtensionService.UpdateExtensionState(id, enabled, config) } - + // 更新前端编辑器扩展 const manager = getExtensionManager() if (manager) { @@ -292,7 +266,7 @@ export const useEditorStore = defineStore('editor', () => { // 重新加载扩展配置 await extensionStore.loadExtensions() - + // 更新快捷键映射 if (editorView.value) { updateKeymapExtension(editorView.value) @@ -321,7 +295,6 @@ export const useEditorStore = defineStore('editor', () => { createEditor, reconfigureTabSettings, reconfigureFontSettings, - handleManualSave, destroyEditor, updateExtension }; diff --git a/frontend/src/views/editor/basic/autoSaveExtension.ts b/frontend/src/views/editor/basic/autoSaveExtension.ts index fc8210c..d18b016 100644 --- a/frontend/src/views/editor/basic/autoSaveExtension.ts +++ b/frontend/src/views/editor/basic/autoSaveExtension.ts @@ -1,98 +1,109 @@ import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view'; -import { DocumentService } from '@/../bindings/voidraft/internal/services'; -import { useDebounceFn } from '@vueuse/core'; +import { useDocumentStore } from '@/stores/documentStore'; -// 定义自动保存配置选项 +// 自动保存配置选项 export interface AutoSaveOptions { - // 保存回调 - onSave?: (success: boolean) => void; - // 内容变更延迟传递(毫秒)- 输入时不会立即发送,有一个小延迟,避免频繁调用后端 + // 防抖延迟(毫秒) debounceDelay?: number; + // 保存状态回调 + onSaveStart?: () => void; + onSaveSuccess?: () => void; + onSaveError?: () => void; +} + +/** + * 简单防抖函数 + */ +function debounce any>( + func: T, + delay: number +): T & { cancel: () => void } { + let timeoutId: number | null = null; + + const debounced = ((...args: Parameters) => { + if (timeoutId !== null) { + clearTimeout(timeoutId); + } + + timeoutId = window.setTimeout(() => { + timeoutId = null; + func(...args); + }, delay); + }) as T & { cancel: () => void }; + + debounced.cancel = () => { + if (timeoutId !== null) { + clearTimeout(timeoutId); + timeoutId = null; + } + }; + + return debounced; } /** * 创建自动保存插件 - * - * @param options 配置选项 - * @returns EditorView.Plugin */ export function createAutoSavePlugin(options: AutoSaveOptions = {}) { - const { - onSave = () => {}, - debounceDelay = 2000 + const { + debounceDelay = 2000, + onSaveStart = () => {}, + onSaveSuccess = () => {}, + onSaveError = () => {} } = options; return ViewPlugin.fromClass( - class { - private isActive: boolean = true; - private isSaving: boolean = false; - private readonly contentUpdateFn: (view: EditorView) => void; + class AutoSavePlugin { + private documentStore = useDocumentStore(); + private debouncedSave: ((content: string) => void) & { cancel: () => void }; + private isDestroyed = false; + private lastContent = ''; constructor(private view: EditorView) { - // 创建内容更新函数,简单传递内容给后端 - this.contentUpdateFn = this.createDebouncedUpdateFn(debounceDelay); + this.lastContent = view.state.doc.toString(); + this.debouncedSave = debounce( + (content: string) => this.performSave(content), + debounceDelay + ); } - /** - * 创建防抖的内容更新函数 - */ - private createDebouncedUpdateFn(delay: number): (view: EditorView) => void { - // 使用VueUse的防抖函数创建一个新函数 - return useDebounceFn(async (view: EditorView) => { - // 如果插件已不活跃或正在保存中,不发送 - if (!this.isActive || this.isSaving) return; + private async performSave(content: string): Promise { + if (this.isDestroyed) return; + + try { + onSaveStart(); + const success = await this.documentStore.saveDocumentContent(content); - this.isSaving = true; - const content = view.state.doc.toString(); - - try { - // 简单将内容传递给后端,让后端处理保存策略 - await DocumentService.UpdateActiveDocumentContent(content); - onSave(true); - } catch (err) { - // 静默处理错误,不在控制台打印 - onSave(false); - } finally { - this.isSaving = false; + if (success) { + this.lastContent = content; + onSaveSuccess(); + } else { + onSaveError(); } - }, delay); + } catch (error) { + onSaveError(); + } } update(update: ViewUpdate) { - // 如果内容没有变化,直接返回 - if (!update.docChanged) return; + if (!update.docChanged || this.isDestroyed) return; - // 调用防抖函数 - this.contentUpdateFn(this.view); + const newContent = this.view.state.doc.toString(); + if (newContent === this.lastContent) return; + + this.debouncedSave(newContent); } destroy() { - // 标记插件不再活跃 - this.isActive = false; + this.isDestroyed = true; + this.debouncedSave.cancel(); - // 静默发送最终内容,忽略错误 - const content = this.view.state.doc.toString(); - DocumentService.UpdateActiveDocumentContent(content).then(); + // 如果内容有变化,立即保存 + const currentContent = this.view.state.doc.toString(); + if (currentContent !== this.lastContent) { + this.documentStore.saveDocumentContent(currentContent).catch(() => {}); + } } } ); -} - -/** - * 创建处理保存快捷键的插件 - * @param onSave 保存回调 - * @returns EditorView.Plugin - */ -export function createSaveShortcutPlugin(onSave: () => void) { - return EditorView.domEventHandlers({ - keydown: (event) => { - // Ctrl+S / Cmd+S - if ((event.ctrlKey || event.metaKey) && event.key === 's') { - event.preventDefault(); - onSave(); - return true; - } - return false; - } - }); } \ No newline at end of file diff --git a/go.mod b/go.mod index d985e27..c720eef 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/knadh/koanf/v2 v2.2.1 github.com/wailsapp/wails/v3 v3.0.0-alpha.9 golang.org/x/sys v0.33.0 + modernc.org/sqlite v1.21.0 ) require ( @@ -23,6 +24,7 @@ require ( github.com/bep/debounce v1.2.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/ebitengine/purego v0.8.4 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/fatih/structs v1.1.0 // indirect @@ -38,6 +40,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect @@ -50,6 +53,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/samber/lo v1.51.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect @@ -59,8 +63,20 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/crypto v0.39.0 // indirect golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect + golang.org/x/mod v0.25.0 // indirect golang.org/x/net v0.41.0 // indirect + golang.org/x/sync v0.15.0 // indirect golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + lukechampine.com/uint128 v1.2.0 // indirect + modernc.org/cc/v3 v3.40.0 // indirect + modernc.org/ccgo/v3 v3.16.13 // indirect + modernc.org/libc v1.22.3 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/opt v0.1.3 // indirect + modernc.org/strutil v1.1.3 // indirect + modernc.org/token v1.0.1 // indirect ) diff --git a/go.sum b/go.sum index f06d182..809d24b 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGL github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= @@ -57,12 +59,16 @@ github.com/google/go-github/v63 v63.0.0 h1:13xwK/wk9alSokujB9lJkuzdmQuVn2QCPeck7 github.com/google/go-github/v63 v63.0.0/go.mod h1:IqbcrgUmIcEaioWrGYei/09o+ge5vhffGOcxrO0AfmA= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= @@ -95,6 +101,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -109,6 +117,9 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -139,10 +150,14 @@ golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -161,6 +176,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -174,3 +191,31 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY= +modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.21.0 h1:4aP4MdUf15i3R3M2mx6Q90WHKz3nZLoz96zlB6tNdow= +modernc.org/sqlite v1.21.0/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.15.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws= +modernc.org/tcl v1.15.1/go.mod h1:aEjeGJX2gz1oWKOLDVZ2tnEWLUrIn8H+GFu+akoDhqs= +modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= +modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE= +modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ= diff --git a/internal/models/config.go b/internal/models/config.go index dbb8349..9b2ea12 100644 --- a/internal/models/config.go +++ b/internal/models/config.go @@ -135,7 +135,7 @@ func NewDefaultAppConfig() *AppConfig { TabSize: 4, TabType: TabTypeSpaces, // 保存选项 - AutoSaveDelay: 5000, // 5秒后自动保存 + AutoSaveDelay: 2000, // 2秒后自动保存 }, Appearance: AppearanceConfig{ Language: LangZhCN, diff --git a/internal/models/document.go b/internal/models/document.go index 99569af..e316aad 100644 --- a/internal/models/document.go +++ b/internal/models/document.go @@ -4,38 +4,27 @@ import ( "time" ) -// DocumentMeta 文档元数据 -type DocumentMeta struct { - ID string `json:"id"` // 文档唯一标识 - Title string `json:"title"` // 文档标题 - LastUpdated time.Time `json:"lastUpdated"` // 最后更新时间 - CreatedAt time.Time `json:"createdAt"` // 创建时间 -} - -// Document 表示一个文档 +// Document 表示一个文档(使用自增主键) type Document struct { - Meta DocumentMeta `json:"meta"` // 元数据 - Content string `json:"content"` // 文档内容 + ID int64 `json:"id" db:"id"` + Title string `json:"title" db:"title"` + Content string `json:"content" db:"content"` + CreatedAt time.Time `json:"createdAt" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` } -// DocumentInfo 文档信息(不包含内容,用于列表展示) -type DocumentInfo struct { - ID string `json:"id"` // 文档ID - Title string `json:"title"` // 文档标题 - LastUpdated time.Time `json:"lastUpdated"` // 最后更新时间 - Path string `json:"path"` // 文档路径 +// NewDocument 创建新文档(不需要传ID,由数据库自增) +func NewDocument(title, content string) *Document { + now := time.Now() + return &Document{ + Title: title, + Content: content, + CreatedAt: now, + UpdatedAt: now, + } } // NewDefaultDocument 创建默认文档 func NewDefaultDocument() *Document { - now := time.Now() - return &Document{ - Meta: DocumentMeta{ - ID: "default", - Title: "默认文档", - LastUpdated: now, - CreatedAt: now, - }, - Content: "∞∞∞text-a\n", - } + return NewDocument("default", "∞∞∞text-a\n") } diff --git a/internal/services/config_migration_service.go b/internal/services/config_migration_service.go index e5fd67c..a0ea2ce 100644 --- a/internal/services/config_migration_service.go +++ b/internal/services/config_migration_service.go @@ -132,7 +132,7 @@ func (cms *ConfigMigrationService[T]) checkResourceLimits() error { return nil } -// createBackupOptimized 优化的备份创建(单次扫描删除旧备份) +// createBackupOptimized 优化的备份创建 func (cms *ConfigMigrationService[T]) createBackupOptimized() (string, error) { if _, err := os.Stat(cms.configPath); os.IsNotExist(err) { return "", nil @@ -155,7 +155,7 @@ func (cms *ConfigMigrationService[T]) createBackupOptimized() (string, error) { return newBackupPath, copyFile(cms.configPath, newBackupPath) } -// tryQuickRecovery 快速恢复检查(避免完整的备份恢复) +// tryQuickRecovery 快速恢复检查 func (cms *ConfigMigrationService[T]) tryQuickRecovery(existingConfig *koanf.Koanf) { var testConfig T if existingConfig.Unmarshal("", &testConfig) != nil { @@ -213,7 +213,7 @@ func (cms *ConfigMigrationService[T]) mergeInPlace(existingConfig *koanf.Koanf, return false, fmt.Errorf("config merge failed: %w", err) } - // 更新元数据(直接操作,无需重新序列化) + // 更新元数据 mergeKoanf.Set("metadata.version", cms.targetVersion) mergeKoanf.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339)) @@ -249,12 +249,12 @@ func (cms *ConfigMigrationService[T]) atomicWrite(existingConfig *koanf.Koanf, c return existingConfig.Load(&BytesProvider{configBytes}, jsonparser.Parser()) } -// fastMerge 快速合并函数(优化版本) +// fastMerge 快速合并函数 func (cms *ConfigMigrationService[T]) fastMerge(src, dest map[string]interface{}) error { return cms.fastMergeRecursive(src, dest, 0) } -// fastMergeRecursive 快速递归合并(单次遍历,最小化反射使用) +// fastMergeRecursive 快速递归合并 func (cms *ConfigMigrationService[T]) fastMergeRecursive(src, dest map[string]interface{}, depth int) error { if depth > MaxRecursionDepth { return fmt.Errorf("recursion depth exceeded") diff --git a/internal/services/config_notification_service.go b/internal/services/config_notification_service.go index dc823cd..2be9032 100644 --- a/internal/services/config_notification_service.go +++ b/internal/services/config_notification_service.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/json" "fmt" + "reflect" "sync" "time" "voidraft/internal/models" @@ -28,6 +29,7 @@ type ConfigChangeCallback func(changeType ConfigChangeType, oldConfig, newConfig // ConfigListener 配置监听器 type ConfigListener struct { + ID string // 监听器唯一ID Name string // 监听器名称 ChangeType ConfigChangeType // 监听的配置变更类型 Callback ConfigChangeCallback // 回调函数(现在包含新旧配置) @@ -45,9 +47,10 @@ type ConfigListener struct { // ConfigNotificationService 配置通知服务 type ConfigNotificationService struct { - listeners sync.Map // 使用sync.Map替代普通map+锁 - logger *log.LoggerService // 日志服务 - koanf *koanf.Koanf // koanf实例 + listeners map[ConfigChangeType][]*ConfigListener // 支持多监听器的map + mu sync.RWMutex // 监听器map的读写锁 + logger *log.LoggerService // 日志服务 + koanf *koanf.Koanf // koanf实例 ctx context.Context cancel context.CancelFunc wg sync.WaitGroup @@ -57,20 +60,19 @@ type ConfigNotificationService struct { func NewConfigNotificationService(k *koanf.Koanf, logger *log.LoggerService) *ConfigNotificationService { ctx, cancel := context.WithCancel(context.Background()) return &ConfigNotificationService{ - logger: logger, - koanf: k, - ctx: ctx, - cancel: cancel, + listeners: make(map[ConfigChangeType][]*ConfigListener), + logger: logger, + koanf: k, + ctx: ctx, + cancel: cancel, } } // RegisterListener 注册配置监听器 func (cns *ConfigNotificationService) RegisterListener(listener *ConfigListener) error { - // 清理已存在的监听器 - if existingValue, loaded := cns.listeners.LoadAndDelete(listener.ChangeType); loaded { - if existing, ok := existingValue.(interface{ cancel() }); ok { - existing.cancel() - } + // 生成唯一ID如果没有提供 + if listener.ID == "" { + listener.ID = fmt.Sprintf("%s_%d", listener.Name, time.Now().UnixNano()) } // 初始化新监听器 @@ -80,7 +82,11 @@ func (cns *ConfigNotificationService) RegisterListener(listener *ConfigListener) return fmt.Errorf("failed to initialize listener state: %w", err) } - cns.listeners.Store(listener.ChangeType, listener) + // 添加到监听器列表 + cns.mu.Lock() + cns.listeners[listener.ChangeType] = append(cns.listeners[listener.ChangeType], listener) + cns.mu.Unlock() + return nil } @@ -92,7 +98,7 @@ func (cns *ConfigNotificationService) initializeListenerState(listener *ConfigLi if config := listener.GetConfigFunc(cns.koanf); config != nil { listener.mu.Lock() - listener.lastConfig = deepCopyConfig(config) + listener.lastConfig = deepCopyConfigReflect(config) listener.lastConfigHash = computeConfigHash(config) listener.mu.Unlock() } @@ -100,23 +106,59 @@ func (cns *ConfigNotificationService) initializeListenerState(listener *ConfigLi return nil } -// UnregisterListener 注销配置监听器 -func (cns *ConfigNotificationService) UnregisterListener(changeType ConfigChangeType) { - if value, loaded := cns.listeners.LoadAndDelete(changeType); loaded { - if listener, ok := value.(*ConfigListener); ok { +// UnregisterListener 注销指定ID的配置监听器 +func (cns *ConfigNotificationService) UnregisterListener(changeType ConfigChangeType, listenerID string) { + cns.mu.Lock() + defer cns.mu.Unlock() + + listeners := cns.listeners[changeType] + for i, listener := range listeners { + if listener.ID == listenerID { + // 取消监听器 + listener.cancel() + // 从切片中移除 + cns.listeners[changeType] = append(listeners[:i], listeners[i+1:]...) + break + } + } + + // 如果该类型没有监听器了,删除整个条目 + if len(cns.listeners[changeType]) == 0 { + delete(cns.listeners, changeType) + } +} + +// UnregisterAllListeners 注销指定类型的所有监听器 +func (cns *ConfigNotificationService) UnregisterAllListeners(changeType ConfigChangeType) { + cns.mu.Lock() + defer cns.mu.Unlock() + + if listeners, exists := cns.listeners[changeType]; exists { + for _, listener := range listeners { listener.cancel() } + delete(cns.listeners, changeType) } } // CheckConfigChanges 检查配置变更并通知相关监听器 func (cns *ConfigNotificationService) CheckConfigChanges() { - cns.listeners.Range(func(key, value interface{}) bool { - if listener, ok := value.(*ConfigListener); ok { + cns.mu.RLock() + allListeners := make(map[ConfigChangeType][]*ConfigListener) + for changeType, listeners := range cns.listeners { + // 创建监听器切片的副本以避免并发访问问题 + listenersCopy := make([]*ConfigListener, len(listeners)) + copy(listenersCopy, listeners) + allListeners[changeType] = listenersCopy + } + cns.mu.RUnlock() + + // 检查所有监听器 + for _, listeners := range allListeners { + for _, listener := range listeners { cns.checkAndNotify(listener) } - return true - }) + } } // checkAndNotify 检查配置变更并通知 @@ -144,7 +186,7 @@ func (cns *ConfigNotificationService) checkAndNotify(listener *ConfigListener) { if hasChanges { listener.mu.Lock() - listener.lastConfig = deepCopyConfig(currentConfig) + listener.lastConfig = deepCopyConfigReflect(currentConfig) listener.lastConfigHash = currentHash listener.mu.Unlock() @@ -167,7 +209,82 @@ func computeConfigHash(config *models.AppConfig) string { return fmt.Sprintf("%x", hash) } -// deepCopyConfig 深拷贝配置对象 +// deepCopyConfigReflect 使用反射实现高效深拷贝 +func deepCopyConfigReflect(src *models.AppConfig) *models.AppConfig { + if src == nil { + return nil + } + + // 使用反射进行深拷贝 + srcValue := reflect.ValueOf(src).Elem() + dstValue := reflect.New(srcValue.Type()).Elem() + + deepCopyValue(srcValue, dstValue) + + return dstValue.Addr().Interface().(*models.AppConfig) +} + +// deepCopyValue 递归深拷贝reflect.Value +func deepCopyValue(src, dst reflect.Value) { + switch src.Kind() { + case reflect.Ptr: + if src.IsNil() { + return + } + dst.Set(reflect.New(src.Elem().Type())) + deepCopyValue(src.Elem(), dst.Elem()) + + case reflect.Struct: + for i := 0; i < src.NumField(); i++ { + if dst.Field(i).CanSet() { + deepCopyValue(src.Field(i), dst.Field(i)) + } + } + + case reflect.Slice: + if src.IsNil() { + return + } + dst.Set(reflect.MakeSlice(src.Type(), src.Len(), src.Cap())) + for i := 0; i < src.Len(); i++ { + deepCopyValue(src.Index(i), dst.Index(i)) + } + + case reflect.Map: + if src.IsNil() { + return + } + dst.Set(reflect.MakeMap(src.Type())) + for _, key := range src.MapKeys() { + srcValue := src.MapIndex(key) + dstValue := reflect.New(srcValue.Type()).Elem() + deepCopyValue(srcValue, dstValue) + dst.SetMapIndex(key, dstValue) + } + + case reflect.Interface: + if src.IsNil() { + return + } + srcValue := src.Elem() + dstValue := reflect.New(srcValue.Type()).Elem() + deepCopyValue(srcValue, dstValue) + dst.Set(dstValue) + + case reflect.Array: + for i := 0; i < src.Len(); i++ { + deepCopyValue(src.Index(i), dst.Index(i)) + } + + default: + // 对于基本类型和string,直接赋值 + if dst.CanSet() { + dst.Set(src) + } + } +} + +// deepCopyConfig 保留原有的JSON深拷贝方法作为备用 func deepCopyConfig(src *models.AppConfig) *models.AppConfig { if src == nil { return nil @@ -197,8 +314,8 @@ func (cns *ConfigNotificationService) debounceNotify(listener *ConfigListener, o } // 创建配置副本,避免在闭包中持有原始引用 - oldConfigCopy := deepCopyConfig(oldConfig) - newConfigCopy := deepCopyConfig(newConfig) + oldConfigCopy := deepCopyConfigReflect(oldConfig) + newConfigCopy := deepCopyConfigReflect(newConfig) changeType := listener.ChangeType @@ -246,18 +363,33 @@ func (cns *ConfigNotificationService) executeCallback( func (cns *ConfigNotificationService) Cleanup() { cns.cancel() // 取消所有context - cns.listeners.Range(func(key, value interface{}) bool { - cns.listeners.Delete(key) - return true - }) + cns.mu.Lock() + for changeType, listeners := range cns.listeners { + for _, listener := range listeners { + listener.cancel() + } + delete(cns.listeners, changeType) + } + cns.mu.Unlock() cns.wg.Wait() // 等待所有协程完成 } +// GetListeners 获取指定类型的所有监听器 +func (cns *ConfigNotificationService) GetListeners(changeType ConfigChangeType) []*ConfigListener { + cns.mu.RLock() + defer cns.mu.RUnlock() + + listeners := cns.listeners[changeType] + result := make([]*ConfigListener, len(listeners)) + copy(result, listeners) + return result +} + // CreateHotkeyListener 创建热键配置监听器 -func CreateHotkeyListener(callback func(enable bool, hotkey *models.HotkeyCombo) error) *ConfigListener { +func CreateHotkeyListener(name string, callback func(enable bool, hotkey *models.HotkeyCombo) error) *ConfigListener { return &ConfigListener{ - Name: "HotkeyListener", + Name: name, ChangeType: ConfigChangeTypeHotkey, Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error { if newConfig != nil { @@ -279,9 +411,9 @@ func CreateHotkeyListener(callback func(enable bool, hotkey *models.HotkeyCombo) } // CreateDataPathListener 创建数据路径配置监听器 -func CreateDataPathListener(callback func(oldPath, newPath string) error) *ConfigListener { +func CreateDataPathListener(name string, callback func() error) *ConfigListener { return &ConfigListener{ - Name: "DataPathListener", + Name: name, ChangeType: ConfigChangeTypeDataPath, Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error { var oldPath, newPath string @@ -298,7 +430,7 @@ func CreateDataPathListener(callback func(oldPath, newPath string) error) *Confi } if oldPath != newPath { - return callback(oldPath, newPath) + return callback() } return nil }, @@ -313,8 +445,8 @@ func CreateDataPathListener(callback func(oldPath, newPath string) error) *Confi } } -// ServiceShutdown 关闭服务 -func (cns *ConfigNotificationService) ServiceShutdown() error { +// OnShutdown 关闭服务 +func (cns *ConfigNotificationService) OnShutdown() error { cns.Cleanup() return nil } diff --git a/internal/services/config_service.go b/internal/services/config_service.go index 073f4ad..d5060ba 100644 --- a/internal/services/config_service.go +++ b/internal/services/config_service.go @@ -290,22 +290,22 @@ func (cs *ConfigService) SetHotkeyChangeCallback(callback func(enable bool, hotk defer cs.mu.Unlock() // 创建热键监听器并注册 - hotkeyListener := CreateHotkeyListener(callback) + hotkeyListener := CreateHotkeyListener("DefaultHotkeyListener", callback) return cs.notificationService.RegisterListener(hotkeyListener) } // SetDataPathChangeCallback 设置数据路径配置变更回调 -func (cs *ConfigService) SetDataPathChangeCallback(callback func(oldPath, newPath string) error) error { +func (cs *ConfigService) SetDataPathChangeCallback(callback func() error) error { cs.mu.Lock() defer cs.mu.Unlock() // 创建数据路径监听器并注册 - dataPathListener := CreateDataPathListener(callback) + dataPathListener := CreateDataPathListener("DefaultDataPathListener", callback) return cs.notificationService.RegisterListener(dataPathListener) } -// ServiceShutdown 关闭服务 -func (cs *ConfigService) ServiceShutdown() error { +// OnShutdown 关闭服务 +func (cs *ConfigService) OnShutdown() error { cs.stopWatching() if cs.notificationService != nil { cs.notificationService.Cleanup() diff --git a/internal/services/document_service.go b/internal/services/document_service.go index ef9da32..7aae1e7 100644 --- a/internal/services/document_service.go +++ b/internal/services/document_service.go @@ -2,270 +2,337 @@ package services import ( "context" - "os" + "database/sql" + "fmt" "path/filepath" "sync" - "sync/atomic" "time" "voidraft/internal/models" + "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/services/log" + _ "modernc.org/sqlite" // SQLite driver ) -// DocumentService 提供文档管理功能 +// SQL constants for database operations +const ( + dbName = "voidraft.db" + // Database schema (simplified single table with auto-increment ID) + sqlCreateDocumentsTable = ` +CREATE TABLE IF NOT EXISTS documents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content TEXT DEFAULT '∞∞∞text-a', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +)` + + // Performance optimization indexes + sqlCreateIndexUpdatedAt = `CREATE INDEX IF NOT EXISTS idx_documents_updated_at ON documents(updated_at DESC)` + sqlCreateIndexTitle = `CREATE INDEX IF NOT EXISTS idx_documents_title ON documents(title)` + + // SQLite performance optimization settings + sqlOptimizationSettings = ` +PRAGMA journal_mode = WAL; +PRAGMA synchronous = NORMAL; +PRAGMA cache_size = -64000; +PRAGMA temp_store = MEMORY; +PRAGMA foreign_keys = ON;` + + // Document operations + sqlGetDocumentByID = ` +SELECT id, title, content, created_at, updated_at +FROM documents +WHERE id = ?` + + sqlInsertDocument = ` +INSERT INTO documents (title, content, created_at, updated_at) +VALUES (?, ?, ?, ?)` + + sqlUpdateDocument = ` +UPDATE documents +SET title = ?, content = ?, updated_at = ? +WHERE id = ?` + + sqlUpdateDocumentContent = ` +UPDATE documents +SET content = ?, updated_at = ? +WHERE id = ?` + + sqlUpdateDocumentTitle = ` +UPDATE documents +SET title = ?, updated_at = ? +WHERE id = ?` + + sqlDeleteDocument = ` +DELETE FROM documents WHERE id = ?` + + sqlListAllDocumentsMeta = ` +SELECT id, title, created_at, updated_at +FROM documents +ORDER BY updated_at DESC` + + sqlGetFirstDocumentID = ` +SELECT id FROM documents ORDER BY id LIMIT 1` +) + +// DocumentService provides document management functionality type DocumentService struct { configService *ConfigService logger *log.LoggerService - docStore *Store[models.Document] - - // 文档状态管理 - mu sync.RWMutex - document *models.Document - - // 自动保存管理 + db *sql.DB + mu sync.RWMutex ctx context.Context - cancel context.CancelFunc - isDirty atomic.Bool - lastSaveTime atomic.Int64 // unix timestamp - saveScheduler chan struct{} - - // 初始化控制 - initOnce sync.Once } -// NewDocumentService 创建文档服务 +// NewDocumentService creates a new document service func NewDocumentService(configService *ConfigService, logger *log.LoggerService) *DocumentService { if logger == nil { logger = log.New() } - ctx, cancel := context.WithCancel(context.Background()) return &DocumentService{ configService: configService, logger: logger, - ctx: ctx, - cancel: cancel, - saveScheduler: make(chan struct{}, 1), } } -// Initialize 初始化服务 -func (ds *DocumentService) Initialize() error { - var initErr error - ds.initOnce.Do(func() { - initErr = ds.doInitialize() - }) - return initErr +// OnStartup initializes the service when the application starts +func (ds *DocumentService) OnStartup(ctx context.Context, _ application.ServiceOptions) error { + ds.ctx = ctx + return ds.initDatabase() } -// doInitialize 执行初始化 -func (ds *DocumentService) doInitialize() error { - if err := ds.initStore(); err != nil { - return err - } - - ds.loadDocument() - go ds.autoSaveWorker() - return nil -} - -// initStore 初始化存储 -func (ds *DocumentService) initStore() error { - docPath, err := ds.getDocumentPath() +// initDatabase initializes the SQLite database +func (ds *DocumentService) initDatabase() error { + dbPath, err := ds.getDatabasePath() if err != nil { - return err + return fmt.Errorf("failed to get database path: %w", err) } - if err := os.MkdirAll(filepath.Dir(docPath), 0755); err != nil { - return err + db, err := sql.Open("sqlite", dbPath) + if err != nil { + return fmt.Errorf("failed to open database: %w", err) } - ds.docStore = NewStore[models.Document](StoreOption{ - FilePath: docPath, - AutoSave: false, - Logger: ds.logger, - }) + ds.db = db + + // Apply optimization settings + if _, err := db.Exec(sqlOptimizationSettings); err != nil { + return fmt.Errorf("failed to apply optimization settings: %w", err) + } + + // Create table + if _, err := db.Exec(sqlCreateDocumentsTable); err != nil { + return fmt.Errorf("failed to create table: %w", err) + } + + // Create indexes + if err := ds.createIndexes(); err != nil { + return fmt.Errorf("failed to create indexes: %w", err) + } + + // Ensure default document exists + if err := ds.ensureDefaultDocument(); err != nil { + return fmt.Errorf("failed to ensure default document: %w", err) + } return nil } -// getDocumentPath 获取文档路径 -func (ds *DocumentService) getDocumentPath() (string, error) { +// getDatabasePath gets the database file path +func (ds *DocumentService) getDatabasePath() (string, error) { config, err := ds.configService.GetConfig() if err != nil { return "", err } - return filepath.Join(config.General.DataPath, "docs", "default.json"), nil + return filepath.Join(config.General.DataPath, dbName), nil } -// loadDocument 加载文档 -func (ds *DocumentService) loadDocument() { - ds.mu.Lock() - defer ds.mu.Unlock() - - doc := ds.docStore.Get() - if doc.Meta.ID == "" { - ds.document = models.NewDefaultDocument() - ds.docStore.Set(*ds.document) - } else { - ds.document = &doc +// createIndexes creates database indexes +func (ds *DocumentService) createIndexes() error { + indexes := []string{ + sqlCreateIndexUpdatedAt, + sqlCreateIndexTitle, } - ds.lastSaveTime.Store(time.Now().Unix()) -} - -// GetActiveDocument 获取活动文档 -func (ds *DocumentService) GetActiveDocument() (*models.Document, error) { - ds.mu.RLock() - defer ds.mu.RUnlock() - - if ds.document == nil { - return nil, nil - } - - docCopy := *ds.document - return &docCopy, nil -} - -// UpdateActiveDocumentContent 更新文档内容 -func (ds *DocumentService) UpdateActiveDocumentContent(content string) error { - ds.mu.Lock() - defer ds.mu.Unlock() - - if ds.document != nil && ds.document.Content != content { - ds.document.Content = content - ds.markDirty() - } - - return nil -} - -// markDirty 标记为脏数据并触发自动保存 -func (ds *DocumentService) markDirty() { - if ds.isDirty.CompareAndSwap(false, true) { - select { - case ds.saveScheduler <- struct{}{}: - default: // 已有保存任务在队列中 - } - } -} - -// ForceSave 强制保存 -func (ds *DocumentService) ForceSave() error { - return ds.saveDocument() -} - -// saveDocument 保存文档 -func (ds *DocumentService) saveDocument() error { - ds.mu.Lock() - defer ds.mu.Unlock() - - if ds.document == nil { - return nil - } - - now := time.Now() - ds.document.Meta.LastUpdated = now - - if err := ds.docStore.Set(*ds.document); err != nil { - return err - } - - if err := ds.docStore.Save(); err != nil { - return err - } - - ds.isDirty.Store(false) - ds.lastSaveTime.Store(now.Unix()) - return nil -} - -// autoSaveWorker 自动保存工作协程 -func (ds *DocumentService) autoSaveWorker() { - ticker := time.NewTicker(ds.getAutoSaveInterval()) - defer ticker.Stop() - - for { - select { - case <-ds.ctx.Done(): - return - case <-ds.saveScheduler: - ds.performAutoSave() - case <-ticker.C: - if ds.isDirty.Load() { - ds.performAutoSave() - } - // 动态调整保存间隔 - ticker.Reset(ds.getAutoSaveInterval()) - } - } -} - -// getAutoSaveInterval 获取自动保存间隔 -func (ds *DocumentService) getAutoSaveInterval() time.Duration { - config, err := ds.configService.GetConfig() - if err != nil { - return 5 * time.Second - } - return time.Duration(config.Editing.AutoSaveDelay) * time.Millisecond -} - -// performAutoSave 执行自动保存 -func (ds *DocumentService) performAutoSave() { - if !ds.isDirty.Load() { - return - } - - // 防抖:避免过于频繁的保存 - lastSave := time.Unix(ds.lastSaveTime.Load(), 0) - if time.Since(lastSave) < time.Second { - // 延迟重试 - time.AfterFunc(time.Second, func() { - select { - case ds.saveScheduler <- struct{}{}: - default: - } - }) - return - } - - if err := ds.saveDocument(); err != nil { - ds.logger.Error("auto save failed", "error", err) - } -} - -// ReloadDocument 重新加载文档 -func (ds *DocumentService) ReloadDocument() error { - // 先保存当前文档 - if ds.isDirty.Load() { - if err := ds.saveDocument(); err != nil { + for _, index := range indexes { + if _, err := ds.db.Exec(index); err != nil { return err } } + return nil +} - // 重新初始化存储 - if err := ds.initStore(); err != nil { +// ensureDefaultDocument ensures a default document exists +func (ds *DocumentService) ensureDefaultDocument() error { + // Check if any document exists + var count int + err := ds.db.QueryRow("SELECT COUNT(*) FROM documents").Scan(&count) + if err != nil { return err } - // 重新加载 - ds.loadDocument() + // If no documents exist, create default document + if count == 0 { + defaultDoc := models.NewDefaultDocument() + _, err := ds.CreateDocument(defaultDoc.Title) + return err + } return nil } -// ServiceShutdown 关闭服务 -func (ds *DocumentService) ServiceShutdown() error { - ds.cancel() // 停止自动保存工作协程 +// GetDocumentByID gets a document by ID +func (ds *DocumentService) GetDocumentByID(id int64) (*models.Document, error) { + ds.mu.RLock() + defer ds.mu.RUnlock() - // 最后保存 - if ds.isDirty.Load() { - return ds.saveDocument() + var doc models.Document + row := ds.db.QueryRow(sqlGetDocumentByID, id) + err := row.Scan(&doc.ID, &doc.Title, &doc.Content, &doc.CreatedAt, &doc.UpdatedAt) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, fmt.Errorf("failed to get document by ID: %w", err) } + return &doc, nil +} + +// CreateDocument creates a new document and returns the created document with ID +func (ds *DocumentService) CreateDocument(title string) (*models.Document, error) { + ds.mu.Lock() + defer ds.mu.Unlock() + + // Create document with default content + now := time.Now() + doc := &models.Document{ + Title: title, + Content: "∞∞∞text-a\n", + CreatedAt: now, + UpdatedAt: now, + } + + result, err := ds.db.Exec(sqlInsertDocument, doc.Title, doc.Content, doc.CreatedAt, doc.UpdatedAt) + if err != nil { + return nil, fmt.Errorf("failed to create document: %w", err) + } + + // Get the auto-generated ID + id, err := result.LastInsertId() + if err != nil { + return nil, fmt.Errorf("failed to get last insert ID: %w", err) + } + + // Return the created document with ID + doc.ID = id + return doc, nil +} + +// UpdateDocumentContent updates the content of a document +func (ds *DocumentService) UpdateDocumentContent(id int64, content string) error { + ds.mu.Lock() + defer ds.mu.Unlock() + + _, err := ds.db.Exec(sqlUpdateDocumentContent, content, time.Now(), id) + if err != nil { + return fmt.Errorf("failed to update document content: %w", err) + } return nil } -// OnDataPathChanged 处理数据路径变更 -func (ds *DocumentService) OnDataPathChanged(oldPath, newPath string) error { - return ds.ReloadDocument() +// UpdateDocumentTitle updates the title of a document +func (ds *DocumentService) UpdateDocumentTitle(id int64, title string) error { + ds.mu.Lock() + defer ds.mu.Unlock() + + _, err := ds.db.Exec(sqlUpdateDocumentTitle, title, time.Now(), id) + if err != nil { + return fmt.Errorf("failed to update document title: %w", err) + } + return nil +} + +// DeleteDocument deletes a document (not allowed if it's the only document) +func (ds *DocumentService) DeleteDocument(id int64) error { + ds.mu.Lock() + defer ds.mu.Unlock() + + // Check if this is the only document + var count int + err := ds.db.QueryRow("SELECT COUNT(*) FROM documents").Scan(&count) + if err != nil { + return fmt.Errorf("failed to count documents: %w", err) + } + + // Don't allow deletion if this is the only document + if count <= 1 { + return fmt.Errorf("cannot delete the last document") + } + + _, err = ds.db.Exec(sqlDeleteDocument, id) + if err != nil { + return fmt.Errorf("failed to delete document: %w", err) + } + return nil +} + +// ListAllDocumentsMeta lists all document metadata +func (ds *DocumentService) ListAllDocumentsMeta() ([]*models.Document, error) { + ds.mu.RLock() + defer ds.mu.RUnlock() + + rows, err := ds.db.Query(sqlListAllDocumentsMeta) + if err != nil { + return nil, fmt.Errorf("failed to list document meta: %w", err) + } + defer rows.Close() + + var documents []*models.Document + for rows.Next() { + var doc models.Document + err := rows.Scan(&doc.ID, &doc.Title, &doc.CreatedAt, &doc.UpdatedAt) + if err != nil { + return nil, fmt.Errorf("failed to scan document meta: %w", err) + } + documents = append(documents, &doc) + } + + return documents, nil +} + +// GetFirstDocumentID gets the first document's ID for frontend initialization +func (ds *DocumentService) GetFirstDocumentID() (int64, error) { + ds.mu.RLock() + defer ds.mu.RUnlock() + + var id int64 + err := ds.db.QueryRow(sqlGetFirstDocumentID).Scan(&id) + if err != nil { + if err == sql.ErrNoRows { + return 0, nil // No documents exist + } + return 0, fmt.Errorf("failed to get first document ID: %w", err) + } + return id, nil +} + +// OnShutdown shuts down the service when the application closes +func (ds *DocumentService) OnShutdown() error { + if ds.db != nil { + return ds.db.Close() + } + return nil +} + +// OnDataPathChanged handles data path changes +func (ds *DocumentService) OnDataPathChanged() error { + // Close existing database + if ds.db != nil { + ds.db.Close() + } + + // Reinitialize with new path + return ds.initDatabase() } diff --git a/internal/services/extension_service.go b/internal/services/extension_service.go index b9ae944..e5784e2 100644 --- a/internal/services/extension_service.go +++ b/internal/services/extension_service.go @@ -257,8 +257,8 @@ func (es *ExtensionService) ResetAllExtensionsToDefault() error { return nil } -// ServiceShutdown 关闭服务 -func (es *ExtensionService) ServiceShutdown() error { +// OnShutdown 关闭服务 +func (es *ExtensionService) OnShutdown() error { es.cancel() return nil } diff --git a/internal/services/hotkey_service.go b/internal/services/hotkey_service.go index 901af5c..feb8d69 100644 --- a/internal/services/hotkey_service.go +++ b/internal/services/hotkey_service.go @@ -259,7 +259,7 @@ func (hs *HotkeyService) IsRegistered() bool { return hs.isRegistered.Load() } -// ServiceShutdown 关闭服务 +// OnShutdown 关闭服务 func (hs *HotkeyService) ServiceShutdown() error { hs.cancel() hs.wg.Wait() diff --git a/internal/services/hotkey_service_darwin.go b/internal/services/hotkey_service_darwin.go index cf628b6..b68b42c 100644 --- a/internal/services/hotkey_service_darwin.go +++ b/internal/services/hotkey_service_darwin.go @@ -283,8 +283,8 @@ func (hs *HotkeyService) IsRegistered() bool { return hs.isRegistered.Load() } -// ServiceShutdown 关闭热键服务 -func (hs *HotkeyService) ServiceShutdown() error { +// OnShutdown 关闭热键服务 +func (hs *HotkeyService) OnShutdown() error { return hs.UnregisterHotkey() } diff --git a/internal/services/hotkey_service_linux.go b/internal/services/hotkey_service_linux.go index b9301eb..f8ec918 100644 --- a/internal/services/hotkey_service_linux.go +++ b/internal/services/hotkey_service_linux.go @@ -384,8 +384,8 @@ func (hs *HotkeyService) IsRegistered() bool { return hs.isRegistered.Load() } -// ServiceShutdown 关闭服务 -func (hs *HotkeyService) ServiceShutdown() error { +// OnShutdown 关闭服务 +func (hs *HotkeyService) OnShutdown() error { hs.cancel() hs.wg.Wait() C.closeX11Display() diff --git a/internal/services/keybinding_service.go b/internal/services/keybinding_service.go index fe9d9f3..a0179cb 100644 --- a/internal/services/keybinding_service.go +++ b/internal/services/keybinding_service.go @@ -185,8 +185,8 @@ func (kbs *KeyBindingService) GetAllKeyBindings() ([]models.KeyBinding, error) { return config.KeyBindings, nil } -// ServiceShutdown 关闭服务 -func (kbs *KeyBindingService) ServiceShutdown() error { +// OnShutdown 关闭服务 +func (kbs *KeyBindingService) OnShutdown() error { kbs.cancel() return nil } diff --git a/internal/services/path_migration_service.go b/internal/services/path_migration_service.go index 5d87790..13c1686 100644 --- a/internal/services/path_migration_service.go +++ b/internal/services/path_migration_service.go @@ -417,8 +417,8 @@ func (ms *MigrationService) CancelMigration() error { return fmt.Errorf("no active migration to cancel") } -// ServiceShutdown 服务关闭 -func (ms *MigrationService) ServiceShutdown() error { +// OnShutdown 服务关闭 +func (ms *MigrationService) OnShutdown() error { ms.CancelMigration() return nil } diff --git a/internal/services/service_manager.go b/internal/services/service_manager.go index 464117f..ccfd4f5 100644 --- a/internal/services/service_manager.go +++ b/internal/services/service_manager.go @@ -74,19 +74,13 @@ func NewServiceManager() *ServiceManager { } // 设置数据路径变更监听,处理配置重置和路径变更 - err = configService.SetDataPathChangeCallback(func(oldPath, newPath string) error { - return documentService.OnDataPathChanged(oldPath, newPath) + err = configService.SetDataPathChangeCallback(func() error { + return documentService.OnDataPathChanged() }) if err != nil { panic(err) } - // 初始化文档服务 - err = documentService.Initialize() - if err != nil { - panic(err) - } - return &ServiceManager{ configService: configService, documentService: documentService,