✨ Use SQLite instead of JSON storage
This commit is contained in:
@@ -159,27 +159,32 @@ export class ConfigMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Document 表示一个文档
|
* Document 表示一个文档(使用自增主键)
|
||||||
*/
|
*/
|
||||||
export class Document {
|
export class Document {
|
||||||
/**
|
"id": number;
|
||||||
* 元数据
|
"title": string;
|
||||||
*/
|
|
||||||
"meta": DocumentMeta;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文档内容
|
|
||||||
*/
|
|
||||||
"content": string;
|
"content": string;
|
||||||
|
"createdAt": time$0.Time;
|
||||||
|
"updatedAt": time$0.Time;
|
||||||
|
|
||||||
/** Creates a new Document instance. */
|
/** Creates a new Document instance. */
|
||||||
constructor($$source: Partial<Document> = {}) {
|
constructor($$source: Partial<Document> = {}) {
|
||||||
if (!("meta" in $$source)) {
|
if (!("id" in $$source)) {
|
||||||
this["meta"] = (new DocumentMeta());
|
this["id"] = 0;
|
||||||
|
}
|
||||||
|
if (!("title" in $$source)) {
|
||||||
|
this["title"] = "";
|
||||||
}
|
}
|
||||||
if (!("content" in $$source)) {
|
if (!("content" in $$source)) {
|
||||||
this["content"] = "";
|
this["content"] = "";
|
||||||
}
|
}
|
||||||
|
if (!("createdAt" in $$source)) {
|
||||||
|
this["createdAt"] = null;
|
||||||
|
}
|
||||||
|
if (!("updatedAt" in $$source)) {
|
||||||
|
this["updatedAt"] = null;
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(this, $$source);
|
Object.assign(this, $$source);
|
||||||
}
|
}
|
||||||
@@ -188,66 +193,11 @@ export class Document {
|
|||||||
* Creates a new Document instance from a string or object.
|
* Creates a new Document instance from a string or object.
|
||||||
*/
|
*/
|
||||||
static createFrom($$source: any = {}): Document {
|
static createFrom($$source: any = {}): Document {
|
||||||
const $$createField0_0 = $$createType5;
|
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
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<Document>);
|
return new Document($$parsedSource as Partial<Document>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* DocumentMeta 文档元数据
|
|
||||||
*/
|
|
||||||
export class DocumentMeta {
|
|
||||||
/**
|
|
||||||
* 文档唯一标识
|
|
||||||
*/
|
|
||||||
"id": string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文档标题
|
|
||||||
*/
|
|
||||||
"title": string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 最后更新时间
|
|
||||||
*/
|
|
||||||
"lastUpdated": time$0.Time;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建时间
|
|
||||||
*/
|
|
||||||
"createdAt": time$0.Time;
|
|
||||||
|
|
||||||
/** Creates a new DocumentMeta instance. */
|
|
||||||
constructor($$source: Partial<DocumentMeta> = {}) {
|
|
||||||
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<DocumentMeta>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EditingConfig 编辑设置配置
|
* EditingConfig 编辑设置配置
|
||||||
*/
|
*/
|
||||||
@@ -380,7 +330,7 @@ export class Extension {
|
|||||||
* Creates a new Extension instance from a string or object.
|
* Creates a new Extension instance from a string or object.
|
||||||
*/
|
*/
|
||||||
static createFrom($$source: any = {}): Extension {
|
static createFrom($$source: any = {}): Extension {
|
||||||
const $$createField3_0 = $$createType6;
|
const $$createField3_0 = $$createType5;
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
if ("config" in $$parsedSource) {
|
if ("config" in $$parsedSource) {
|
||||||
$$parsedSource["config"] = $$createField3_0($$parsedSource["config"]);
|
$$parsedSource["config"] = $$createField3_0($$parsedSource["config"]);
|
||||||
@@ -508,7 +458,7 @@ export class GeneralConfig {
|
|||||||
* Creates a new GeneralConfig instance from a string or object.
|
* Creates a new GeneralConfig instance from a string or object.
|
||||||
*/
|
*/
|
||||||
static createFrom($$source: any = {}): GeneralConfig {
|
static createFrom($$source: any = {}): GeneralConfig {
|
||||||
const $$createField5_0 = $$createType8;
|
const $$createField5_0 = $$createType7;
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
if ("globalHotkey" in $$parsedSource) {
|
if ("globalHotkey" in $$parsedSource) {
|
||||||
$$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]);
|
$$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]);
|
||||||
@@ -956,8 +906,8 @@ export class KeyBindingConfig {
|
|||||||
* Creates a new KeyBindingConfig instance from a string or object.
|
* Creates a new KeyBindingConfig instance from a string or object.
|
||||||
*/
|
*/
|
||||||
static createFrom($$source: any = {}): KeyBindingConfig {
|
static createFrom($$source: any = {}): KeyBindingConfig {
|
||||||
const $$createField0_0 = $$createType10;
|
const $$createField0_0 = $$createType9;
|
||||||
const $$createField1_0 = $$createType11;
|
const $$createField1_0 = $$createType10;
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
if ("keyBindings" in $$parsedSource) {
|
if ("keyBindings" in $$parsedSource) {
|
||||||
$$parsedSource["keyBindings"] = $$createField0_0($$parsedSource["keyBindings"]);
|
$$parsedSource["keyBindings"] = $$createField0_0($$parsedSource["keyBindings"]);
|
||||||
@@ -1110,15 +1060,14 @@ const $$createType1 = EditingConfig.createFrom;
|
|||||||
const $$createType2 = AppearanceConfig.createFrom;
|
const $$createType2 = AppearanceConfig.createFrom;
|
||||||
const $$createType3 = UpdatesConfig.createFrom;
|
const $$createType3 = UpdatesConfig.createFrom;
|
||||||
const $$createType4 = ConfigMetadata.createFrom;
|
const $$createType4 = ConfigMetadata.createFrom;
|
||||||
const $$createType5 = DocumentMeta.createFrom;
|
var $$createType5 = (function $$initCreateType5(...args): any {
|
||||||
var $$createType6 = (function $$initCreateType6(...args): any {
|
if ($$createType5 === $$initCreateType5) {
|
||||||
if ($$createType6 === $$initCreateType6) {
|
$$createType5 = $$createType6;
|
||||||
$$createType6 = $$createType7;
|
|
||||||
}
|
}
|
||||||
return $$createType6(...args);
|
return $$createType5(...args);
|
||||||
});
|
});
|
||||||
const $$createType7 = $Create.Map($Create.Any, $Create.Any);
|
const $$createType6 = $Create.Map($Create.Any, $Create.Any);
|
||||||
const $$createType8 = HotkeyCombo.createFrom;
|
const $$createType7 = HotkeyCombo.createFrom;
|
||||||
const $$createType9 = KeyBinding.createFrom;
|
const $$createType8 = KeyBinding.createFrom;
|
||||||
const $$createType10 = $Create.Array($$createType9);
|
const $$createType9 = $Create.Array($$createType8);
|
||||||
const $$createType11 = KeyBindingMetadata.createFrom;
|
const $$createType10 = KeyBindingMetadata.createFrom;
|
||||||
|
@@ -42,14 +42,6 @@ export function ResetConfig(): Promise<void> & { cancel(): void } {
|
|||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ServiceShutdown 关闭服务
|
|
||||||
*/
|
|
||||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(3963562361) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set 设置配置项
|
* Set 设置配置项
|
||||||
*/
|
*/
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DocumentService 提供文档管理功能
|
* DocumentService provides document management functionality
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -15,18 +15,10 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
|||||||
import * as models$0 from "../models/models.js";
|
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<void> & { cancel(): void } {
|
export function CreateDocument(title: string): Promise<models$0.Document | null> & { cancel(): void } {
|
||||||
let $resultPromise = $Call.ByID(2767091023) as any;
|
let $resultPromise = $Call.ByID(3360680842, title) as any;
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GetActiveDocument 获取活动文档
|
|
||||||
*/
|
|
||||||
export function GetActiveDocument(): Promise<models$0.Document | null> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(1785823398) as any;
|
|
||||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||||
return $$createType1($result);
|
return $$createType1($result);
|
||||||
}) as any;
|
}) as any;
|
||||||
@@ -35,45 +27,70 @@ export function GetActiveDocument(): Promise<models$0.Document | null> & { cance
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize 初始化服务
|
* DeleteDocument deletes a document (not allowed if it's the only document)
|
||||||
*/
|
*/
|
||||||
export function Initialize(): Promise<void> & { cancel(): void } {
|
export function DeleteDocument(id: number): Promise<void> & { cancel(): void } {
|
||||||
let $resultPromise = $Call.ByID(3418008221) as any;
|
let $resultPromise = $Call.ByID(412287269, id) as any;
|
||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OnDataPathChanged 处理数据路径变更
|
* GetDocumentByID gets a document by ID
|
||||||
*/
|
*/
|
||||||
export function OnDataPathChanged(oldPath: string, newPath: string): Promise<void> & { cancel(): void } {
|
export function GetDocumentByID(id: number): Promise<models$0.Document | null> & { cancel(): void } {
|
||||||
let $resultPromise = $Call.ByID(269349439, oldPath, newPath) as any;
|
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<number> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2970773833) as any;
|
||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ReloadDocument 重新加载文档
|
* ListAllDocumentsMeta lists all document metadata
|
||||||
*/
|
*/
|
||||||
export function ReloadDocument(): Promise<void> & { cancel(): void } {
|
export function ListAllDocumentsMeta(): Promise<(models$0.Document | null)[]> & { cancel(): void } {
|
||||||
let $resultPromise = $Call.ByID(3093415283) as any;
|
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<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(269349439) as any;
|
||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ServiceShutdown 关闭服务
|
* UpdateDocumentContent updates the content of a document
|
||||||
*/
|
*/
|
||||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
export function UpdateDocumentContent(id: number, content: string): Promise<void> & { cancel(): void } {
|
||||||
let $resultPromise = $Call.ByID(638578044) as any;
|
let $resultPromise = $Call.ByID(3251897116, id, content) as any;
|
||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UpdateActiveDocumentContent 更新文档内容
|
* UpdateDocumentTitle updates the title of a document
|
||||||
*/
|
*/
|
||||||
export function UpdateActiveDocumentContent(content: string): Promise<void> & { cancel(): void } {
|
export function UpdateDocumentTitle(id: number, title: string): Promise<void> & { cancel(): void } {
|
||||||
let $resultPromise = $Call.ByID(1486276638, content) as any;
|
let $resultPromise = $Call.ByID(2045530459, id, title) as any;
|
||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private type creation functions
|
// Private type creation functions
|
||||||
const $$createType0 = models$0.Document.createFrom;
|
const $$createType0 = models$0.Document.createFrom;
|
||||||
const $$createType1 = $Create.Nullable($$createType0);
|
const $$createType1 = $Create.Nullable($$createType0);
|
||||||
|
const $$createType2 = $Create.Array($$createType1);
|
||||||
|
@@ -42,14 +42,6 @@ export function ResetExtensionToDefault(id: models$0.ExtensionID): Promise<void>
|
|||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ServiceShutdown 关闭服务
|
|
||||||
*/
|
|
||||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(4127635746) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UpdateExtensionEnabled 更新扩展启用状态
|
* UpdateExtensionEnabled 更新扩展启用状态
|
||||||
*/
|
*/
|
||||||
|
@@ -54,7 +54,7 @@ export function RegisterHotkey(hotkey: models$0.HotkeyCombo | null): Promise<voi
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ServiceShutdown 关闭服务
|
* OnShutdown 关闭服务
|
||||||
*/
|
*/
|
||||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||||
let $resultPromise = $Call.ByID(157291181) as any;
|
let $resultPromise = $Call.ByID(157291181) as any;
|
||||||
|
@@ -38,14 +38,6 @@ export function GetKeyBindingConfig(): Promise<models$0.KeyBindingConfig | null>
|
|||||||
return $typingPromise;
|
return $typingPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ServiceShutdown 关闭服务
|
|
||||||
*/
|
|
||||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(1610182855) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private type creation functions
|
// Private type creation functions
|
||||||
const $$createType0 = models$0.KeyBinding.createFrom;
|
const $$createType0 = models$0.KeyBinding.createFrom;
|
||||||
const $$createType1 = $Create.Array($$createType0);
|
const $$createType1 = $Create.Array($$createType0);
|
||||||
|
@@ -42,13 +42,5 @@ export function MigrateDirectory(srcPath: string, dstPath: string): Promise<void
|
|||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ServiceShutdown 服务关闭
|
|
||||||
*/
|
|
||||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(3472042605) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private type creation functions
|
// Private type creation functions
|
||||||
const $$createType0 = $models.MigrationProgress.createFrom;
|
const $$createType0 = $models.MigrationProgress.createFrom;
|
||||||
|
@@ -4,103 +4,113 @@ import {DocumentService} from '@/../bindings/voidraft/internal/services';
|
|||||||
import {Document} from '@/../bindings/voidraft/internal/models/models';
|
import {Document} from '@/../bindings/voidraft/internal/models/models';
|
||||||
|
|
||||||
export const useDocumentStore = defineStore('document', () => {
|
export const useDocumentStore = defineStore('document', () => {
|
||||||
// 状态
|
|
||||||
const activeDocument = ref<Document | null>(null);
|
|
||||||
const isLoading = ref(false);
|
|
||||||
const isSaving = ref(false);
|
|
||||||
const lastSaved = ref<Date | null>(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<Document | null> => {
|
|
||||||
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<boolean> => {
|
|
||||||
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<boolean> => {
|
|
||||||
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<void> => {
|
|
||||||
await loadDocument();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
// 状态
|
// 状态
|
||||||
activeDocument,
|
const currentDocument = ref<Document | null>(null);
|
||||||
isLoading,
|
const isLoading = ref(false);
|
||||||
isSaving,
|
const isSaving = ref(false);
|
||||||
lastSaved,
|
const lastSaved = ref<Date | null>(null);
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
documentContent,
|
const documentContent = computed(() => currentDocument.value?.content ?? '');
|
||||||
documentTitle,
|
const documentTitle = computed(() => currentDocument.value?.title ?? '');
|
||||||
hasActiveDocument,
|
const hasDocument = computed(() => !!currentDocument.value);
|
||||||
isSaveInProgress,
|
const isSaveInProgress = computed(() => isSaving.value);
|
||||||
lastSavedTime,
|
const lastSavedTime = computed(() => lastSaved.value);
|
||||||
|
|
||||||
// 方法
|
// 加载文档
|
||||||
loadDocument,
|
const loadDocument = async (documentId = 1): Promise<Document | null> => {
|
||||||
saveDocument,
|
if (isLoading.value) return currentDocument.value;
|
||||||
forceSaveDocument,
|
|
||||||
initialize
|
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<boolean> => {
|
||||||
|
// 如果内容没有变化,直接返回成功
|
||||||
|
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<boolean> => {
|
||||||
|
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<void> => {
|
||||||
|
await loadDocument();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
currentDocument,
|
||||||
|
isLoading,
|
||||||
|
isSaving,
|
||||||
|
lastSaved,
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
documentContent,
|
||||||
|
documentTitle,
|
||||||
|
hasDocument,
|
||||||
|
isSaveInProgress,
|
||||||
|
lastSavedTime,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
loadDocument,
|
||||||
|
saveDocumentContent,
|
||||||
|
saveDocumentTitle,
|
||||||
|
initialize
|
||||||
|
};
|
||||||
});
|
});
|
@@ -5,16 +5,15 @@ import {EditorState, Extension} from '@codemirror/state';
|
|||||||
import {useConfigStore} from './configStore';
|
import {useConfigStore} from './configStore';
|
||||||
import {useDocumentStore} from './documentStore';
|
import {useDocumentStore} from './documentStore';
|
||||||
import {useThemeStore} from './themeStore';
|
import {useThemeStore} from './themeStore';
|
||||||
import {useKeybindingStore} from './keybindingStore';
|
|
||||||
import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
|
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 {ensureSyntaxTree} from "@codemirror/language"
|
||||||
import {createBasicSetup} from '@/views/editor/basic/basicSetup';
|
import {createBasicSetup} from '@/views/editor/basic/basicSetup';
|
||||||
import {createThemeExtension, updateEditorTheme} from '@/views/editor/basic/themeExtension';
|
import {createThemeExtension, updateEditorTheme} from '@/views/editor/basic/themeExtension';
|
||||||
import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtension';
|
import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtension';
|
||||||
import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension';
|
import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension';
|
||||||
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
|
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 {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
|
||||||
import {createDynamicExtensions, getExtensionManager, setExtensionManagerView} from '@/views/editor/manager';
|
import {createDynamicExtensions, getExtensionManager, setExtensionManagerView} from '@/views/editor/manager';
|
||||||
import {useExtensionStore} from './extensionStore';
|
import {useExtensionStore} from './extensionStore';
|
||||||
@@ -128,21 +127,10 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
updateDocumentStats
|
updateDocumentStats
|
||||||
);
|
);
|
||||||
|
|
||||||
// 创建保存快捷键插件
|
|
||||||
const saveShortcutExtension = createSaveShortcutPlugin(() => {
|
|
||||||
if (editorView.value) {
|
|
||||||
handleManualSave();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建自动保存插件
|
// 创建自动保存插件
|
||||||
const autoSaveExtension = createAutoSavePlugin({
|
const autoSaveExtension = createAutoSavePlugin({
|
||||||
debounceDelay: 300, // 300毫秒的输入防抖
|
debounceDelay: configStore.config.editing.autoSaveDelay
|
||||||
onSave: (success) => {
|
|
||||||
if (success) {
|
|
||||||
documentStore.lastSaved = new Date();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// 代码块功能
|
// 代码块功能
|
||||||
const codeBlockExtension = createCodeBlockExtension({
|
const codeBlockExtension = createCodeBlockExtension({
|
||||||
@@ -164,7 +152,6 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
...tabExtensions,
|
...tabExtensions,
|
||||||
fontExtension,
|
fontExtension,
|
||||||
statsExtension,
|
statsExtension,
|
||||||
saveShortcutExtension,
|
|
||||||
autoSaveExtension,
|
autoSaveExtension,
|
||||||
codeBlockExtension,
|
codeBlockExtension,
|
||||||
...dynamicExtensions
|
...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 = () => {
|
const destroyEditor = () => {
|
||||||
if (editorView.value) {
|
if (editorView.value) {
|
||||||
@@ -283,7 +257,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
// 如果需要更新配置
|
// 如果需要更新配置
|
||||||
await ExtensionService.UpdateExtensionState(id, enabled, config)
|
await ExtensionService.UpdateExtensionState(id, enabled, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新前端编辑器扩展
|
// 更新前端编辑器扩展
|
||||||
const manager = getExtensionManager()
|
const manager = getExtensionManager()
|
||||||
if (manager) {
|
if (manager) {
|
||||||
@@ -292,7 +266,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 重新加载扩展配置
|
// 重新加载扩展配置
|
||||||
await extensionStore.loadExtensions()
|
await extensionStore.loadExtensions()
|
||||||
|
|
||||||
// 更新快捷键映射
|
// 更新快捷键映射
|
||||||
if (editorView.value) {
|
if (editorView.value) {
|
||||||
updateKeymapExtension(editorView.value)
|
updateKeymapExtension(editorView.value)
|
||||||
@@ -321,7 +295,6 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
createEditor,
|
createEditor,
|
||||||
reconfigureTabSettings,
|
reconfigureTabSettings,
|
||||||
reconfigureFontSettings,
|
reconfigureFontSettings,
|
||||||
handleManualSave,
|
|
||||||
destroyEditor,
|
destroyEditor,
|
||||||
updateExtension
|
updateExtension
|
||||||
};
|
};
|
||||||
|
@@ -1,98 +1,109 @@
|
|||||||
import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
|
import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
|
||||||
import { DocumentService } from '@/../bindings/voidraft/internal/services';
|
import { useDocumentStore } from '@/stores/documentStore';
|
||||||
import { useDebounceFn } from '@vueuse/core';
|
|
||||||
|
|
||||||
// 定义自动保存配置选项
|
// 自动保存配置选项
|
||||||
export interface AutoSaveOptions {
|
export interface AutoSaveOptions {
|
||||||
// 保存回调
|
// 防抖延迟(毫秒)
|
||||||
onSave?: (success: boolean) => void;
|
|
||||||
// 内容变更延迟传递(毫秒)- 输入时不会立即发送,有一个小延迟,避免频繁调用后端
|
|
||||||
debounceDelay?: number;
|
debounceDelay?: number;
|
||||||
|
// 保存状态回调
|
||||||
|
onSaveStart?: () => void;
|
||||||
|
onSaveSuccess?: () => void;
|
||||||
|
onSaveError?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单防抖函数
|
||||||
|
*/
|
||||||
|
function debounce<T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
delay: number
|
||||||
|
): T & { cancel: () => void } {
|
||||||
|
let timeoutId: number | null = null;
|
||||||
|
|
||||||
|
const debounced = ((...args: Parameters<T>) => {
|
||||||
|
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 = {}) {
|
export function createAutoSavePlugin(options: AutoSaveOptions = {}) {
|
||||||
const {
|
const {
|
||||||
onSave = () => {},
|
debounceDelay = 2000,
|
||||||
debounceDelay = 2000
|
onSaveStart = () => {},
|
||||||
|
onSaveSuccess = () => {},
|
||||||
|
onSaveError = () => {}
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
return ViewPlugin.fromClass(
|
return ViewPlugin.fromClass(
|
||||||
class {
|
class AutoSavePlugin {
|
||||||
private isActive: boolean = true;
|
private documentStore = useDocumentStore();
|
||||||
private isSaving: boolean = false;
|
private debouncedSave: ((content: string) => void) & { cancel: () => void };
|
||||||
private readonly contentUpdateFn: (view: EditorView) => void;
|
private isDestroyed = false;
|
||||||
|
private lastContent = '';
|
||||||
|
|
||||||
constructor(private view: EditorView) {
|
constructor(private view: EditorView) {
|
||||||
// 创建内容更新函数,简单传递内容给后端
|
this.lastContent = view.state.doc.toString();
|
||||||
this.contentUpdateFn = this.createDebouncedUpdateFn(debounceDelay);
|
this.debouncedSave = debounce(
|
||||||
|
(content: string) => this.performSave(content),
|
||||||
|
debounceDelay
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private async performSave(content: string): Promise<void> {
|
||||||
* 创建防抖的内容更新函数
|
if (this.isDestroyed) return;
|
||||||
*/
|
|
||||||
private createDebouncedUpdateFn(delay: number): (view: EditorView) => void {
|
try {
|
||||||
// 使用VueUse的防抖函数创建一个新函数
|
onSaveStart();
|
||||||
return useDebounceFn(async (view: EditorView) => {
|
const success = await this.documentStore.saveDocumentContent(content);
|
||||||
// 如果插件已不活跃或正在保存中,不发送
|
|
||||||
if (!this.isActive || this.isSaving) return;
|
|
||||||
|
|
||||||
this.isSaving = true;
|
if (success) {
|
||||||
const content = view.state.doc.toString();
|
this.lastContent = content;
|
||||||
|
onSaveSuccess();
|
||||||
try {
|
} else {
|
||||||
// 简单将内容传递给后端,让后端处理保存策略
|
onSaveError();
|
||||||
await DocumentService.UpdateActiveDocumentContent(content);
|
|
||||||
onSave(true);
|
|
||||||
} catch (err) {
|
|
||||||
// 静默处理错误,不在控制台打印
|
|
||||||
onSave(false);
|
|
||||||
} finally {
|
|
||||||
this.isSaving = false;
|
|
||||||
}
|
}
|
||||||
}, delay);
|
} catch (error) {
|
||||||
|
onSaveError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update(update: ViewUpdate) {
|
update(update: ViewUpdate) {
|
||||||
// 如果内容没有变化,直接返回
|
if (!update.docChanged || this.isDestroyed) return;
|
||||||
if (!update.docChanged) return;
|
|
||||||
|
|
||||||
// 调用防抖函数
|
const newContent = this.view.state.doc.toString();
|
||||||
this.contentUpdateFn(this.view);
|
if (newContent === this.lastContent) return;
|
||||||
|
|
||||||
|
this.debouncedSave(newContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
// 标记插件不再活跃
|
this.isDestroyed = true;
|
||||||
this.isActive = false;
|
this.debouncedSave.cancel();
|
||||||
|
|
||||||
// 静默发送最终内容,忽略错误
|
// 如果内容有变化,立即保存
|
||||||
const content = this.view.state.doc.toString();
|
const currentContent = this.view.state.doc.toString();
|
||||||
DocumentService.UpdateActiveDocumentContent(content).then();
|
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
16
go.mod
16
go.mod
@@ -13,6 +13,7 @@ require (
|
|||||||
github.com/knadh/koanf/v2 v2.2.1
|
github.com/knadh/koanf/v2 v2.2.1
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.9
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.9
|
||||||
golang.org/x/sys v0.33.0
|
golang.org/x/sys v0.33.0
|
||||||
|
modernc.org/sqlite v1.21.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -23,6 +24,7 @@ require (
|
|||||||
github.com/bep/debounce v1.2.1 // indirect
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.4.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/ebitengine/purego v0.8.4 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/fatih/structs v1.1.0 // indirect
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
@@ -38,6 +40,7 @@ require (
|
|||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // 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/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/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
github.com/knadh/koanf/maps v0.1.2 // indirect
|
github.com/knadh/koanf/maps v0.1.2 // indirect
|
||||||
github.com/leaanthony/go-ansi-parser v1.6.1 // 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/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // 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/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/samber/lo v1.51.0 // indirect
|
github.com/samber/lo v1.51.0 // indirect
|
||||||
github.com/sergi/go-diff v1.4.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
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
golang.org/x/crypto v0.39.0 // indirect
|
golang.org/x/crypto v0.39.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // 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/net v0.41.0 // indirect
|
||||||
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
golang.org/x/text v0.26.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/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // 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
|
||||||
)
|
)
|
||||||
|
45
go.sum
45
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
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-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 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
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 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
|
||||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
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 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
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=
|
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-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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
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/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 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
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-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/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 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
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-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-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
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.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=
|
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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=
|
||||||
|
@@ -135,7 +135,7 @@ func NewDefaultAppConfig() *AppConfig {
|
|||||||
TabSize: 4,
|
TabSize: 4,
|
||||||
TabType: TabTypeSpaces,
|
TabType: TabTypeSpaces,
|
||||||
// 保存选项
|
// 保存选项
|
||||||
AutoSaveDelay: 5000, // 5秒后自动保存
|
AutoSaveDelay: 2000, // 2秒后自动保存
|
||||||
},
|
},
|
||||||
Appearance: AppearanceConfig{
|
Appearance: AppearanceConfig{
|
||||||
Language: LangZhCN,
|
Language: LangZhCN,
|
||||||
|
@@ -4,38 +4,27 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DocumentMeta 文档元数据
|
// Document 表示一个文档(使用自增主键)
|
||||||
type DocumentMeta struct {
|
|
||||||
ID string `json:"id"` // 文档唯一标识
|
|
||||||
Title string `json:"title"` // 文档标题
|
|
||||||
LastUpdated time.Time `json:"lastUpdated"` // 最后更新时间
|
|
||||||
CreatedAt time.Time `json:"createdAt"` // 创建时间
|
|
||||||
}
|
|
||||||
|
|
||||||
// Document 表示一个文档
|
|
||||||
type Document struct {
|
type Document struct {
|
||||||
Meta DocumentMeta `json:"meta"` // 元数据
|
ID int64 `json:"id" db:"id"`
|
||||||
Content string `json:"content"` // 文档内容
|
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 文档信息(不包含内容,用于列表展示)
|
// NewDocument 创建新文档(不需要传ID,由数据库自增)
|
||||||
type DocumentInfo struct {
|
func NewDocument(title, content string) *Document {
|
||||||
ID string `json:"id"` // 文档ID
|
now := time.Now()
|
||||||
Title string `json:"title"` // 文档标题
|
return &Document{
|
||||||
LastUpdated time.Time `json:"lastUpdated"` // 最后更新时间
|
Title: title,
|
||||||
Path string `json:"path"` // 文档路径
|
Content: content,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDefaultDocument 创建默认文档
|
// NewDefaultDocument 创建默认文档
|
||||||
func NewDefaultDocument() *Document {
|
func NewDefaultDocument() *Document {
|
||||||
now := time.Now()
|
return NewDocument("default", "∞∞∞text-a\n")
|
||||||
return &Document{
|
|
||||||
Meta: DocumentMeta{
|
|
||||||
ID: "default",
|
|
||||||
Title: "默认文档",
|
|
||||||
LastUpdated: now,
|
|
||||||
CreatedAt: now,
|
|
||||||
},
|
|
||||||
Content: "∞∞∞text-a\n",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -132,7 +132,7 @@ func (cms *ConfigMigrationService[T]) checkResourceLimits() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createBackupOptimized 优化的备份创建(单次扫描删除旧备份)
|
// createBackupOptimized 优化的备份创建
|
||||||
func (cms *ConfigMigrationService[T]) createBackupOptimized() (string, error) {
|
func (cms *ConfigMigrationService[T]) createBackupOptimized() (string, error) {
|
||||||
if _, err := os.Stat(cms.configPath); os.IsNotExist(err) {
|
if _, err := os.Stat(cms.configPath); os.IsNotExist(err) {
|
||||||
return "", nil
|
return "", nil
|
||||||
@@ -155,7 +155,7 @@ func (cms *ConfigMigrationService[T]) createBackupOptimized() (string, error) {
|
|||||||
return newBackupPath, copyFile(cms.configPath, newBackupPath)
|
return newBackupPath, copyFile(cms.configPath, newBackupPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// tryQuickRecovery 快速恢复检查(避免完整的备份恢复)
|
// tryQuickRecovery 快速恢复检查
|
||||||
func (cms *ConfigMigrationService[T]) tryQuickRecovery(existingConfig *koanf.Koanf) {
|
func (cms *ConfigMigrationService[T]) tryQuickRecovery(existingConfig *koanf.Koanf) {
|
||||||
var testConfig T
|
var testConfig T
|
||||||
if existingConfig.Unmarshal("", &testConfig) != nil {
|
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)
|
return false, fmt.Errorf("config merge failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新元数据(直接操作,无需重新序列化)
|
// 更新元数据
|
||||||
mergeKoanf.Set("metadata.version", cms.targetVersion)
|
mergeKoanf.Set("metadata.version", cms.targetVersion)
|
||||||
mergeKoanf.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339))
|
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())
|
return existingConfig.Load(&BytesProvider{configBytes}, jsonparser.Parser())
|
||||||
}
|
}
|
||||||
|
|
||||||
// fastMerge 快速合并函数(优化版本)
|
// fastMerge 快速合并函数
|
||||||
func (cms *ConfigMigrationService[T]) fastMerge(src, dest map[string]interface{}) error {
|
func (cms *ConfigMigrationService[T]) fastMerge(src, dest map[string]interface{}) error {
|
||||||
return cms.fastMergeRecursive(src, dest, 0)
|
return cms.fastMergeRecursive(src, dest, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fastMergeRecursive 快速递归合并(单次遍历,最小化反射使用)
|
// fastMergeRecursive 快速递归合并
|
||||||
func (cms *ConfigMigrationService[T]) fastMergeRecursive(src, dest map[string]interface{}, depth int) error {
|
func (cms *ConfigMigrationService[T]) fastMergeRecursive(src, dest map[string]interface{}, depth int) error {
|
||||||
if depth > MaxRecursionDepth {
|
if depth > MaxRecursionDepth {
|
||||||
return fmt.Errorf("recursion depth exceeded")
|
return fmt.Errorf("recursion depth exceeded")
|
||||||
|
@@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"voidraft/internal/models"
|
"voidraft/internal/models"
|
||||||
@@ -28,6 +29,7 @@ type ConfigChangeCallback func(changeType ConfigChangeType, oldConfig, newConfig
|
|||||||
|
|
||||||
// ConfigListener 配置监听器
|
// ConfigListener 配置监听器
|
||||||
type ConfigListener struct {
|
type ConfigListener struct {
|
||||||
|
ID string // 监听器唯一ID
|
||||||
Name string // 监听器名称
|
Name string // 监听器名称
|
||||||
ChangeType ConfigChangeType // 监听的配置变更类型
|
ChangeType ConfigChangeType // 监听的配置变更类型
|
||||||
Callback ConfigChangeCallback // 回调函数(现在包含新旧配置)
|
Callback ConfigChangeCallback // 回调函数(现在包含新旧配置)
|
||||||
@@ -45,9 +47,10 @@ type ConfigListener struct {
|
|||||||
|
|
||||||
// ConfigNotificationService 配置通知服务
|
// ConfigNotificationService 配置通知服务
|
||||||
type ConfigNotificationService struct {
|
type ConfigNotificationService struct {
|
||||||
listeners sync.Map // 使用sync.Map替代普通map+锁
|
listeners map[ConfigChangeType][]*ConfigListener // 支持多监听器的map
|
||||||
logger *log.LoggerService // 日志服务
|
mu sync.RWMutex // 监听器map的读写锁
|
||||||
koanf *koanf.Koanf // koanf实例
|
logger *log.LoggerService // 日志服务
|
||||||
|
koanf *koanf.Koanf // koanf实例
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
@@ -57,20 +60,19 @@ type ConfigNotificationService struct {
|
|||||||
func NewConfigNotificationService(k *koanf.Koanf, logger *log.LoggerService) *ConfigNotificationService {
|
func NewConfigNotificationService(k *koanf.Koanf, logger *log.LoggerService) *ConfigNotificationService {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
return &ConfigNotificationService{
|
return &ConfigNotificationService{
|
||||||
logger: logger,
|
listeners: make(map[ConfigChangeType][]*ConfigListener),
|
||||||
koanf: k,
|
logger: logger,
|
||||||
ctx: ctx,
|
koanf: k,
|
||||||
cancel: cancel,
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterListener 注册配置监听器
|
// RegisterListener 注册配置监听器
|
||||||
func (cns *ConfigNotificationService) RegisterListener(listener *ConfigListener) error {
|
func (cns *ConfigNotificationService) RegisterListener(listener *ConfigListener) error {
|
||||||
// 清理已存在的监听器
|
// 生成唯一ID如果没有提供
|
||||||
if existingValue, loaded := cns.listeners.LoadAndDelete(listener.ChangeType); loaded {
|
if listener.ID == "" {
|
||||||
if existing, ok := existingValue.(interface{ cancel() }); ok {
|
listener.ID = fmt.Sprintf("%s_%d", listener.Name, time.Now().UnixNano())
|
||||||
existing.cancel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化新监听器
|
// 初始化新监听器
|
||||||
@@ -80,7 +82,11 @@ func (cns *ConfigNotificationService) RegisterListener(listener *ConfigListener)
|
|||||||
return fmt.Errorf("failed to initialize listener state: %w", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +98,7 @@ func (cns *ConfigNotificationService) initializeListenerState(listener *ConfigLi
|
|||||||
|
|
||||||
if config := listener.GetConfigFunc(cns.koanf); config != nil {
|
if config := listener.GetConfigFunc(cns.koanf); config != nil {
|
||||||
listener.mu.Lock()
|
listener.mu.Lock()
|
||||||
listener.lastConfig = deepCopyConfig(config)
|
listener.lastConfig = deepCopyConfigReflect(config)
|
||||||
listener.lastConfigHash = computeConfigHash(config)
|
listener.lastConfigHash = computeConfigHash(config)
|
||||||
listener.mu.Unlock()
|
listener.mu.Unlock()
|
||||||
}
|
}
|
||||||
@@ -100,23 +106,59 @@ func (cns *ConfigNotificationService) initializeListenerState(listener *ConfigLi
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnregisterListener 注销配置监听器
|
// UnregisterListener 注销指定ID的配置监听器
|
||||||
func (cns *ConfigNotificationService) UnregisterListener(changeType ConfigChangeType) {
|
func (cns *ConfigNotificationService) UnregisterListener(changeType ConfigChangeType, listenerID string) {
|
||||||
if value, loaded := cns.listeners.LoadAndDelete(changeType); loaded {
|
cns.mu.Lock()
|
||||||
if listener, ok := value.(*ConfigListener); ok {
|
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()
|
listener.cancel()
|
||||||
}
|
}
|
||||||
|
delete(cns.listeners, changeType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckConfigChanges 检查配置变更并通知相关监听器
|
// CheckConfigChanges 检查配置变更并通知相关监听器
|
||||||
func (cns *ConfigNotificationService) CheckConfigChanges() {
|
func (cns *ConfigNotificationService) CheckConfigChanges() {
|
||||||
cns.listeners.Range(func(key, value interface{}) bool {
|
cns.mu.RLock()
|
||||||
if listener, ok := value.(*ConfigListener); ok {
|
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)
|
cns.checkAndNotify(listener)
|
||||||
}
|
}
|
||||||
return true
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkAndNotify 检查配置变更并通知
|
// checkAndNotify 检查配置变更并通知
|
||||||
@@ -144,7 +186,7 @@ func (cns *ConfigNotificationService) checkAndNotify(listener *ConfigListener) {
|
|||||||
|
|
||||||
if hasChanges {
|
if hasChanges {
|
||||||
listener.mu.Lock()
|
listener.mu.Lock()
|
||||||
listener.lastConfig = deepCopyConfig(currentConfig)
|
listener.lastConfig = deepCopyConfigReflect(currentConfig)
|
||||||
listener.lastConfigHash = currentHash
|
listener.lastConfigHash = currentHash
|
||||||
listener.mu.Unlock()
|
listener.mu.Unlock()
|
||||||
|
|
||||||
@@ -167,7 +209,82 @@ func computeConfigHash(config *models.AppConfig) string {
|
|||||||
return fmt.Sprintf("%x", hash)
|
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 {
|
func deepCopyConfig(src *models.AppConfig) *models.AppConfig {
|
||||||
if src == nil {
|
if src == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -197,8 +314,8 @@ func (cns *ConfigNotificationService) debounceNotify(listener *ConfigListener, o
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建配置副本,避免在闭包中持有原始引用
|
// 创建配置副本,避免在闭包中持有原始引用
|
||||||
oldConfigCopy := deepCopyConfig(oldConfig)
|
oldConfigCopy := deepCopyConfigReflect(oldConfig)
|
||||||
newConfigCopy := deepCopyConfig(newConfig)
|
newConfigCopy := deepCopyConfigReflect(newConfig)
|
||||||
|
|
||||||
changeType := listener.ChangeType
|
changeType := listener.ChangeType
|
||||||
|
|
||||||
@@ -246,18 +363,33 @@ func (cns *ConfigNotificationService) executeCallback(
|
|||||||
func (cns *ConfigNotificationService) Cleanup() {
|
func (cns *ConfigNotificationService) Cleanup() {
|
||||||
cns.cancel() // 取消所有context
|
cns.cancel() // 取消所有context
|
||||||
|
|
||||||
cns.listeners.Range(func(key, value interface{}) bool {
|
cns.mu.Lock()
|
||||||
cns.listeners.Delete(key)
|
for changeType, listeners := range cns.listeners {
|
||||||
return true
|
for _, listener := range listeners {
|
||||||
})
|
listener.cancel()
|
||||||
|
}
|
||||||
|
delete(cns.listeners, changeType)
|
||||||
|
}
|
||||||
|
cns.mu.Unlock()
|
||||||
|
|
||||||
cns.wg.Wait() // 等待所有协程完成
|
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 创建热键配置监听器
|
// 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{
|
return &ConfigListener{
|
||||||
Name: "HotkeyListener",
|
Name: name,
|
||||||
ChangeType: ConfigChangeTypeHotkey,
|
ChangeType: ConfigChangeTypeHotkey,
|
||||||
Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
|
Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
|
||||||
if newConfig != nil {
|
if newConfig != nil {
|
||||||
@@ -279,9 +411,9 @@ func CreateHotkeyListener(callback func(enable bool, hotkey *models.HotkeyCombo)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateDataPathListener 创建数据路径配置监听器
|
// CreateDataPathListener 创建数据路径配置监听器
|
||||||
func CreateDataPathListener(callback func(oldPath, newPath string) error) *ConfigListener {
|
func CreateDataPathListener(name string, callback func() error) *ConfigListener {
|
||||||
return &ConfigListener{
|
return &ConfigListener{
|
||||||
Name: "DataPathListener",
|
Name: name,
|
||||||
ChangeType: ConfigChangeTypeDataPath,
|
ChangeType: ConfigChangeTypeDataPath,
|
||||||
Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
|
Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
|
||||||
var oldPath, newPath string
|
var oldPath, newPath string
|
||||||
@@ -298,7 +430,7 @@ func CreateDataPathListener(callback func(oldPath, newPath string) error) *Confi
|
|||||||
}
|
}
|
||||||
|
|
||||||
if oldPath != newPath {
|
if oldPath != newPath {
|
||||||
return callback(oldPath, newPath)
|
return callback()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@@ -313,8 +445,8 @@ func CreateDataPathListener(callback func(oldPath, newPath string) error) *Confi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceShutdown 关闭服务
|
// OnShutdown 关闭服务
|
||||||
func (cns *ConfigNotificationService) ServiceShutdown() error {
|
func (cns *ConfigNotificationService) OnShutdown() error {
|
||||||
cns.Cleanup()
|
cns.Cleanup()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -290,22 +290,22 @@ func (cs *ConfigService) SetHotkeyChangeCallback(callback func(enable bool, hotk
|
|||||||
defer cs.mu.Unlock()
|
defer cs.mu.Unlock()
|
||||||
|
|
||||||
// 创建热键监听器并注册
|
// 创建热键监听器并注册
|
||||||
hotkeyListener := CreateHotkeyListener(callback)
|
hotkeyListener := CreateHotkeyListener("DefaultHotkeyListener", callback)
|
||||||
return cs.notificationService.RegisterListener(hotkeyListener)
|
return cs.notificationService.RegisterListener(hotkeyListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDataPathChangeCallback 设置数据路径配置变更回调
|
// SetDataPathChangeCallback 设置数据路径配置变更回调
|
||||||
func (cs *ConfigService) SetDataPathChangeCallback(callback func(oldPath, newPath string) error) error {
|
func (cs *ConfigService) SetDataPathChangeCallback(callback func() error) error {
|
||||||
cs.mu.Lock()
|
cs.mu.Lock()
|
||||||
defer cs.mu.Unlock()
|
defer cs.mu.Unlock()
|
||||||
|
|
||||||
// 创建数据路径监听器并注册
|
// 创建数据路径监听器并注册
|
||||||
dataPathListener := CreateDataPathListener(callback)
|
dataPathListener := CreateDataPathListener("DefaultDataPathListener", callback)
|
||||||
return cs.notificationService.RegisterListener(dataPathListener)
|
return cs.notificationService.RegisterListener(dataPathListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceShutdown 关闭服务
|
// OnShutdown 关闭服务
|
||||||
func (cs *ConfigService) ServiceShutdown() error {
|
func (cs *ConfigService) OnShutdown() error {
|
||||||
cs.stopWatching()
|
cs.stopWatching()
|
||||||
if cs.notificationService != nil {
|
if cs.notificationService != nil {
|
||||||
cs.notificationService.Cleanup()
|
cs.notificationService.Cleanup()
|
||||||
|
@@ -2,270 +2,337 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
"voidraft/internal/models"
|
"voidraft/internal/models"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
"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 {
|
type DocumentService struct {
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
logger *log.LoggerService
|
logger *log.LoggerService
|
||||||
docStore *Store[models.Document]
|
db *sql.DB
|
||||||
|
mu sync.RWMutex
|
||||||
// 文档状态管理
|
|
||||||
mu sync.RWMutex
|
|
||||||
document *models.Document
|
|
||||||
|
|
||||||
// 自动保存管理
|
|
||||||
ctx context.Context
|
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 {
|
func NewDocumentService(configService *ConfigService, logger *log.LoggerService) *DocumentService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
return &DocumentService{
|
return &DocumentService{
|
||||||
configService: configService,
|
configService: configService,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
ctx: ctx,
|
|
||||||
cancel: cancel,
|
|
||||||
saveScheduler: make(chan struct{}, 1),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize 初始化服务
|
// OnStartup initializes the service when the application starts
|
||||||
func (ds *DocumentService) Initialize() error {
|
func (ds *DocumentService) OnStartup(ctx context.Context, _ application.ServiceOptions) error {
|
||||||
var initErr error
|
ds.ctx = ctx
|
||||||
ds.initOnce.Do(func() {
|
return ds.initDatabase()
|
||||||
initErr = ds.doInitialize()
|
|
||||||
})
|
|
||||||
return initErr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// doInitialize 执行初始化
|
// initDatabase initializes the SQLite database
|
||||||
func (ds *DocumentService) doInitialize() error {
|
func (ds *DocumentService) initDatabase() error {
|
||||||
if err := ds.initStore(); err != nil {
|
dbPath, err := ds.getDatabasePath()
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ds.loadDocument()
|
|
||||||
go ds.autoSaveWorker()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// initStore 初始化存储
|
|
||||||
func (ds *DocumentService) initStore() error {
|
|
||||||
docPath, err := ds.getDocumentPath()
|
|
||||||
if err != nil {
|
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 {
|
db, err := sql.Open("sqlite", dbPath)
|
||||||
return err
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.docStore = NewStore[models.Document](StoreOption{
|
ds.db = db
|
||||||
FilePath: docPath,
|
|
||||||
AutoSave: false,
|
// Apply optimization settings
|
||||||
Logger: ds.logger,
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDocumentPath 获取文档路径
|
// getDatabasePath gets the database file path
|
||||||
func (ds *DocumentService) getDocumentPath() (string, error) {
|
func (ds *DocumentService) getDatabasePath() (string, error) {
|
||||||
config, err := ds.configService.GetConfig()
|
config, err := ds.configService.GetConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return filepath.Join(config.General.DataPath, "docs", "default.json"), nil
|
return filepath.Join(config.General.DataPath, dbName), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadDocument 加载文档
|
// createIndexes creates database indexes
|
||||||
func (ds *DocumentService) loadDocument() {
|
func (ds *DocumentService) createIndexes() error {
|
||||||
ds.mu.Lock()
|
indexes := []string{
|
||||||
defer ds.mu.Unlock()
|
sqlCreateIndexUpdatedAt,
|
||||||
|
sqlCreateIndexTitle,
|
||||||
doc := ds.docStore.Get()
|
|
||||||
if doc.Meta.ID == "" {
|
|
||||||
ds.document = models.NewDefaultDocument()
|
|
||||||
ds.docStore.Set(*ds.document)
|
|
||||||
} else {
|
|
||||||
ds.document = &doc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.lastSaveTime.Store(time.Now().Unix())
|
for _, index := range indexes {
|
||||||
}
|
if _, err := ds.db.Exec(index); err != nil {
|
||||||
|
|
||||||
// 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 {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// 重新初始化存储
|
// ensureDefaultDocument ensures a default document exists
|
||||||
if err := ds.initStore(); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新加载
|
// If no documents exist, create default document
|
||||||
ds.loadDocument()
|
if count == 0 {
|
||||||
|
defaultDoc := models.NewDefaultDocument()
|
||||||
|
_, err := ds.CreateDocument(defaultDoc.Title)
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceShutdown 关闭服务
|
// GetDocumentByID gets a document by ID
|
||||||
func (ds *DocumentService) ServiceShutdown() error {
|
func (ds *DocumentService) GetDocumentByID(id int64) (*models.Document, error) {
|
||||||
ds.cancel() // 停止自动保存工作协程
|
ds.mu.RLock()
|
||||||
|
defer ds.mu.RUnlock()
|
||||||
|
|
||||||
// 最后保存
|
var doc models.Document
|
||||||
if ds.isDirty.Load() {
|
row := ds.db.QueryRow(sqlGetDocumentByID, id)
|
||||||
return ds.saveDocument()
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnDataPathChanged 处理数据路径变更
|
// UpdateDocumentTitle updates the title of a document
|
||||||
func (ds *DocumentService) OnDataPathChanged(oldPath, newPath string) error {
|
func (ds *DocumentService) UpdateDocumentTitle(id int64, title string) error {
|
||||||
return ds.ReloadDocument()
|
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()
|
||||||
}
|
}
|
||||||
|
@@ -257,8 +257,8 @@ func (es *ExtensionService) ResetAllExtensionsToDefault() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceShutdown 关闭服务
|
// OnShutdown 关闭服务
|
||||||
func (es *ExtensionService) ServiceShutdown() error {
|
func (es *ExtensionService) OnShutdown() error {
|
||||||
es.cancel()
|
es.cancel()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -259,7 +259,7 @@ func (hs *HotkeyService) IsRegistered() bool {
|
|||||||
return hs.isRegistered.Load()
|
return hs.isRegistered.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceShutdown 关闭服务
|
// OnShutdown 关闭服务
|
||||||
func (hs *HotkeyService) ServiceShutdown() error {
|
func (hs *HotkeyService) ServiceShutdown() error {
|
||||||
hs.cancel()
|
hs.cancel()
|
||||||
hs.wg.Wait()
|
hs.wg.Wait()
|
||||||
|
@@ -283,8 +283,8 @@ func (hs *HotkeyService) IsRegistered() bool {
|
|||||||
return hs.isRegistered.Load()
|
return hs.isRegistered.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceShutdown 关闭热键服务
|
// OnShutdown 关闭热键服务
|
||||||
func (hs *HotkeyService) ServiceShutdown() error {
|
func (hs *HotkeyService) OnShutdown() error {
|
||||||
return hs.UnregisterHotkey()
|
return hs.UnregisterHotkey()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -384,8 +384,8 @@ func (hs *HotkeyService) IsRegistered() bool {
|
|||||||
return hs.isRegistered.Load()
|
return hs.isRegistered.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceShutdown 关闭服务
|
// OnShutdown 关闭服务
|
||||||
func (hs *HotkeyService) ServiceShutdown() error {
|
func (hs *HotkeyService) OnShutdown() error {
|
||||||
hs.cancel()
|
hs.cancel()
|
||||||
hs.wg.Wait()
|
hs.wg.Wait()
|
||||||
C.closeX11Display()
|
C.closeX11Display()
|
||||||
|
@@ -185,8 +185,8 @@ func (kbs *KeyBindingService) GetAllKeyBindings() ([]models.KeyBinding, error) {
|
|||||||
return config.KeyBindings, nil
|
return config.KeyBindings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceShutdown 关闭服务
|
// OnShutdown 关闭服务
|
||||||
func (kbs *KeyBindingService) ServiceShutdown() error {
|
func (kbs *KeyBindingService) OnShutdown() error {
|
||||||
kbs.cancel()
|
kbs.cancel()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -417,8 +417,8 @@ func (ms *MigrationService) CancelMigration() error {
|
|||||||
return fmt.Errorf("no active migration to cancel")
|
return fmt.Errorf("no active migration to cancel")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceShutdown 服务关闭
|
// OnShutdown 服务关闭
|
||||||
func (ms *MigrationService) ServiceShutdown() error {
|
func (ms *MigrationService) OnShutdown() error {
|
||||||
ms.CancelMigration()
|
ms.CancelMigration()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -74,19 +74,13 @@ func NewServiceManager() *ServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 设置数据路径变更监听,处理配置重置和路径变更
|
// 设置数据路径变更监听,处理配置重置和路径变更
|
||||||
err = configService.SetDataPathChangeCallback(func(oldPath, newPath string) error {
|
err = configService.SetDataPathChangeCallback(func() error {
|
||||||
return documentService.OnDataPathChanged(oldPath, newPath)
|
return documentService.OnDataPathChanged()
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化文档服务
|
|
||||||
err = documentService.Initialize()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ServiceManager{
|
return &ServiceManager{
|
||||||
configService: configService,
|
configService: configService,
|
||||||
documentService: documentService,
|
documentService: documentService,
|
||||||
|
Reference in New Issue
Block a user