diff --git a/README.md b/README.md index 6f0d7c2..a99607e 100644 --- a/README.md +++ b/README.md @@ -121,8 +121,8 @@ Voidraft/ ### Planned Features - ✅ Custom themes - Customize editor themes - ✅ Multi-window support - Support editing multiple documents simultaneously +- ✅ Data synchronization - Cloud backup for documents - [ ] Enhanced clipboard - Monitor and manage clipboard history -- [ ] Data synchronization - Cloud backup for configurations and documents - [ ] Extension system - Support for custom plugins ## Acknowledgments diff --git a/README_ZH.md b/README_ZH.md index 66d7203..106b105 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -122,8 +122,8 @@ Voidraft/ ### 计划添加的功能 - ✅ 自定义主题 - 自定义编辑器主题 - ✅ 多窗口支持 - 支持同时编辑多个文档 +- ✅ 数据同步 - 文档云端备份 - [ ] 剪切板增强 - 监听和管理剪切板历史 -- [ ] 数据同步 - 配置和文档云端备份 - [ ] 扩展系统 - 支持自定义插件 diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/index.ts b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/index.ts deleted file mode 100644 index 4e17f5e..0000000 --- a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -import * as Service from "./service.js"; -export { - Service -}; - -export * from "./models.js"; diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/models.ts b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/models.ts deleted file mode 100644 index 8ac9575..0000000 --- a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/models.ts +++ /dev/null @@ -1,51 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import {Create as $Create} from "@wailsio/runtime"; - -export class Config { - /** - * DBSource is the database URI to use. - * The string ":memory:" can be used to create an in-memory database. - * The sqlite driver can be configured through query parameters. - * For more details see https://pkg.go.dev/modernc.org/sqlite#Driver.Open - */ - "DBSource": string; - - /** Creates a new Config instance. */ - constructor($$source: Partial = {}) { - if (!("DBSource" in $$source)) { - this["DBSource"] = ""; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new Config instance from a string or object. - */ - static createFrom($$source: any = {}): Config { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new Config($$parsedSource as Partial); - } -} - -/** - * Row holds a single row in the result of a query. - * It is a key-value map where keys are column names. - */ -export type Row = { [_: string]: any }; - -/** - * Rows holds the result of a query - * as an array of key-value maps where keys are column names. - */ -export type Rows = Row[]; - -/** - * Stmt wraps a prepared sql statement pointer. - * It provides the same methods as the [sql.Stmt] type. - */ -export type Stmt = string; diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/service.ts b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/service.ts deleted file mode 100644 index 2f820f8..0000000 --- a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/service.ts +++ /dev/null @@ -1,223 +0,0 @@ -// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL -// This file is automatically generated. DO NOT EDIT - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import {Call as $Call, Create as $Create} from "@wailsio/runtime"; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as application$0 from "../../application/models.js"; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Unused imports -import * as $models from "./models.js"; - -export { - ExecContext as Execute, - QueryContext as Query -}; - -import { Stmt } from "./stmt.js"; - -/** - * Prepare creates a prepared statement for later queries or executions. - * Multiple queries or executions may be run concurrently from the returned statement. - * - * The caller must call the statement's Close method when it is no longer needed. - * Statements are closed automatically - * when the connection they are associated with is closed. - * - * Prepare supports early cancellation. - */ -export function Prepare(query: string): Promise & { cancel(): void } { - const promise = PrepareContext(query); - const wrapper: any = (promise.then(function (id) { - return id == null ? null : new Stmt( - ClosePrepared.bind(null, id), - ExecPrepared.bind(null, id), - QueryPrepared.bind(null, id)); - })); - wrapper.cancel = promise.cancel; - return wrapper; -} - -/** - * Close closes the current database connection if one is open, otherwise has no effect. - * Additionally, Close closes all open prepared statements associated to the connection. - * - * Even when a non-nil error is returned, - * the database service is left in a consistent state, - * ready for a call to [Service.Open]. - */ -export function Close(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1888105376) as any; - return $resultPromise; -} - -/** - * ClosePrepared closes a prepared statement - * obtained with [Service.Prepare] or [Service.PrepareContext]. - * ClosePrepared is idempotent: - * it has no effect on prepared statements that are already closed. - */ -function ClosePrepared(stmt: $models.Stmt | null): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2526200629, stmt) as any; - return $resultPromise; -} - -/** - * Configure changes the database service configuration. - * The connection state at call time is preserved. - * Consumers will need to call [Service.Open] manually after Configure - * in order to reconnect with the new configuration. - * - * See [NewWithConfig] for details on configuration. - */ -export function Configure(config: $models.Config | null): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1939578712, config) as any; - return $resultPromise; -} - -/** - * ExecContext executes a query without returning any rows. - * It supports early cancellation. - */ -function ExecContext(query: string, ...args: any[]): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(674944556, query, args) as any; - return $resultPromise; -} - -/** - * ExecPrepared executes a prepared statement - * obtained with [Service.Prepare] or [Service.PrepareContext] - * without returning any rows. - * It supports early cancellation. - */ -function ExecPrepared(stmt: $models.Stmt | null, ...args: any[]): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2086877656, stmt, args) as any; - return $resultPromise; -} - -/** - * Execute executes a query without returning any rows. - */ -export function Execute(query: string, ...args: any[]): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3811930203, query, args) as any; - return $resultPromise; -} - -/** - * Open validates the current configuration, - * closes the current connection if one is present, - * then opens and validates a new connection. - * - * Even when a non-nil error is returned, - * the database service is left in a consistent state, - * ready for a new call to Open. - */ -export function Open(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(2012175612) as any; - return $resultPromise; -} - -/** - * Prepare creates a prepared statement for later queries or executions. - * Multiple queries or executions may be run concurrently from the returned statement. - * - * The caller should call the statement's Close method when it is no longer needed. - * Statements are closed automatically - * when the connection they are associated with is closed. - */ -export function Prepare(query: string): Promise<$models.Stmt | null> & { cancel(): void } { - let $resultPromise = $Call.ByID(1801965143, query) as any; - return $resultPromise; -} - -/** - * PrepareContext creates a prepared statement for later queries or executions. - * Multiple queries or executions may be run concurrently from the returned statement. - * - * The caller must call the statement's Close method when it is no longer needed. - * Statements are closed automatically - * when the connection they are associated with is closed. - * - * PrepareContext supports early cancellation. - */ -function PrepareContext(query: string): Promise<$models.Stmt | null> & { cancel(): void } { - let $resultPromise = $Call.ByID(570941694, query) as any; - return $resultPromise; -} - -/** - * Query executes a query and returns a slice of key-value records, - * one per row, with column names as keys. - */ -export function Query(query: string, ...args: any[]): Promise<$models.Rows> & { cancel(): void } { - let $resultPromise = $Call.ByID(860757720, query, args) as any; - let $typingPromise = $resultPromise.then(($result: any) => { - return $$createType1($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; -} - -/** - * QueryContext executes a query and returns a slice of key-value records, - * one per row, with column names as keys. - * It supports early cancellation, returning the slice of results fetched so far. - */ -function QueryContext(query: string, ...args: any[]): Promise<$models.Rows> & { cancel(): void } { - let $resultPromise = $Call.ByID(4115542347, query, args) as any; - let $typingPromise = $resultPromise.then(($result: any) => { - return $$createType1($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; -} - -/** - * QueryPrepared executes a prepared statement - * obtained with [Service.Prepare] or [Service.PrepareContext] - * and returns a slice of key-value records, one per row, with column names as keys. - * It supports early cancellation, returning the slice of results fetched so far. - */ -function QueryPrepared(stmt: $models.Stmt | null, ...args: any[]): Promise<$models.Rows> & { cancel(): void } { - let $resultPromise = $Call.ByID(3885083725, stmt, args) as any; - let $typingPromise = $resultPromise.then(($result: any) => { - return $$createType1($result); - }) as any; - $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); - return $typingPromise; -} - -/** - * ServiceName returns the name of the plugin. - * You should use the go module format e.g. github.com/myuser/myplugin - */ -export function ServiceName(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1637123084) as any; - return $resultPromise; -} - -/** - * ServiceShutdown closes the database connection. - * It returns a non-nil error in case of failures. - */ -export function ServiceShutdown(): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(3650435925) as any; - return $resultPromise; -} - -/** - * ServiceStartup opens the database connection. - * It returns a non-nil error in case of failures. - */ -export function ServiceStartup(options: application$0.ServiceOptions): Promise & { cancel(): void } { - let $resultPromise = $Call.ByID(1113159936, options) as any; - return $resultPromise; -} - -// Private type creation functions -const $$createType0 = $Create.Map($Create.Any, $Create.Any); -const $$createType1 = $Create.Array($$createType0); diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/stmt.js b/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/stmt.js deleted file mode 100644 index 948b0c3..0000000 --- a/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/sqlite/stmt.js +++ /dev/null @@ -1,79 +0,0 @@ -//@ts-check - -//@ts-ignore: Unused imports -import * as $models from "./models.js"; - -const execSymbol = Symbol("exec"), - querySymbol = Symbol("query"), - closeSymbol = Symbol("close"); - -/** - * Stmt represents a prepared statement for later queries or executions. - * Multiple queries or executions may be run concurrently on the same statement. - * - * The caller must call the statement's Close method when it is no longer needed. - * Statements are closed automatically - * when the connection they are associated with is closed. - */ -export class Stmt { - /** - * Constructs a new prepared statement instance. - * @param {(...args: any[]) => Promise} close - * @param {(...args: any[]) => Promise & { cancel(): void }} exec - * @param {(...args: any[]) => Promise<$models.Rows> & { cancel(): void }} query - */ - constructor(close, exec, query) { - /** - * @member - * @private - * @type {typeof close} - */ - this[closeSymbol] = close; - - /** - * @member - * @private - * @type {typeof exec} - */ - this[execSymbol] = exec; - - /** - * @member - * @private - * @type {typeof query} - */ - this[querySymbol] = query; - } - - /** - * Closes the prepared statement. - * It has no effect when the statement is already closed. - * @returns {Promise} - */ - Close() { - return this[closeSymbol](); - } - - /** - * Executes the prepared statement without returning any rows. - * It supports early cancellation. - * - * @param {any[]} args - * @returns {Promise & { cancel(): void }} - */ - Exec(...args) { - return this[execSymbol](...args); - } - - /** - * Executes the prepared statement - * and returns a slice of key-value records, one per row, with column names as keys. - * It supports early cancellation, returning the array of results fetched so far. - * - * @param {any[]} args - * @returns {Promise<$models.Rows> & { cancel(): void }} - */ - Query(...args) { - return this[querySymbol](...args); - } -} diff --git a/frontend/bindings/voidraft/internal/models/models.ts b/frontend/bindings/voidraft/internal/models/models.ts index 93cf158..9cd16d8 100644 --- a/frontend/bindings/voidraft/internal/models/models.ts +++ b/frontend/bindings/voidraft/internal/models/models.ts @@ -34,9 +34,9 @@ export class AppConfig { "updates": UpdatesConfig; /** - * Git同步设置 + * Git备份设置 */ - "sync": GitSyncConfig; + "backup": GitBackupConfig; /** * 配置元数据 @@ -57,8 +57,8 @@ export class AppConfig { if (!("updates" in $$source)) { this["updates"] = (new UpdatesConfig()); } - if (!("sync" in $$source)) { - this["sync"] = (new GitSyncConfig()); + if (!("backup" in $$source)) { + this["backup"] = (new GitBackupConfig()); } if (!("metadata" in $$source)) { this["metadata"] = (new ConfigMetadata()); @@ -90,8 +90,8 @@ export class AppConfig { if ("updates" in $$parsedSource) { $$parsedSource["updates"] = $$createField3_0($$parsedSource["updates"]); } - if ("sync" in $$parsedSource) { - $$parsedSource["sync"] = $$createField4_0($$parsedSource["sync"]); + if ("backup" in $$parsedSource) { + $$parsedSource["backup"] = $$createField4_0($$parsedSource["backup"]); } if ("metadata" in $$parsedSource) { $$parsedSource["metadata"] = $$createField5_0($$parsedSource["metadata"]); @@ -148,6 +148,8 @@ export class AppearanceConfig { } /** + * Git备份相关类型定义 + * * AuthMethod 定义Git认证方式 */ export enum AuthMethod { @@ -157,18 +159,10 @@ export enum AuthMethod { $zero = "", /** - * 个人访问令牌 + * 认证方式 */ Token = "token", - - /** - * SSH密钥 - */ SSHKey = "ssh_key", - - /** - * 用户名密码 - */ UserPass = "user_pass", }; @@ -577,12 +571,11 @@ export class GeneralConfig { } /** - * GitSyncConfig 保存Git同步的配置信息 + * GitBackupConfig Git备份配置 */ -export class GitSyncConfig { +export class GitBackupConfig { "enabled": boolean; "repo_url": string; - "branch": string; "auth_method": AuthMethod; "username"?: string; "password"?: string; @@ -591,73 +584,38 @@ export class GitSyncConfig { "ssh_key_passphrase"?: string; /** - * 同步间隔(分钟) + * 分钟 */ - "sync_interval": number; - "last_sync_time": time$0.Time; + "backup_interval": number; + "auto_backup": boolean; - /** - * 是否启用自动同步 - */ - "auto_sync": boolean; - "local_repo_path": string; - - /** - * 合并冲突策略 - */ - "sync_strategy": SyncStrategy; - - /** - * 要同步的文件列表,默认为数据库文件 - */ - "files_to_sync": string[]; - - /** Creates a new GitSyncConfig instance. */ - constructor($$source: Partial = {}) { + /** Creates a new GitBackupConfig instance. */ + constructor($$source: Partial = {}) { if (!("enabled" in $$source)) { this["enabled"] = false; } if (!("repo_url" in $$source)) { this["repo_url"] = ""; } - if (!("branch" in $$source)) { - this["branch"] = ""; - } if (!("auth_method" in $$source)) { this["auth_method"] = ("" as AuthMethod); } - if (!("sync_interval" in $$source)) { - this["sync_interval"] = 0; + if (!("backup_interval" in $$source)) { + this["backup_interval"] = 0; } - if (!("last_sync_time" in $$source)) { - this["last_sync_time"] = null; - } - if (!("auto_sync" in $$source)) { - this["auto_sync"] = false; - } - if (!("local_repo_path" in $$source)) { - this["local_repo_path"] = ""; - } - if (!("sync_strategy" in $$source)) { - this["sync_strategy"] = ("" as SyncStrategy); - } - if (!("files_to_sync" in $$source)) { - this["files_to_sync"] = []; + if (!("auto_backup" in $$source)) { + this["auto_backup"] = false; } Object.assign(this, $$source); } /** - * Creates a new GitSyncConfig instance from a string or object. + * Creates a new GitBackupConfig instance from a string or object. */ - static createFrom($$source: any = {}): GitSyncConfig { - const $$createField14_0 = $$createType11; + static createFrom($$source: any = {}): GitBackupConfig { let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - if ("files_to_sync" in $$parsedSource) { - $$parsedSource["files_to_sync"] = $$createField14_0($$parsedSource["files_to_sync"]); - } - return new GitSyncConfig($$parsedSource as Partial); + return new GitBackupConfig($$parsedSource as Partial); } } @@ -1168,26 +1126,6 @@ export enum LanguageType { LangEnUS = "en-US", }; -/** - * SyncStrategy 定义同步策略 - */ -export enum SyncStrategy { - /** - * The Go zero value for the underlying type of the enum. - */ - $zero = "", - - /** - * LocalFirst 本地优先:如有冲突,保留本地修改 - */ - LocalFirst = "local_first", - - /** - * RemoteFirst 远程优先:如有冲突,采用远程版本 - */ - RemoteFirst = "remote_first", -}; - /** * SystemThemeType 系统主题类型定义 */ @@ -1539,8 +1477,8 @@ export class UpdatesConfig { * Creates a new UpdatesConfig instance from a string or object. */ static createFrom($$source: any = {}): UpdatesConfig { - const $$createField6_0 = $$createType12; - const $$createField7_0 = $$createType13; + const $$createField6_0 = $$createType11; + const $$createField7_0 = $$createType12; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("github" in $$parsedSource) { $$parsedSource["github"] = $$createField6_0($$parsedSource["github"]); @@ -1557,7 +1495,7 @@ const $$createType0 = GeneralConfig.createFrom; const $$createType1 = EditingConfig.createFrom; const $$createType2 = AppearanceConfig.createFrom; const $$createType3 = UpdatesConfig.createFrom; -const $$createType4 = GitSyncConfig.createFrom; +const $$createType4 = GitBackupConfig.createFrom; const $$createType5 = ConfigMetadata.createFrom; const $$createType6 = CustomThemeConfig.createFrom; const $$createType7 = ThemeColorConfig.createFrom; @@ -1569,6 +1507,5 @@ var $$createType8 = (function $$initCreateType8(...args): any { }); const $$createType9 = $Create.Map($Create.Any, $Create.Any); const $$createType10 = HotkeyCombo.createFrom; -const $$createType11 = $Create.Array($Create.Any); -const $$createType12 = GithubConfig.createFrom; -const $$createType13 = GiteaConfig.createFrom; +const $$createType11 = GithubConfig.createFrom; +const $$createType12 = GiteaConfig.createFrom; diff --git a/frontend/bindings/voidraft/internal/services/backupservice.ts b/frontend/bindings/voidraft/internal/services/backupservice.ts new file mode 100644 index 0000000..582eda0 --- /dev/null +++ b/frontend/bindings/voidraft/internal/services/backupservice.ts @@ -0,0 +1,71 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * BackupService 提供基于Git的备份功能 + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import {Call as $Call, Create as $Create} from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as models$0 from "../models/models.js"; + +/** + * HandleConfigChange 处理备份配置变更 + */ +export function HandleConfigChange(config: models$0.GitBackupConfig | null): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(395287784, config) as any; + return $resultPromise; +} + +/** + * Initialize 初始化备份服务 + */ +export function Initialize(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(1052437974) as any; + return $resultPromise; +} + +/** + * PushToRemote 推送本地更改到远程仓库 + */ +export function PushToRemote(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(262644139) as any; + return $resultPromise; +} + +/** + * Reinitialize 重新初始化备份服务,用于响应配置变更 + */ +export function Reinitialize(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(301562543) as any; + return $resultPromise; +} + +/** + * ServiceShutdown 服务关闭时的清理工作 + */ +export function ServiceShutdown(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(422131801) as any; + return $resultPromise; +} + +/** + * StartAutoBackup 启动自动备份定时器 + */ +export function StartAutoBackup(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(3035755449) as any; + return $resultPromise; +} + +/** + * StopAutoBackup 停止自动备份 + */ +export function StopAutoBackup(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(2641894021) as any; + return $resultPromise; +} diff --git a/frontend/bindings/voidraft/internal/services/configservice.ts b/frontend/bindings/voidraft/internal/services/configservice.ts index 1067bd3..050051f 100644 --- a/frontend/bindings/voidraft/internal/services/configservice.ts +++ b/frontend/bindings/voidraft/internal/services/configservice.ts @@ -58,6 +58,14 @@ export function Set(key: string, value: any): Promise & { cancel(): void } return $resultPromise; } +/** + * SetBackupConfigChangeCallback 设置备份配置变更回调 + */ +export function SetBackupConfigChangeCallback(callback: any): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(3264871659, callback) as any; + return $resultPromise; +} + /** * SetDataPathChangeCallback 设置数据路径配置变更回调 */ diff --git a/frontend/bindings/voidraft/internal/services/dialogservice.ts b/frontend/bindings/voidraft/internal/services/dialogservice.ts index 3456d55..2589217 100644 --- a/frontend/bindings/voidraft/internal/services/dialogservice.ts +++ b/frontend/bindings/voidraft/internal/services/dialogservice.ts @@ -22,6 +22,14 @@ export function SelectDirectory(): Promise & { cancel(): void } { return $resultPromise; } +/** + * SelectFile 打开文件选择对话框 + */ +export function SelectFile(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(37302920) as any; + return $resultPromise; +} + /** * SetWindow 设置绑定的窗口 */ diff --git a/frontend/bindings/voidraft/internal/services/index.ts b/frontend/bindings/voidraft/internal/services/index.ts index 6647c72..2f85a3d 100644 --- a/frontend/bindings/voidraft/internal/services/index.ts +++ b/frontend/bindings/voidraft/internal/services/index.ts @@ -1,6 +1,7 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT +import * as BackupService from "./backupservice.js"; import * as ConfigService from "./configservice.js"; import * as DatabaseService from "./databaseservice.js"; import * as DialogService from "./dialogservice.js"; @@ -16,6 +17,7 @@ import * as TranslationService from "./translationservice.js"; import * as TrayService from "./trayservice.js"; import * as WindowService from "./windowservice.js"; export { + BackupService, ConfigService, DatabaseService, DialogService, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 040e827..60ce0a3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -37,7 +37,7 @@ "@codemirror/lint": "^6.8.5", "@codemirror/search": "^6.5.11", "@codemirror/state": "^6.5.2", - "@codemirror/view": "^6.38.0", + "@codemirror/view": "^6.38.1", "@lezer/highlight": "^1.2.1", "@lezer/lr": "^1.4.2", "codemirror": "^6.0.2", @@ -53,25 +53,24 @@ "remarkable": "^2.0.1", "sass": "^1.89.2", "vue": "^3.5.17", - "vue-i18n": "^11.1.9", + "vue-i18n": "^11.1.10", "vue-pick-colors": "^1.8.0", "vue-router": "^4.5.1" }, "devDependencies": { - "@eslint/js": "^9.30.1", + "@eslint/js": "^9.31.0", "@lezer/generator": "^1.8.0", - "@types/lodash": "^4.17.20", - "@types/node": "^24.0.12", + "@types/node": "^24.0.14", "@types/remarkable": "^2.0.8", "@vitejs/plugin-vue": "^6.0.0", "@wailsio/runtime": "latest", - "eslint": "^9.30.1", + "eslint": "^9.31.0", "eslint-plugin-vue": "^10.3.0", "globals": "^16.3.0", "typescript": "^5.8.3", - "typescript-eslint": "^8.36.0", + "typescript-eslint": "^8.37.0", "unplugin-vue-components": "^28.8.0", - "vite": "^7.0.3", + "vite": "^7.0.4", "vue-eslint-parser": "^10.2.0", "vue-tsc": "^3.0.1" } @@ -507,9 +506,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.38.0", - "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.38.0.tgz", - "integrity": "sha512-yvSchUwHOdupXkd7xJ0ob36jdsSR/I+/C+VbY0ffBiL5NiSTEBDfB1ZGWbbIlDd5xgdUkody+lukAdOxYrOBeg==", + "version": "6.38.1", + "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.38.1.tgz", + "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.5.0", @@ -1061,9 +1060,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.30.1", - "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.30.1.tgz", - "integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==", + "version": "9.31.0", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, "license": "MIT", "engines": { @@ -1164,13 +1163,13 @@ } }, "node_modules/@intlify/core-base": { - "version": "11.1.9", - "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.1.9.tgz", - "integrity": "sha512-Lrdi4wp3XnGhWmB/mMD/XtfGUw1Jt+PGpZI/M63X1ZqhTDjNHRVCs/i8vv8U1cwaj1A9fb0bkCQHLSL0SK+pIQ==", + "version": "11.1.10", + "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.1.10.tgz", + "integrity": "sha512-JhRb40hD93Vk0BgMgDc/xMIFtdXPHoytzeK6VafBNOj6bb6oUZrGamXkBKecMsmGvDQQaPRGG2zpa25VCw8pyw==", "license": "MIT", "dependencies": { - "@intlify/message-compiler": "11.1.9", - "@intlify/shared": "11.1.9" + "@intlify/message-compiler": "11.1.10", + "@intlify/shared": "11.1.10" }, "engines": { "node": ">= 16" @@ -1180,12 +1179,12 @@ } }, "node_modules/@intlify/message-compiler": { - "version": "11.1.9", - "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.1.9.tgz", - "integrity": "sha512-84SNs3Ikjg0rD1bOuchzb3iK1vR2/8nxrkyccIl5DjFTeMzE/Fxv6X+A7RN5ZXjEWelc1p5D4kHA6HEOhlKL5Q==", + "version": "11.1.10", + "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.1.10.tgz", + "integrity": "sha512-TABl3c8tSLWbcD+jkQTyBhrnW251dzqW39MPgEUCsd69Ua3ceoimsbIzvkcPzzZvt1QDxNkenMht+5//V3JvLQ==", "license": "MIT", "dependencies": { - "@intlify/shared": "11.1.9", + "@intlify/shared": "11.1.10", "source-map-js": "^1.0.2" }, "engines": { @@ -1196,9 +1195,9 @@ } }, "node_modules/@intlify/shared": { - "version": "11.1.9", - "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.1.9.tgz", - "integrity": "sha512-H/83xgU1l8ox+qG305p6ucmoy93qyjIPnvxGWRA7YdOoHe1tIiW9IlEu4lTdsOR7cfP1ecrwyflQSqXdXBacXA==", + "version": "11.1.10", + "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.1.10.tgz", + "integrity": "sha512-6ZW/f3Zzjxfa1Wh0tYQI5pLKUtU+SY7l70pEG+0yd0zjcsYcK0EBt6Fz30Dy0tZhEqemziQQy2aNU3GJzyrMUA==", "license": "MIT", "engines": { "node": ">= 16" @@ -2134,17 +2133,10 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { - "version": "24.0.12", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.0.12.tgz", - "integrity": "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==", + "version": "24.0.14", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.0.14.tgz", + "integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==", "dev": true, "license": "MIT", "dependencies": { @@ -2159,17 +2151,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.36.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz", - "integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==", + "version": "8.37.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz", + "integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.36.0", - "@typescript-eslint/type-utils": "8.36.0", - "@typescript-eslint/utils": "8.36.0", - "@typescript-eslint/visitor-keys": "8.36.0", + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/type-utils": "8.37.0", + "@typescript-eslint/utils": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2183,7 +2175,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.36.0", + "@typescript-eslint/parser": "^8.37.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -2199,16 +2191,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.36.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.36.0.tgz", - "integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==", + "version": "8.37.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.37.0.tgz", + "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.36.0", - "@typescript-eslint/types": "8.36.0", - "@typescript-eslint/typescript-estree": "8.36.0", - "@typescript-eslint/visitor-keys": "8.36.0", + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4" }, "engines": { @@ -2224,14 +2216,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.36.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.36.0.tgz", - "integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==", + "version": "8.37.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.37.0.tgz", + "integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.36.0", - "@typescript-eslint/types": "^8.36.0", + "@typescript-eslint/tsconfig-utils": "^8.37.0", + "@typescript-eslint/types": "^8.37.0", "debug": "^4.3.4" }, "engines": { @@ -2246,14 +2238,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.36.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz", - "integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==", + "version": "8.37.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz", + "integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.36.0", - "@typescript-eslint/visitor-keys": "8.36.0" + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2264,9 +2256,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.36.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz", - "integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==", + "version": "8.37.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz", + "integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==", "dev": true, "license": "MIT", "engines": { @@ -2281,14 +2273,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.36.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz", - "integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==", + "version": "8.37.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz", + "integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.36.0", - "@typescript-eslint/utils": "8.36.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/utils": "8.37.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2305,9 +2298,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.36.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.36.0.tgz", - "integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==", + "version": "8.37.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.37.0.tgz", + "integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==", "dev": true, "license": "MIT", "engines": { @@ -2319,16 +2312,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.36.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz", - "integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==", + "version": "8.37.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz", + "integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.36.0", - "@typescript-eslint/tsconfig-utils": "8.36.0", - "@typescript-eslint/types": "8.36.0", - "@typescript-eslint/visitor-keys": "8.36.0", + "@typescript-eslint/project-service": "8.37.0", + "@typescript-eslint/tsconfig-utils": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2374,16 +2367,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.36.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.36.0.tgz", - "integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==", + "version": "8.37.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.37.0.tgz", + "integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.36.0", - "@typescript-eslint/types": "8.36.0", - "@typescript-eslint/typescript-estree": "8.36.0" + "@typescript-eslint/scope-manager": "8.37.0", + "@typescript-eslint/types": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2398,13 +2391,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.36.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz", - "integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==", + "version": "8.37.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz", + "integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/types": "8.37.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -3199,9 +3192,9 @@ } }, "node_modules/eslint": { - "version": "9.30.1", - "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.30.1.tgz", - "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", + "version": "9.31.0", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3209,9 +3202,9 @@ "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.30.1", + "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -3317,6 +3310,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", @@ -4827,15 +4833,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.36.0", - "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.36.0.tgz", - "integrity": "sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==", + "version": "8.37.0", + "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.37.0.tgz", + "integrity": "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.36.0", - "@typescript-eslint/parser": "8.36.0", - "@typescript-eslint/utils": "8.36.0" + "@typescript-eslint/eslint-plugin": "8.37.0", + "@typescript-eslint/parser": "8.37.0", + "@typescript-eslint/typescript-estree": "8.37.0", + "@typescript-eslint/utils": "8.37.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5135,9 +5142,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.0.3", - "resolved": "https://registry.npmmirror.com/vite/-/vite-7.0.3.tgz", - "integrity": "sha512-y2L5oJZF7bj4c0jgGYgBNSdIu+5HF+m68rn2cQXFbGoShdhV1phX9rbnxy9YXj82aS8MMsCLAAFkRxZeWdldrQ==", + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/vite/-/vite-7.0.4.tgz", + "integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==", "dev": true, "license": "MIT", "dependencies": { @@ -5290,13 +5297,13 @@ } }, "node_modules/vue-i18n": { - "version": "11.1.9", - "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.1.9.tgz", - "integrity": "sha512-N9ZTsXdRmX38AwS9F6Rh93RtPkvZTkSy/zNv63FTIwZCUbLwwrpqlKz9YQuzFLdlvRdZTnWAUE5jMxr8exdl7g==", + "version": "11.1.10", + "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.1.10.tgz", + "integrity": "sha512-C+IwnSg8QDSOAox0gdFYP5tsKLx5jNWxiawNoiNB/Tw4CReXmM1VJMXbduhbrEzAFLhreqzfDocuSVjGbxQrag==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.1.9", - "@intlify/shared": "11.1.9", + "@intlify/core-base": "11.1.10", + "@intlify/shared": "11.1.10", "@vue/devtools-api": "^6.5.0" }, "engines": { diff --git a/frontend/package.json b/frontend/package.json index d37068d..5c0e8f9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,7 +41,7 @@ "@codemirror/lint": "^6.8.5", "@codemirror/search": "^6.5.11", "@codemirror/state": "^6.5.2", - "@codemirror/view": "^6.38.0", + "@codemirror/view": "^6.38.1", "@lezer/highlight": "^1.2.1", "@lezer/lr": "^1.4.2", "codemirror": "^6.0.2", @@ -57,25 +57,24 @@ "remarkable": "^2.0.1", "sass": "^1.89.2", "vue": "^3.5.17", - "vue-i18n": "^11.1.9", + "vue-i18n": "^11.1.10", "vue-pick-colors": "^1.8.0", "vue-router": "^4.5.1" }, "devDependencies": { - "@eslint/js": "^9.30.1", + "@eslint/js": "^9.31.0", "@lezer/generator": "^1.8.0", - "@types/lodash": "^4.17.20", - "@types/node": "^24.0.12", + "@types/node": "^24.0.14", "@types/remarkable": "^2.0.8", "@vitejs/plugin-vue": "^6.0.0", "@wailsio/runtime": "latest", - "eslint": "^9.30.1", + "eslint": "^9.31.0", "eslint-plugin-vue": "^10.3.0", "globals": "^16.3.0", "typescript": "^5.8.3", - "typescript-eslint": "^8.36.0", + "typescript-eslint": "^8.37.0", "unplugin-vue-components": "^28.8.0", - "vite": "^7.0.3", + "vite": "^7.0.4", "vue-eslint-parser": "^10.2.0", "vue-tsc": "^3.0.1" } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index fb05e86..89c1b82 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,10 +1,11 @@ + + + + \ No newline at end of file diff --git a/frontend/src/views/settings/pages/UpdatesPage.vue b/frontend/src/views/settings/pages/UpdatesPage.vue index 3464ef7..6b0cc31 100644 --- a/frontend/src/views/settings/pages/UpdatesPage.vue +++ b/frontend/src/views/settings/pages/UpdatesPage.vue @@ -266,6 +266,7 @@ const currentVersion = computed(() => { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--settings-border, rgba(0,0,0,0.1)); + background: transparent; .notes-title { font-size: 12px; @@ -278,24 +279,76 @@ const currentVersion = computed(() => { font-size: 12px; color: var(--settings-text); line-height: 1.4; + background: transparent; /* Markdown内容样式 */ :deep(p) { margin: 0 0 6px 0; + background: transparent; } :deep(ul), :deep(ol) { margin: 6px 0; padding-left: 16px; + background: transparent; } :deep(li) { margin-bottom: 4px; + background: transparent; } :deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) { margin: 10px 0 6px 0; font-size: 13px; + background: transparent; + } + + :deep(pre), :deep(code) { + background-color: var(--settings-code-bg, rgba(0,0,0,0.05)); + border-radius: 3px; + padding: 2px 4px; + font-family: monospace; + } + + :deep(pre) { + padding: 8px; + overflow-x: auto; + margin: 6px 0; + } + + :deep(blockquote) { + border-left: 3px solid var(--settings-border, rgba(0,0,0,0.1)); + margin: 6px 0; + padding-left: 10px; + color: var(--settings-text-secondary, #757575); + background: transparent; + } + + :deep(a) { + color: var(--theme-primary, #2196f3); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + :deep(table) { + border-collapse: collapse; + width: 100%; + margin: 6px 0; + background: transparent; + } + + :deep(th), :deep(td) { + border: 1px solid var(--settings-border, rgba(0,0,0,0.1)); + padding: 4px 8px; + background: transparent; + } + + :deep(th) { + background-color: var(--settings-table-header-bg, rgba(0,0,0,0.02)); } } } diff --git a/go.mod b/go.mod index 0772b0a..70ec3bb 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,17 @@ go 1.24.4 require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/creativeprojects/go-selfupdate v1.5.0 + github.com/go-git/go-git/v5 v5.16.2 github.com/knadh/koanf/parsers/json v1.0.0 github.com/knadh/koanf/providers/file v1.2.0 github.com/knadh/koanf/providers/structs v1.0.0 github.com/knadh/koanf/v2 v2.2.2 github.com/robertkrimen/otto v0.5.1 - github.com/wailsapp/wails/v3 v3.0.0-alpha.11 + github.com/wailsapp/wails/v3 v3.0.0-alpha.12 golang.org/x/net v0.42.0 golang.org/x/sys v0.34.0 golang.org/x/text v0.27.0 + modernc.org/sqlite v1.38.0 ) require ( @@ -35,9 +37,8 @@ require ( github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect - github.com/go-git/go-git/v5 v5.16.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-viper/mapstructure/v2 v2.3.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/go-github/v30 v30.1.0 // indirect @@ -82,5 +83,4 @@ require ( modernc.org/libc v1.66.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.38.0 // indirect ) diff --git a/go.sum b/go.sum index 8a84fd3..f75f4ad 100644 --- a/go.sum +++ b/go.sum @@ -58,8 +58,8 @@ github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77 github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= -github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= @@ -164,8 +164,8 @@ github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2 github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/wailsapp/wails/v3 v3.0.0-alpha.11 h1:MYZk2ci8fBd3loWanLzAYgAFcmq4qTRFyNggVqHMaHY= -github.com/wailsapp/wails/v3 v3.0.0-alpha.11/go.mod h1:4LCCW7s9e4PuSmu7l9OTvfWIGMO8TaSiftSeR5NpBIc= +github.com/wailsapp/wails/v3 v3.0.0-alpha.12 h1:z4wYfujk5tSuZXh+59S2DdEncQHG63CW51i2mZFKLS8= +github.com/wailsapp/wails/v3 v3.0.0-alpha.12/go.mod h1:4LCCW7s9e4PuSmu7l9OTvfWIGMO8TaSiftSeR5NpBIc= github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8= github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= diff --git a/internal/models/backup.go b/internal/models/backup.go new file mode 100644 index 0000000..1b2cd60 --- /dev/null +++ b/internal/models/backup.go @@ -0,0 +1,28 @@ +package models + +// Git备份相关类型定义 +type ( + // AuthMethod 定义Git认证方式 + AuthMethod string +) + +const ( + // 认证方式 + Token AuthMethod = "token" + SSHKey AuthMethod = "ssh_key" + UserPass AuthMethod = "user_pass" +) + +// GitBackupConfig Git备份配置 +type GitBackupConfig struct { + Enabled bool `json:"enabled"` + RepoURL string `json:"repo_url"` + AuthMethod AuthMethod `json:"auth_method"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Token string `json:"token,omitempty"` + SSHKeyPath string `json:"ssh_key_path,omitempty"` + SSHKeyPass string `json:"ssh_key_passphrase,omitempty"` + BackupInterval int `json:"backup_interval"` // 分钟 + AutoBackup bool `json:"auto_backup"` +} diff --git a/internal/models/config.go b/internal/models/config.go index 7cfeb4c..d2faa00 100644 --- a/internal/models/config.go +++ b/internal/models/config.go @@ -124,7 +124,7 @@ type AppConfig struct { Editing EditingConfig `json:"editing"` // 编辑设置 Appearance AppearanceConfig `json:"appearance"` // 外观设置 Updates UpdatesConfig `json:"updates"` // 更新设置 - Sync GitSyncConfig `json:"sync"` // Git同步设置 + Backup GitBackupConfig `json:"backup"` // Git备份设置 Metadata ConfigMetadata `json:"metadata"` // 配置元数据 } @@ -139,7 +139,6 @@ func NewDefaultAppConfig() *AppConfig { currentDir, _ := os.UserHomeDir() dataDir := filepath.Join(currentDir, ".voidraft", "data") - repoPath := filepath.Join(currentDir, ".voidraft", "sync-repo") return &AppConfig{ General: GeneralConfig{ @@ -175,10 +174,10 @@ func NewDefaultAppConfig() *AppConfig { CustomTheme: *NewDefaultCustomThemeConfig(), }, Updates: UpdatesConfig{ - Version: "1.2.0", + Version: "1.3.0", AutoUpdate: true, - PrimarySource: UpdateSourceGithub, - BackupSource: UpdateSourceGitea, + PrimarySource: UpdateSourceGitea, + BackupSource: UpdateSourceGithub, BackupBeforeUpdate: true, UpdateTimeout: 30, Github: GithubConfig{ @@ -191,21 +190,16 @@ func NewDefaultAppConfig() *AppConfig { Repo: "voidraft", }, }, - Sync: GitSyncConfig{ - Enabled: false, - RepoURL: "", - Branch: "main", - AuthMethod: Token, - Username: "", - Password: "", - Token: "", - SSHKeyPath: "", - SyncInterval: 60, - LastSyncTime: time.Time{}, - AutoSync: true, - LocalRepoPath: repoPath, - SyncStrategy: LocalFirst, - FilesToSync: []string{"voidraft.db"}, + Backup: GitBackupConfig{ + Enabled: false, + RepoURL: "", + AuthMethod: UserPass, + Username: "", + Password: "", + Token: "", + SSHKeyPath: "", + BackupInterval: 60, + AutoBackup: false, }, Metadata: ConfigMetadata{ LastUpdated: time.Now().Format(time.RFC3339), diff --git a/internal/models/document.go b/internal/models/document.go index 9b7b71f..19cb317 100644 --- a/internal/models/document.go +++ b/internal/models/document.go @@ -11,7 +11,7 @@ type Document struct { Content string `json:"content" db:"content"` CreatedAt time.Time `json:"createdAt" db:"created_at"` UpdatedAt time.Time `json:"updatedAt" db:"updated_at"` - IsDeleted bool `json:"is_deleted"` + IsDeleted bool `json:"is_deleted" db:"is_deleted"` IsLocked bool `json:"is_locked" db:"is_locked"` // 锁定标志,锁定的文档无法被删除 } diff --git a/internal/models/sync.go b/internal/models/sync.go deleted file mode 100644 index f3f6d01..0000000 --- a/internal/models/sync.go +++ /dev/null @@ -1,63 +0,0 @@ -package models - -import "time" - -// AuthMethod 定义Git认证方式 -type AuthMethod string - -const ( - Token AuthMethod = "token" // 个人访问令牌 - SSHKey AuthMethod = "ssh_key" // SSH密钥 - UserPass AuthMethod = "user_pass" // 用户名密码 -) - -// SyncStrategy 定义同步策略 -type SyncStrategy string - -const ( - // LocalFirst 本地优先:如有冲突,保留本地修改 - LocalFirst SyncStrategy = "local_first" - // RemoteFirst 远程优先:如有冲突,采用远程版本 - RemoteFirst SyncStrategy = "remote_first" -) - -// GitSyncConfig 保存Git同步的配置信息 -type GitSyncConfig struct { - Enabled bool `json:"enabled"` - RepoURL string `json:"repo_url"` - Branch string `json:"branch"` - AuthMethod AuthMethod `json:"auth_method"` - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - Token string `json:"token,omitempty"` - SSHKeyPath string `json:"ssh_key_path,omitempty"` - SSHKeyPassphrase string `json:"ssh_key_passphrase,omitempty"` - SyncInterval int `json:"sync_interval"` // 同步间隔(分钟) - LastSyncTime time.Time `json:"last_sync_time"` - AutoSync bool `json:"auto_sync"` // 是否启用自动同步 - LocalRepoPath string `json:"local_repo_path"` - SyncStrategy SyncStrategy `json:"sync_strategy"` // 合并冲突策略 - FilesToSync []string `json:"files_to_sync"` // 要同步的文件列表,默认为数据库文件 -} - -// GitSyncStatus 保存同步状态信息 -type GitSyncStatus struct { - IsSyncing bool `json:"is_syncing"` - LastSyncTime time.Time `json:"last_sync_time"` - LastSyncStatus string `json:"last_sync_status"` // success, failed, conflict - LastErrorMsg string `json:"last_error_msg,omitempty"` - LastCommitID string `json:"last_commit_id,omitempty"` - RemoteCommitID string `json:"remote_commit_id,omitempty"` - CommitAhead int `json:"commit_ahead"` // 本地领先远程的提交数 - CommitBehind int `json:"commit_behind"` // 本地落后远程的提交数 -} - -// SyncLogEntry 记录每次同步操作的日志 -type SyncLogEntry struct { - ID int64 `json:"id"` - Timestamp time.Time `json:"timestamp"` - Action string `json:"action"` // push, pull, reset - Status string `json:"status"` // success, failed - Message string `json:"message,omitempty"` - ChangedFiles int `json:"changed_files"` -} diff --git a/internal/models/theme.go b/internal/models/theme.go index 75da5ea..b4260e7 100644 --- a/internal/models/theme.go +++ b/internal/models/theme.go @@ -66,9 +66,9 @@ func NewDefaultDarkTheme() ThemeColorConfig { Cursor: "#ffffff", Selection: "#0865a9", SelectionBlur: "#225377", - ActiveLine: "#ffffff", - LineNumber: "#ffffff", - ActiveLineNumber: "#ffffff", + ActiveLine: "#ffffff0a", + LineNumber: "#ffffff26", + ActiveLineNumber: "#ffffff99", // 边框分割线 BorderColor: "#1e222a", @@ -104,17 +104,17 @@ func NewDefaultLightTheme() ThemeColorConfig { Cursor: "#000000", Selection: "#77baff", SelectionBlur: "#b2c2ca", - ActiveLine: "#000000", - LineNumber: "#000000", - ActiveLineNumber: "#000000", + ActiveLine: "#0000000a", + LineNumber: "#00000040", + ActiveLineNumber: "#000000aa", // 边框分割线 BorderColor: "#dfdfdf", - BorderLight: "#0000000d", + BorderLight: "#0000000c", // 搜索匹配 SearchMatch: "#005cc5", - MatchingBracket: "#0000001a", + MatchingBracket: "#00000019", } } diff --git a/internal/services/backup_service.go b/internal/services/backup_service.go new file mode 100644 index 0000000..7220683 --- /dev/null +++ b/internal/services/backup_service.go @@ -0,0 +1,388 @@ +package services + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/go-git/go-git/v5" + gitConfig "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/go-git/go-git/v5/plumbing/transport/ssh" + "github.com/wailsapp/wails/v3/pkg/services/log" + + "voidraft/internal/models" + + _ "modernc.org/sqlite" +) + +const ( + dbSerializeFile = "voidraft_data.bin" +) + +// BackupService 提供基于Git的备份功能 +type BackupService struct { + configService *ConfigService + dbService *DatabaseService + repository *git.Repository + logger *log.Service + isInitialized bool + autoBackupTicker *time.Ticker + autoBackupStop chan bool +} + +// NewBackupService 创建新的备份服务实例 +func NewBackupService(configService *ConfigService, dbService *DatabaseService, logger *log.Service) *BackupService { + return &BackupService{ + configService: configService, + dbService: dbService, + logger: logger, + } +} + +// Initialize 初始化备份服务 +func (s *BackupService) Initialize() error { + config, repoPath, err := s.getConfigAndPath() + if err != nil { + return fmt.Errorf("getting backup config: %w", err) + } + + if !config.Enabled { + return nil + } + + // 初始化仓库 + if err := s.initializeRepository(config, repoPath); err != nil { + return fmt.Errorf("initializing repository: %w", err) + } + + // 启动自动备份 + if config.AutoBackup && config.BackupInterval > 0 { + s.StartAutoBackup() + } + + s.isInitialized = true + return nil +} + +// getConfigAndPath 获取备份配置和仓库路径 +func (s *BackupService) getConfigAndPath() (*models.GitBackupConfig, string, error) { + appConfig, err := s.configService.GetConfig() + if err != nil { + return nil, "", fmt.Errorf("getting app config: %w", err) + } + return &appConfig.Backup, appConfig.General.DataPath, nil +} + +// initializeRepository 初始化或打开Git仓库并设置远程 +func (s *BackupService) initializeRepository(config *models.GitBackupConfig, repoPath string) error { + + // 检查本地仓库是否存在 + _, err := os.Stat(filepath.Join(repoPath, ".git")) + if os.IsNotExist(err) { + // 仓库不存在,初始化新仓库 + repo, err := git.PlainInit(repoPath, false) + if err != nil { + return fmt.Errorf("error initializing repository: %w", err) + } + s.repository = repo + } else if err != nil { + return fmt.Errorf("error checking repository path: %w", err) + } else { + // 仓库已存在,打开现有仓库 + repo, err := git.PlainOpen(repoPath) + if err != nil { + return fmt.Errorf("error opening local repository: %w", err) + } + s.repository = repo + } + + // 设置或更新远程仓库 + remote, err := s.repository.Remote("origin") + if err != nil { + if errors.Is(err, git.ErrRemoteNotFound) { + // 远程不存在,添加远程 + _, err = s.repository.CreateRemote(&gitConfig.RemoteConfig{ + Name: "origin", + URLs: []string{config.RepoURL}, + }) + if err != nil { + return fmt.Errorf("error creating remote: %w", err) + } + } else { + return fmt.Errorf("error getting remote: %w", err) + } + } else { + // 检查远程URL是否一致,如果不一致则更新 + if len(remote.Config().URLs) > 0 && remote.Config().URLs[0] != config.RepoURL { + if err := s.repository.DeleteRemote("origin"); err != nil { + return fmt.Errorf("error deleting remote: %w", err) + } + _, err = s.repository.CreateRemote(&gitConfig.RemoteConfig{ + Name: "origin", + URLs: []string{config.RepoURL}, + }) + if err != nil { + return fmt.Errorf("error creating new remote: %w", err) + } + } + } + + return nil +} + +// getAuthMethod 根据配置获取认证方法 +func (s *BackupService) getAuthMethod(config *models.GitBackupConfig) (transport.AuthMethod, error) { + switch config.AuthMethod { + case models.Token: + if config.Token == "" { + return nil, errors.New("token authentication requires a valid token") + } + return &http.BasicAuth{ + Username: "git", // 使用token时,用户名可以是任意值 + Password: config.Token, + }, nil + + case models.UserPass: + if config.Username == "" || config.Password == "" { + return nil, errors.New("username/password authentication requires both username and password") + } + return &http.BasicAuth{ + Username: config.Username, + Password: config.Password, + }, nil + + case models.SSHKey: + if config.SSHKeyPath == "" { + return nil, errors.New("SSH key authentication requires a valid SSH key path") + } + publicKeys, err := ssh.NewPublicKeysFromFile("git", config.SSHKeyPath, config.SSHKeyPass) + if err != nil { + return nil, fmt.Errorf("error creating SSH public keys: %w", err) + } + return publicKeys, nil + + default: + return nil, fmt.Errorf("unsupported authentication method: %s", config.AuthMethod) + } +} + +// serializeDatabase 序列化数据库到文件 +func (s *BackupService) serializeDatabase(repoPath string) error { + if s.dbService == nil || s.dbService.db == nil { + return errors.New("database service not available") + } + + // 获取数据库路径 + dbPath, err := s.dbService.getDatabasePath() + if err != nil { + return fmt.Errorf("getting database path: %w", err) + } + + // 关闭数据库连接以确保所有更改都写入磁盘 + if err := s.dbService.ServiceShutdown(); err != nil { + s.logger.Error("Failed to close database connection", "error", err) + } + + // 直接复制数据库文件到序列化文件 + dbData, err := os.ReadFile(dbPath) + if err != nil { + return fmt.Errorf("reading database file: %w", err) + } + + binFilePath := filepath.Join(repoPath, dbSerializeFile) + if err := os.WriteFile(binFilePath, dbData, 0644); err != nil { + return fmt.Errorf("writing serialized database to file: %w", err) + } + + // 重新初始化数据库服务 + if err := s.dbService.initDatabase(); err != nil { + return fmt.Errorf("reinitializing database: %w", err) + } + + return nil +} + +// PushToRemote 推送本地更改到远程仓库 +func (s *BackupService) PushToRemote() error { + if !s.isInitialized { + return errors.New("backup service not initialized") + } + + config, repoPath, err := s.getConfigAndPath() + if err != nil { + return fmt.Errorf("getting backup config: %w", err) + } + + if !config.Enabled { + return errors.New("backup is disabled") + } + + // 数据库序列化文件的路径 + binFilePath := filepath.Join(repoPath, dbSerializeFile) + + // 函数返回前都删除临时文件 + defer func() { + if _, err := os.Stat(binFilePath); err == nil { + os.Remove(binFilePath) + } + }() + + // 序列化数据库 + if err := s.serializeDatabase(repoPath); err != nil { + return fmt.Errorf("serializing database: %w", err) + } + + // 获取工作树 + w, err := s.repository.Worktree() + if err != nil { + return fmt.Errorf("getting worktree: %w", err) + } + + // 添加序列化的数据库文件 + if _, err := w.Add(dbSerializeFile); err != nil { + return fmt.Errorf("adding serialized database file: %w", err) + } + + // 检查是否有变化需要提交 + status, err := w.Status() + if err != nil { + return fmt.Errorf("getting worktree status: %w", err) + } + + // 如果没有变化,直接返回 + if status.IsClean() { + return errors.New("no changes to backup") + } + + // 创建提交 + _, err = w.Commit(fmt.Sprintf("Backup %s", time.Now().Format("2006-01-02 15:04:05")), &git.CommitOptions{ + Author: &object.Signature{ + Name: "VoidRaft", + Email: "backup@voidraft.app", + When: time.Now(), + }, + }) + if err != nil { + if strings.Contains(err.Error(), "cannot create empty commit") { + return errors.New("no changes to backup") + } + return fmt.Errorf("creating commit: %w", err) + } + + // 获取认证方法并推送到远程 + auth, err := s.getAuthMethod(config) + if err != nil { + return fmt.Errorf("getting auth method: %w", err) + } + + // 推送到远程仓库 + if err := s.repository.Push(&git.PushOptions{ + RemoteName: "origin", + Auth: auth, + }); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { + // 忽略一些常见的非错误情况 + if strings.Contains(err.Error(), "clean working tree") || + strings.Contains(err.Error(), "already up-to-date") || + strings.Contains(err.Error(), " clean working tree") || + strings.Contains(err.Error(), "reference not found") { + // 更新最后推送时间 + return errors.New("no changes to backup") + } + return fmt.Errorf("push failed: %w", err) + } + + return nil +} + +// StartAutoBackup 启动自动备份定时器 +func (s *BackupService) StartAutoBackup() error { + config, _, err := s.getConfigAndPath() + if err != nil { + return fmt.Errorf("getting backup config: %w", err) + } + + if !config.AutoBackup || config.BackupInterval <= 0 { + return nil + } + + s.StopAutoBackup() + + // 将秒转换为分钟 + s.autoBackupTicker = time.NewTicker(time.Duration(config.BackupInterval) * time.Minute) + s.autoBackupStop = make(chan bool) + + go func() { + for { + select { + case <-s.autoBackupTicker.C: + // 执行推送操作 + if err := s.PushToRemote(); err != nil { + s.logger.Error("Auto backup failed", "error", err) + } + case <-s.autoBackupStop: + return + } + } + }() + + return nil +} + +// StopAutoBackup 停止自动备份 +func (s *BackupService) StopAutoBackup() { + if s.autoBackupTicker != nil { + s.autoBackupTicker.Stop() + s.autoBackupTicker = nil + } + + if s.autoBackupStop != nil { + close(s.autoBackupStop) + s.autoBackupStop = nil + } +} + +// Reinitialize 重新初始化备份服务,用于响应配置变更 +func (s *BackupService) Reinitialize() error { + // 停止自动备份 + s.StopAutoBackup() + + // 重新设置标志 + s.isInitialized = false + + // 重新初始化 + return s.Initialize() +} + +// HandleConfigChange 处理备份配置变更 +func (s *BackupService) HandleConfigChange(config *models.GitBackupConfig) error { + + // 如果备份功能禁用,只需停止自动备份 + if !config.Enabled { + s.StopAutoBackup() + s.isInitialized = false + return nil + } + + // 如果服务已初始化,重新初始化以应用新配置 + if s.isInitialized { + return s.Reinitialize() + } + + // 如果服务未初始化但已启用,则初始化 + if config.Enabled && !s.isInitialized { + return s.Initialize() + } + + return nil +} + +// ServiceShutdown 服务关闭时的清理工作 +func (s *BackupService) ServiceShutdown() { + s.StopAutoBackup() +} diff --git a/internal/services/config_migration_service.go b/internal/services/config_migration_service.go index 911da2f..ed86289 100644 --- a/internal/services/config_migration_service.go +++ b/internal/services/config_migration_service.go @@ -19,7 +19,7 @@ import ( const ( // CurrentAppConfigVersion 当前应用配置版本 - CurrentAppConfigVersion = "1.2.0" + CurrentAppConfigVersion = "1.3.0" // BackupFilePattern 备份文件名模式 BackupFilePattern = "%s.backup.%s.json" diff --git a/internal/services/config_notification_service.go b/internal/services/config_notification_service.go index 3258950..c5420c0 100644 --- a/internal/services/config_notification_service.go +++ b/internal/services/config_notification_service.go @@ -22,6 +22,8 @@ const ( ConfigChangeTypeHotkey ConfigChangeType = "hotkey" // ConfigChangeTypeDataPath 数据路径配置变更 ConfigChangeTypeDataPath ConfigChangeType = "datapath" + // ConfigChangeTypeBackup 备份配置变更 + ConfigChangeTypeBackup ConfigChangeType = "backup" ) // ConfigChangeCallback 配置变更回调函数类型 @@ -445,6 +447,29 @@ func CreateDataPathListener(name string, callback func() error) *ConfigListener } } +// CreateBackupConfigListener 创建备份配置监听器 +func CreateBackupConfigListener(name string, callback func(config *models.GitBackupConfig) error) *ConfigListener { + return &ConfigListener{ + Name: name, + ChangeType: ConfigChangeTypeBackup, + Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error { + if newConfig == nil { + defaultConfig := models.NewDefaultAppConfig() + return callback(&defaultConfig.Backup) + } + return callback(&newConfig.Backup) + }, + DebounceDelay: 200 * time.Millisecond, + GetConfigFunc: func(k *koanf.Koanf) *models.AppConfig { + var config models.AppConfig + if err := k.Unmarshal("", &config); err != nil { + return nil + } + return &config + }, + } +} + // ServiceShutdown 关闭服务 func (cns *ConfigNotificationService) ServiceShutdown() error { cns.Cleanup() diff --git a/internal/services/config_service.go b/internal/services/config_service.go index 5b7ab94..1fdac32 100644 --- a/internal/services/config_service.go +++ b/internal/services/config_service.go @@ -298,6 +298,16 @@ func (cs *ConfigService) SetDataPathChangeCallback(callback func() error) error return cs.notificationService.RegisterListener(dataPathListener) } +// SetBackupConfigChangeCallback 设置备份配置变更回调 +func (cs *ConfigService) SetBackupConfigChangeCallback(callback func(config *models.GitBackupConfig) error) error { + cs.mu.Lock() + defer cs.mu.Unlock() + + // 创建备份配置监听器并注册 + backupListener := CreateBackupConfigListener("DefaultBackupConfigListener", callback) + return cs.notificationService.RegisterListener(backupListener) +} + // ServiceShutdown 关闭服务 func (cs *ConfigService) ServiceShutdown() error { cs.stopWatching() diff --git a/internal/services/database_service.go b/internal/services/database_service.go index 4155eb4..7fc83ec 100644 --- a/internal/services/database_service.go +++ b/internal/services/database_service.go @@ -2,18 +2,18 @@ package services import ( "context" + "database/sql" "fmt" "os" "path/filepath" "reflect" - "strings" "sync" "time" "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/sqlite" + _ "modernc.org/sqlite" ) const ( @@ -63,20 +63,6 @@ CREATE TABLE IF NOT EXISTS key_bindings ( updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(command, extension) )` - - // Git sync logs table - sqlCreateSyncLogsTable = ` -CREATE TABLE IF NOT EXISTS sync_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - action TEXT NOT NULL, - status TEXT NOT NULL, - message TEXT, - commit_id TEXT, - changed_files INTEGER DEFAULT 0, - repo_url TEXT NOT NULL, - branch TEXT NOT NULL -)` ) // ColumnInfo 存储列的信息 @@ -95,7 +81,7 @@ type TableModel struct { type DatabaseService struct { configService *ConfigService logger *log.Service - SQLite *sqlite.Service + db *sql.DB mu sync.RWMutex ctx context.Context tableModels []TableModel // 注册的表模型 @@ -110,7 +96,6 @@ func NewDatabaseService(configService *ConfigService, logger *log.Service) *Data ds := &DatabaseService{ configService: configService, logger: logger, - SQLite: sqlite.New(), } // 注册所有模型 @@ -127,8 +112,6 @@ func (ds *DatabaseService) registerAllModels() { ds.RegisterModel("extensions", &models.Extension{}) // 快捷键表 ds.RegisterModel("key_bindings", &models.KeyBinding{}) - // 同步日志表 - ds.RegisterModel("sync_logs", &models.SyncLogEntry{}) } // ServiceStartup initializes the service when the application starts @@ -150,28 +133,19 @@ func (ds *DatabaseService) initDatabase() error { return fmt.Errorf("failed to create database directory: %w", err) } - // 检查数据库文件是否存在,如果不存在则创建 - if _, err := os.Stat(dbPath); os.IsNotExist(err) { - // 创建空文件 - file, err := os.Create(dbPath) - if err != nil { - return fmt.Errorf("failed to create database file: %w", err) - } - file.Close() - } - - // 配置SQLite服务 - ds.SQLite.Configure(&sqlite.Config{ - DBSource: dbPath, - }) - // 打开数据库连接 - if err := ds.SQLite.Open(); err != nil { + ds.db, err = sql.Open("sqlite", dbPath) + if err != nil { return fmt.Errorf("failed to open database: %w", err) } + // 测试连接 + if err := ds.db.Ping(); err != nil { + return fmt.Errorf("failed to ping database: %w", err) + } + // 应用性能优化设置 - if err := ds.SQLite.Execute(sqlOptimizationSettings); err != nil { + if _, err := ds.db.Exec(sqlOptimizationSettings); err != nil { return fmt.Errorf("failed to apply optimization settings: %w", err) } @@ -207,11 +181,10 @@ func (ds *DatabaseService) createTables() error { sqlCreateDocumentsTable, sqlCreateExtensionsTable, sqlCreateKeyBindingsTable, - sqlCreateSyncLogsTable, } for _, table := range tables { - if err := ds.SQLite.Execute(table); err != nil { + if _, err := ds.db.Exec(table); err != nil { return err } } @@ -231,14 +204,10 @@ func (ds *DatabaseService) createIndexes() error { `CREATE INDEX IF NOT EXISTS idx_key_bindings_command ON key_bindings(command)`, `CREATE INDEX IF NOT EXISTS idx_key_bindings_extension ON key_bindings(extension)`, `CREATE INDEX IF NOT EXISTS idx_key_bindings_enabled ON key_bindings(enabled)`, - // Sync logs indexes - `CREATE INDEX IF NOT EXISTS idx_sync_logs_timestamp ON sync_logs(timestamp DESC)`, - `CREATE INDEX IF NOT EXISTS idx_sync_logs_action ON sync_logs(action)`, - `CREATE INDEX IF NOT EXISTS idx_sync_logs_status ON sync_logs(status)`, } for _, index := range indexes { - if err := ds.SQLite.Execute(index); err != nil { + if _, err := ds.db.Exec(index); err != nil { return err } } @@ -286,7 +255,7 @@ func (ds *DatabaseService) syncModelTable(tableName string, model interface{}) e // 执行添加列的SQL alterSQL := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s DEFAULT %s", tableName, colName, colInfo.SQLType, colInfo.DefaultValue) - if err := ds.SQLite.Execute(alterSQL); err != nil { + if _, err := ds.db.Exec(alterSQL); err != nil { return fmt.Errorf("failed to add column %s: %w", colName, err) } } @@ -298,23 +267,30 @@ func (ds *DatabaseService) syncModelTable(tableName string, model interface{}) e // getTableColumns 获取表的列信息 func (ds *DatabaseService) getTableColumns(table string) (map[string]string, error) { query := fmt.Sprintf("PRAGMA table_info(%s)", table) - rows, err := ds.SQLite.Query(query) + rows, err := ds.db.Query(query) if err != nil { return nil, err } + defer rows.Close() columns := make(map[string]string) - for _, row := range rows { - name, ok1 := row["name"].(string) - typeName, ok2 := row["type"].(string) + for rows.Next() { + var cid int + var name, typeName string + var notNull, pk int + var dflt_value interface{} - if !ok1 || !ok2 { - continue + if err := rows.Scan(&cid, &name, &typeName, ¬Null, &dflt_value, &pk); err != nil { + return nil, err } columns[name] = typeName } + if err := rows.Err(); err != nil { + return nil, err + } + return columns, nil } @@ -336,11 +312,11 @@ func (ds *DatabaseService) getModelColumns(model interface{}) (map[string]Column for i := 0; i < t.NumField(); i++ { field := t.Field(i) - // 获取数据库字段名 + // 只处理有db标签的字段 dbTag := field.Tag.Get("db") if dbTag == "" { - // 如果没有db标签,则使用字段名的蛇形命名方式 - dbTag = toSnakeCase(field.Name) + // 如果没有db标签,跳过该字段 + continue } // 获取字段类型对应的SQL类型和默认值 @@ -355,18 +331,6 @@ func (ds *DatabaseService) getModelColumns(model interface{}) (map[string]Column return columns, nil } -// toSnakeCase 将驼峰命名转换为蛇形命名 -func toSnakeCase(s string) string { - var result strings.Builder - for i, r := range s { - if i > 0 && 'A' <= r && r <= 'Z' { - result.WriteRune('_') - } - result.WriteRune(r) - } - return strings.ToLower(result.String()) -} - // getSQLTypeAndDefault 根据Go类型获取对应的SQL类型和默认值 func getSQLTypeAndDefault(t reflect.Type) (string, string) { switch t.Kind() { @@ -390,14 +354,19 @@ func getSQLTypeAndDefault(t reflect.Type) (string, string) { // ServiceShutdown shuts down the service when the application closes func (ds *DatabaseService) ServiceShutdown() error { - return ds.SQLite.Close() + if ds.db != nil { + return ds.db.Close() + } + return nil } // OnDataPathChanged handles data path changes func (ds *DatabaseService) OnDataPathChanged() error { // 关闭当前连接 - if err := ds.SQLite.Close(); err != nil { - return err + if ds.db != nil { + if err := ds.db.Close(); err != nil { + return err + } } // 用新路径重新初始化 diff --git a/internal/services/dialog_service.go b/internal/services/dialog_service.go index 9af16f3..a113f13 100644 --- a/internal/services/dialog_service.go +++ b/internal/services/dialog_service.go @@ -50,7 +50,6 @@ func (ds *DialogService) SelectDirectory() (string, error) { // 对话框文本配置 Title: "Select Directory", - Message: "Select the folder where you want to store your app data", ButtonText: "Select", // 不设置过滤器,因为我们选择目录 @@ -69,3 +68,44 @@ func (ds *DialogService) SelectDirectory() (string, error) { } return path, nil } + +// SelectFile 打开文件选择对话框 +func (ds *DialogService) SelectFile() (string, error) { + dialog := application.OpenFileDialog() + dialog.SetOptions(&application.OpenFileDialogOptions{ + // 目录选择配置 + CanChooseDirectories: false, // 允许选择目录 + CanChooseFiles: true, // 不允许选择文件 + CanCreateDirectories: true, // 允许创建新目录 + AllowsMultipleSelection: false, // 单选模式 + + // 显示配置 + ShowHiddenFiles: true, // 不显示隐藏文件 + HideExtension: false, // 不隐藏扩展名 + CanSelectHiddenExtension: false, // 不允许选择隐藏扩展名 + TreatsFilePackagesAsDirectories: false, // 不将文件包当作目录处理 + AllowsOtherFileTypes: false, // 不允许其他文件类型 + + // 系统配置 + ResolvesAliases: true, // 解析别名/快捷方式 + + // 对话框文本配置 + Title: "Select File", + ButtonText: "Select File", + + // 不设置过滤器,因为我们选择目录 + Filters: nil, + + // 不指定默认目录,让系统决定 + Directory: "", + + // 绑定到主窗口 + Window: ds.window, + }) + + path, err := dialog.PromptForSingleSelection() + if err != nil { + return "", err + } + return path, nil +} diff --git a/internal/services/document_service.go b/internal/services/document_service.go index 16f9c63..fe898dc 100644 --- a/internal/services/document_service.go +++ b/internal/services/document_service.go @@ -111,19 +111,15 @@ func (ds *DocumentService) ServiceStartup(ctx context.Context, options applicati // ensureDefaultDocument ensures a default document exists func (ds *DocumentService) ensureDefaultDocument() error { + if ds.databaseService == nil || ds.databaseService.db == nil { + return errors.New("database service not available") + } + // Check if any document exists - rows, err := ds.databaseService.SQLite.Query(sqlCountDocuments) + var count int64 + err := ds.databaseService.db.QueryRow(sqlCountDocuments).Scan(&count) if err != nil { - return err - } - - if len(rows) == 0 { - return fmt.Errorf("failed to query document count") - } - - count, ok := rows[0]["COUNT(*)"].(int64) - if !ok { - return fmt.Errorf("failed to convert count to int64") + return fmt.Errorf("failed to query document count: %w", err) } // If no documents exist, create default document @@ -140,52 +136,42 @@ func (ds *DocumentService) GetDocumentByID(id int64) (*models.Document, error) { ds.mu.RLock() defer ds.mu.RUnlock() - rows, err := ds.databaseService.SQLite.Query(sqlGetDocumentByID, id) + if ds.databaseService == nil || ds.databaseService.db == nil { + return nil, errors.New("database service not available") + } + + doc := &models.Document{} + var createdAt, updatedAt string + var isDeleted, isLocked int + + err := ds.databaseService.db.QueryRow(sqlGetDocumentByID, id).Scan( + &doc.ID, + &doc.Title, + &doc.Content, + &createdAt, + &updatedAt, + &isDeleted, + &isLocked, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } return nil, fmt.Errorf("failed to get document by ID: %w", err) } - if len(rows) == 0 { - return nil, nil + // 转换时间字段 + if t, err := time.Parse("2006-01-02 15:04:05", createdAt); err == nil { + doc.CreatedAt = t + } + if t, err := time.Parse("2006-01-02 15:04:05", updatedAt); err == nil { + doc.UpdatedAt = t } - row := rows[0] - doc := &models.Document{} - - // 从Row中提取数据 - if idVal, ok := row["id"].(int64); ok { - doc.ID = idVal - } - - if title, ok := row["title"].(string); ok { - doc.Title = title - } - - if content, ok := row["content"].(string); ok { - doc.Content = content - } - - if createdAt, ok := row["created_at"].(string); ok { - t, err := time.Parse("2006-01-02 15:04:05", createdAt) - if err == nil { - doc.CreatedAt = t - } - } - - if updatedAt, ok := row["updated_at"].(string); ok { - t, err := time.Parse("2006-01-02 15:04:05", updatedAt) - if err == nil { - doc.UpdatedAt = t - } - } - - if isDeletedInt, ok := row["is_deleted"].(int64); ok { - doc.IsDeleted = isDeletedInt == 1 - } - - if isLockedInt, ok := row["is_locked"].(int64); ok { - doc.IsLocked = isLockedInt == 1 - } + // 转换布尔字段 + doc.IsDeleted = isDeleted == 1 + doc.IsLocked = isLocked == 1 return doc, nil } @@ -195,6 +181,10 @@ func (ds *DocumentService) CreateDocument(title string) (*models.Document, error ds.mu.Lock() defer ds.mu.Unlock() + if ds.databaseService == nil || ds.databaseService.db == nil { + return nil, errors.New("database service not available") + } + // Create document with default content now := time.Now() doc := &models.Document{ @@ -207,27 +197,18 @@ func (ds *DocumentService) CreateDocument(title string) (*models.Document, error } // 执行插入操作 - if err := ds.databaseService.SQLite.Execute(sqlInsertDocument, - doc.Title, doc.Content, doc.CreatedAt, doc.UpdatedAt); err != nil { + result, err := ds.databaseService.db.Exec(sqlInsertDocument, + doc.Title, doc.Content, doc.CreatedAt.Format("2006-01-02 15:04:05"), doc.UpdatedAt.Format("2006-01-02 15:04:05")) + if err != nil { return nil, fmt.Errorf("failed to create document: %w", err) } // 获取自增ID - lastIDRows, err := ds.databaseService.SQLite.Query("SELECT last_insert_rowid()") + lastID, err := result.LastInsertId() if err != nil { return nil, fmt.Errorf("failed to get last insert ID: %w", err) } - if len(lastIDRows) == 0 { - return nil, fmt.Errorf("no rows returned for last insert ID query") - } - - // 从结果中提取ID - lastID, ok := lastIDRows[0]["last_insert_rowid()"].(int64) - if !ok { - return nil, fmt.Errorf("failed to convert last insert ID to int64") - } - // 返回带ID的文档 doc.ID = lastID return doc, nil @@ -238,6 +219,10 @@ func (ds *DocumentService) LockDocument(id int64) error { ds.mu.Lock() defer ds.mu.Unlock() + if ds.databaseService == nil || ds.databaseService.db == nil { + return errors.New("database service not available") + } + // 检查文档是否存在且未删除 doc, err := ds.GetDocumentByID(id) if err != nil { @@ -255,7 +240,7 @@ func (ds *DocumentService) LockDocument(id int64) error { return nil } - err = ds.databaseService.SQLite.Execute(sqlSetDocumentLocked, time.Now(), id) + _, err = ds.databaseService.db.Exec(sqlSetDocumentLocked, time.Now().Format("2006-01-02 15:04:05"), id) if err != nil { return fmt.Errorf("failed to lock document: %w", err) } @@ -267,6 +252,10 @@ func (ds *DocumentService) UnlockDocument(id int64) error { ds.mu.Lock() defer ds.mu.Unlock() + if ds.databaseService == nil || ds.databaseService.db == nil { + return errors.New("database service not available") + } + // 检查文档是否存在 doc, err := ds.GetDocumentByID(id) if err != nil { @@ -281,7 +270,7 @@ func (ds *DocumentService) UnlockDocument(id int64) error { return nil } - err = ds.databaseService.SQLite.Execute(sqlSetDocumentUnlocked, time.Now(), id) + _, err = ds.databaseService.db.Exec(sqlSetDocumentUnlocked, time.Now().Format("2006-01-02 15:04:05"), id) if err != nil { return fmt.Errorf("failed to unlock document: %w", err) } @@ -293,7 +282,11 @@ func (ds *DocumentService) UpdateDocumentContent(id int64, content string) error ds.mu.Lock() defer ds.mu.Unlock() - err := ds.databaseService.SQLite.Execute(sqlUpdateDocumentContent, content, time.Now(), id) + if ds.databaseService == nil || ds.databaseService.db == nil { + return errors.New("database service not available") + } + + _, err := ds.databaseService.db.Exec(sqlUpdateDocumentContent, content, time.Now().Format("2006-01-02 15:04:05"), id) if err != nil { return fmt.Errorf("failed to update document content: %w", err) } @@ -305,7 +298,11 @@ func (ds *DocumentService) UpdateDocumentTitle(id int64, title string) error { ds.mu.Lock() defer ds.mu.Unlock() - err := ds.databaseService.SQLite.Execute(sqlUpdateDocumentTitle, title, time.Now(), id) + if ds.databaseService == nil || ds.databaseService.db == nil { + return errors.New("database service not available") + } + + _, err := ds.databaseService.db.Exec(sqlUpdateDocumentTitle, title, time.Now().Format("2006-01-02 15:04:05"), id) if err != nil { return fmt.Errorf("failed to update document title: %w", err) } @@ -317,6 +314,10 @@ func (ds *DocumentService) DeleteDocument(id int64) error { ds.mu.Lock() defer ds.mu.Unlock() + if ds.databaseService == nil || ds.databaseService.db == nil { + return errors.New("database service not available") + } + // 不允许删除默认文档 if id == sqlDefaultDocumentID { return fmt.Errorf("cannot delete the default document") @@ -334,7 +335,7 @@ func (ds *DocumentService) DeleteDocument(id int64) error { return fmt.Errorf("cannot delete locked document: %d", id) } - err = ds.databaseService.SQLite.Execute(sqlMarkDocumentAsDeleted, time.Now(), id) + _, err = ds.databaseService.db.Exec(sqlMarkDocumentAsDeleted, time.Now().Format("2006-01-02 15:04:05"), id) if err != nil { return fmt.Errorf("failed to mark document as deleted: %w", err) } @@ -346,7 +347,11 @@ func (ds *DocumentService) RestoreDocument(id int64) error { ds.mu.Lock() defer ds.mu.Unlock() - err := ds.databaseService.SQLite.Execute(sqlRestoreDocument, time.Now(), id) + if ds.databaseService == nil || ds.databaseService.db == nil { + return errors.New("database service not available") + } + + _, err := ds.databaseService.db.Exec(sqlRestoreDocument, time.Now().Format("2006-01-02 15:04:05"), id) if err != nil { return fmt.Errorf("failed to restore document: %w", err) } @@ -358,44 +363,50 @@ func (ds *DocumentService) ListAllDocumentsMeta() ([]*models.Document, error) { ds.mu.RLock() defer ds.mu.RUnlock() - rows, err := ds.databaseService.SQLite.Query(sqlListAllDocumentsMeta) + if ds.databaseService == nil || ds.databaseService.db == nil { + return nil, errors.New("database service not available") + } + + rows, err := ds.databaseService.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 _, row := range rows { + for rows.Next() { doc := &models.Document{IsDeleted: false} + var createdAt, updatedAt string + var isLocked int - if id, ok := row["id"].(int64); ok { - doc.ID = id + err := rows.Scan( + &doc.ID, + &doc.Title, + &createdAt, + &updatedAt, + &isLocked, + ) + + if err != nil { + return nil, fmt.Errorf("failed to scan document row: %w", err) } - if title, ok := row["title"].(string); ok { - doc.Title = title - } - - if createdAt, ok := row["created_at"].(string); ok { - t, err := time.Parse("2006-01-02 15:04:05", createdAt) - if err == nil { - doc.CreatedAt = t - } - } - - if updatedAt, ok := row["updated_at"].(string); ok { - t, err := time.Parse("2006-01-02 15:04:05", updatedAt) - if err == nil { - doc.UpdatedAt = t - } - } - - if isLockedInt, ok := row["is_locked"].(int64); ok { - doc.IsLocked = isLockedInt == 1 + // 转换时间字段 + if t, err := time.Parse("2006-01-02 15:04:05", createdAt); err == nil { + doc.CreatedAt = t + } + if t, err := time.Parse("2006-01-02 15:04:05", updatedAt); err == nil { + doc.UpdatedAt = t } + doc.IsLocked = isLocked == 1 documents = append(documents, doc) } + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating document rows: %w", err) + } + return documents, nil } @@ -404,44 +415,50 @@ func (ds *DocumentService) ListDeletedDocumentsMeta() ([]*models.Document, error ds.mu.RLock() defer ds.mu.RUnlock() - rows, err := ds.databaseService.SQLite.Query(sqlListDeletedDocumentsMeta) + if ds.databaseService == nil || ds.databaseService.db == nil { + return nil, errors.New("database service not available") + } + + rows, err := ds.databaseService.db.Query(sqlListDeletedDocumentsMeta) if err != nil { return nil, fmt.Errorf("failed to list deleted document meta: %w", err) } + defer rows.Close() var documents []*models.Document - for _, row := range rows { + for rows.Next() { doc := &models.Document{IsDeleted: true} + var createdAt, updatedAt string + var isLocked int - if id, ok := row["id"].(int64); ok { - doc.ID = id + err := rows.Scan( + &doc.ID, + &doc.Title, + &createdAt, + &updatedAt, + &isLocked, + ) + + if err != nil { + return nil, fmt.Errorf("failed to scan document row: %w", err) } - if title, ok := row["title"].(string); ok { - doc.Title = title - } - - if createdAt, ok := row["created_at"].(string); ok { - t, err := time.Parse("2006-01-02 15:04:05", createdAt) - if err == nil { - doc.CreatedAt = t - } - } - - if updatedAt, ok := row["updated_at"].(string); ok { - t, err := time.Parse("2006-01-02 15:04:05", updatedAt) - if err == nil { - doc.UpdatedAt = t - } - } - - if isLockedInt, ok := row["is_locked"].(int64); ok { - doc.IsLocked = isLockedInt == 1 + // 转换时间字段 + if t, err := time.Parse("2006-01-02 15:04:05", createdAt); err == nil { + doc.CreatedAt = t + } + if t, err := time.Parse("2006-01-02 15:04:05", updatedAt); err == nil { + doc.UpdatedAt = t } + doc.IsLocked = isLocked == 1 documents = append(documents, doc) } + if err = rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating deleted document rows: %w", err) + } + return documents, nil } @@ -450,7 +467,12 @@ func (ds *DocumentService) GetFirstDocumentID() (int64, error) { ds.mu.RLock() defer ds.mu.RUnlock() - rows, err := ds.databaseService.SQLite.Query(sqlGetFirstDocumentID) + if ds.databaseService == nil || ds.databaseService.db == nil { + return 0, errors.New("database service not available") + } + + var id int64 + err := ds.databaseService.db.QueryRow(sqlGetFirstDocumentID).Scan(&id) if err != nil { if errors.Is(err, sql.ErrNoRows) { return 0, nil // No documents exist @@ -458,14 +480,5 @@ func (ds *DocumentService) GetFirstDocumentID() (int64, error) { return 0, fmt.Errorf("failed to get first document ID: %w", err) } - if len(rows) == 0 { - return 0, nil - } - - id, ok := rows[0]["id"].(int64) - if !ok { - return 0, fmt.Errorf("failed to convert ID to int64") - } - return id, nil } diff --git a/internal/services/extension_service.go b/internal/services/extension_service.go index e73f895..7bbe0ac 100644 --- a/internal/services/extension_service.go +++ b/internal/services/extension_service.go @@ -104,21 +104,17 @@ func (es *ExtensionService) initDatabase() error { es.mu.Lock() defer es.mu.Unlock() + if es.databaseService == nil || es.databaseService.db == nil { + return &ExtensionError{"check_db", "", errors.New("database service not available")} + } + // 检查是否已有扩展数据 - rows, err := es.databaseService.SQLite.Query("SELECT COUNT(*) FROM extensions") + var count int64 + err := es.databaseService.db.QueryRow("SELECT COUNT(*) FROM extensions").Scan(&count) if err != nil { return &ExtensionError{"check_extensions_count", "", err} } - if len(rows) == 0 { - return &ExtensionError{"check_extensions_count", "", fmt.Errorf("no rows returned")} - } - - count, ok := rows[0]["COUNT(*)"].(int64) - if !ok { - return &ExtensionError{"convert_count", "", fmt.Errorf("failed to convert count to int64")} - } - // 如果没有数据,插入默认配置 if count == 0 { if err := es.insertDefaultExtensions(); err != nil { @@ -133,16 +129,15 @@ func (es *ExtensionService) initDatabase() error { // insertDefaultExtensions 插入默认扩展配置 func (es *ExtensionService) insertDefaultExtensions() error { defaultSettings := models.NewDefaultExtensionSettings() - now := time.Now() + now := time.Now().Format("2006-01-02 15:04:05") for _, ext := range defaultSettings.Extensions { - configJSON, err := json.Marshal(ext.Config) if err != nil { return &ExtensionError{"marshal_config", string(ext.ID), err} } - err = es.databaseService.SQLite.Execute(sqlInsertExtension, + _, err = es.databaseService.db.Exec(sqlInsertExtension, string(ext.ID), ext.Enabled, ext.IsDefault, @@ -153,7 +148,6 @@ func (es *ExtensionService) insertDefaultExtensions() error { if err != nil { return &ExtensionError{"insert_extension", string(ext.ID), err} } - } return nil @@ -179,38 +173,51 @@ func (es *ExtensionService) GetAllExtensions() ([]models.Extension, error) { es.mu.RLock() defer es.mu.RUnlock() - rows, err := es.databaseService.SQLite.Query(sqlGetAllExtensions) + if es.databaseService == nil || es.databaseService.db == nil { + return nil, &ExtensionError{"query_db", "", errors.New("database service not available")} + } + + rows, err := es.databaseService.db.Query(sqlGetAllExtensions) if err != nil { return nil, &ExtensionError{"query_extensions", "", err} } + defer rows.Close() var extensions []models.Extension - for _, row := range rows { + for rows.Next() { var ext models.Extension + var id string + var configJSON string + var enabled, isDefault int - if id, ok := row["id"].(string); ok { - ext.ID = models.ExtensionID(id) + err := rows.Scan( + &id, + &enabled, + &isDefault, + &configJSON, + ) + + if err != nil { + return nil, &ExtensionError{"scan_extension", "", err} } - if enabled, ok := row["enabled"].(int64); ok { - ext.Enabled = enabled == 1 - } + ext.ID = models.ExtensionID(id) + ext.Enabled = enabled == 1 + ext.IsDefault = isDefault == 1 - if isDefault, ok := row["is_default"].(int64); ok { - ext.IsDefault = isDefault == 1 - } - - if configJSON, ok := row["config"].(string); ok { - var config models.ExtensionConfig - if err := json.Unmarshal([]byte(configJSON), &config); err != nil { - return nil, &ExtensionError{"unmarshal_config", string(ext.ID), err} - } - ext.Config = config + var config models.ExtensionConfig + if err := json.Unmarshal([]byte(configJSON), &config); err != nil { + return nil, &ExtensionError{"unmarshal_config", id, err} } + ext.Config = config extensions = append(extensions, ext) } + if err = rows.Err(); err != nil { + return nil, &ExtensionError{"iterate_extensions", "", err} + } + return extensions, nil } @@ -224,6 +231,10 @@ func (es *ExtensionService) UpdateExtensionState(id models.ExtensionID, enabled es.mu.Lock() defer es.mu.Unlock() + if es.databaseService == nil || es.databaseService.db == nil { + return &ExtensionError{"check_db", string(id), errors.New("database service not available")} + } + var configJSON []byte var err error @@ -234,24 +245,19 @@ func (es *ExtensionService) UpdateExtensionState(id models.ExtensionID, enabled } } else { // 如果没有提供配置,保持原有配置 - rows, err := es.databaseService.SQLite.Query("SELECT config FROM extensions WHERE id = ?", string(id)) + var currentConfigJSON string + err := es.databaseService.db.QueryRow("SELECT config FROM extensions WHERE id = ?", string(id)).Scan(¤tConfigJSON) if err != nil { return &ExtensionError{"query_current_config", string(id), err} } - - if len(rows) == 0 { - return &ExtensionError{"query_current_config", string(id), fmt.Errorf("extension not found")} - } - - currentConfigJSON, ok := rows[0]["config"].(string) - if !ok { - return &ExtensionError{"convert_config", string(id), fmt.Errorf("failed to get current config")} - } - configJSON = []byte(currentConfigJSON) } - err = es.databaseService.SQLite.Execute(sqlUpdateExtension, enabled, string(configJSON), time.Now(), string(id)) + _, err = es.databaseService.db.Exec(sqlUpdateExtension, + enabled, + string(configJSON), + time.Now().Format("2006-01-02 15:04:05"), + string(id)) if err != nil { return &ExtensionError{"update_extension", string(id), err} } @@ -276,8 +282,12 @@ func (es *ExtensionService) ResetAllExtensionsToDefault() error { es.mu.Lock() defer es.mu.Unlock() + if es.databaseService == nil || es.databaseService.db == nil { + return &ExtensionError{"check_db", "", errors.New("database service not available")} + } + // 删除所有现有扩展 - err := es.databaseService.SQLite.Execute(sqlDeleteAllExtensions) + _, err := es.databaseService.db.Exec(sqlDeleteAllExtensions) if err != nil { return &ExtensionError{"delete_all_extensions", "", err} } diff --git a/internal/services/keybinding_service.go b/internal/services/keybinding_service.go index 3d14059..2e7a858 100644 --- a/internal/services/keybinding_service.go +++ b/internal/services/keybinding_service.go @@ -105,21 +105,17 @@ func (kbs *KeyBindingService) initDatabase() error { kbs.mu.Lock() defer kbs.mu.Unlock() + if kbs.databaseService == nil || kbs.databaseService.db == nil { + return &KeyBindingError{"check_db", "", errors.New("database service not available")} + } + // 检查是否已有快捷键数据 - rows, err := kbs.databaseService.SQLite.Query("SELECT COUNT(*) FROM key_bindings") + var count int64 + err := kbs.databaseService.db.QueryRow("SELECT COUNT(*) FROM key_bindings").Scan(&count) if err != nil { return &KeyBindingError{"check_keybindings_count", "", err} } - if len(rows) == 0 { - return &KeyBindingError{"check_keybindings_count", "", fmt.Errorf("no rows returned")} - } - - count, ok := rows[0]["COUNT(*)"].(int64) - if !ok { - return &KeyBindingError{"convert_count", "", fmt.Errorf("failed to convert count to int64")} - } - // 如果没有数据,插入默认配置 if count == 0 { if err := kbs.insertDefaultKeyBindings(); err != nil { @@ -134,11 +130,10 @@ func (kbs *KeyBindingService) initDatabase() error { // insertDefaultKeyBindings 插入默认快捷键配置 func (kbs *KeyBindingService) insertDefaultKeyBindings() error { defaultConfig := models.NewDefaultKeyBindingConfig() - now := time.Now() + now := time.Now().Format("2006-01-02 15:04:05") for _, kb := range defaultConfig.KeyBindings { - - err := kbs.databaseService.SQLite.Execute(sqlInsertKeyBinding, + _, err := kbs.databaseService.db.Exec(sqlInsertKeyBinding, string(kb.Command), // 转换为字符串存储 string(kb.Extension), // 转换为字符串存储 kb.Key, @@ -150,7 +145,6 @@ func (kbs *KeyBindingService) insertDefaultKeyBindings() error { if err != nil { return &KeyBindingError{"insert_keybinding", string(kb.Command), err} } - } return nil @@ -161,38 +155,46 @@ func (kbs *KeyBindingService) GetAllKeyBindings() ([]models.KeyBinding, error) { kbs.mu.RLock() defer kbs.mu.RUnlock() - rows, err := kbs.databaseService.SQLite.Query(sqlGetAllKeyBindings) + if kbs.databaseService == nil || kbs.databaseService.db == nil { + return nil, &KeyBindingError{"query_db", "", errors.New("database service not available")} + } + + rows, err := kbs.databaseService.db.Query(sqlGetAllKeyBindings) if err != nil { return nil, &KeyBindingError{"query_keybindings", "", err} } + defer rows.Close() var keyBindings []models.KeyBinding - for _, row := range rows { + for rows.Next() { var kb models.KeyBinding + var command, extension string + var enabled, isDefault int - if command, ok := row["command"].(string); ok { - kb.Command = models.KeyBindingCommand(command) + err := rows.Scan( + &command, + &extension, + &kb.Key, + &enabled, + &isDefault, + ) + + if err != nil { + return nil, &KeyBindingError{"scan_keybinding", "", err} } - if extension, ok := row["extension"].(string); ok { - kb.Extension = models.ExtensionID(extension) - } - - if key, ok := row["key"].(string); ok { - kb.Key = key - } - - if enabled, ok := row["enabled"].(int64); ok { - kb.Enabled = enabled == 1 - } - - if isDefault, ok := row["is_default"].(int64); ok { - kb.IsDefault = isDefault == 1 - } + kb.Command = models.KeyBindingCommand(command) + kb.Extension = models.ExtensionID(extension) + kb.Enabled = enabled == 1 + kb.IsDefault = isDefault == 1 keyBindings = append(keyBindings, kb) } + if err = rows.Err(); err != nil { + return nil, &KeyBindingError{"iterate_keybindings", "", err} + } + return keyBindings, nil } diff --git a/internal/services/service_manager.go b/internal/services/service_manager.go index d488208..b1c5c4d 100644 --- a/internal/services/service_manager.go +++ b/internal/services/service_manager.go @@ -5,14 +5,12 @@ import ( "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/services/log" - "github.com/wailsapp/wails/v3/pkg/services/sqlite" ) // ServiceManager 服务管理器,负责协调各个服务 type ServiceManager struct { configService *ConfigService databaseService *DatabaseService - sqliteService *sqlite.Service documentService *DocumentService windowService *WindowService migrationService *MigrationService @@ -25,6 +23,7 @@ type ServiceManager struct { startupService *StartupService selfUpdateService *SelfUpdateService translationService *TranslationService + BackupService *BackupService logger *log.Service } @@ -36,9 +35,6 @@ func NewServiceManager() *ServiceManager { // 初始化配置服务 configService := NewConfigService(logger) - // 初始化SQLite服务 - sqliteService := sqlite.New() - // 初始化数据库服务 databaseService := NewDatabaseService(configService, logger) @@ -81,6 +77,9 @@ func NewServiceManager() *ServiceManager { // 初始化翻译服务 translationService := NewTranslationService(logger) + // 初始化备份服务 + backupService := NewBackupService(configService, databaseService, logger) + // 使用新的配置通知系统设置热键配置变更监听 err = configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error { return hotkeyService.UpdateHotkey(enable, hotkey) @@ -97,10 +96,17 @@ func NewServiceManager() *ServiceManager { panic(err) } + // 设置备份配置变更监听,处理备份配置变更 + err = configService.SetBackupConfigChangeCallback(func(config *models.GitBackupConfig) error { + return backupService.HandleConfigChange(config) + }) + if err != nil { + panic(err) + } + return &ServiceManager{ configService: configService, databaseService: databaseService, - sqliteService: sqliteService, documentService: documentService, windowService: windowService, migrationService: migrationService, @@ -113,6 +119,7 @@ func NewServiceManager() *ServiceManager { startupService: startupService, selfUpdateService: selfUpdateService, translationService: translationService, + BackupService: backupService, logger: logger, } } @@ -121,7 +128,6 @@ func NewServiceManager() *ServiceManager { func (sm *ServiceManager) GetServices() []application.Service { services := []application.Service{ application.NewService(sm.configService), - application.NewService(sm.sqliteService), application.NewService(sm.databaseService), application.NewService(sm.documentService), application.NewService(sm.windowService), @@ -135,6 +141,7 @@ func (sm *ServiceManager) GetServices() []application.Service { application.NewService(sm.startupService), application.NewService(sm.selfUpdateService), application.NewService(sm.translationService), + application.NewService(sm.BackupService), } return services } @@ -194,11 +201,6 @@ func (sm *ServiceManager) GetDatabaseService() *DatabaseService { return sm.databaseService } -// GetSQLiteService 获取SQLite服务实例 -func (sm *ServiceManager) GetSQLiteService() *sqlite.Service { - return sm.sqliteService -} - // GetWindowService 获取窗口服务实例 func (sm *ServiceManager) GetWindowService() *WindowService { return sm.windowService