♻️ Refactor backup service complete.
Some checks failed
CodeQL Advanced / Analyze (go) (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (c-cpp) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
CodeQL Advanced / Analyze (rust) (push) Has been cancelled

This commit is contained in:
2025-12-16 23:20:40 +08:00
parent 1ab934cee9
commit 8fce8bdca4
29 changed files with 467 additions and 1171 deletions

View File

@@ -61,5 +61,3 @@ export class ServiceOptions {
return new ServiceOptions($$parsedSource as Partial<ServiceOptions>);
}
}
export type Window = any;

View File

@@ -0,0 +1,4 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export * from "./models.js";

View File

@@ -0,0 +1,17 @@
// 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";
/**
* CancelFunc 取消订阅函数
* 调用此函数可以取消对配置的监听
*/
export type CancelFunc = any;
/**
* ObserverCallback 观察者回调函数
*/
export type ObserverCallback = any;

View File

@@ -21,7 +21,7 @@ export class Document {
/**
* UUID for cross-device sync (UUIDv7)
*/
"uuid": string;
"uuid": string | null;
/**
* creation time
@@ -56,7 +56,7 @@ export class Document {
/** Creates a new Document instance. */
constructor($$source: Partial<Document> = {}) {
if (!("uuid" in $$source)) {
this["uuid"] = "";
this["uuid"] = null;
}
if (!("created_at" in $$source)) {
this["created_at"] = "";
@@ -98,7 +98,7 @@ export class Extension {
/**
* UUID for cross-device sync (UUIDv7)
*/
"uuid": string;
"uuid": string | null;
/**
* creation time
@@ -133,7 +133,7 @@ export class Extension {
/** Creates a new Extension instance. */
constructor($$source: Partial<Extension> = {}) {
if (!("uuid" in $$source)) {
this["uuid"] = "";
this["uuid"] = null;
}
if (!("created_at" in $$source)) {
this["created_at"] = "";
@@ -179,7 +179,7 @@ export class KeyBinding {
/**
* UUID for cross-device sync (UUIDv7)
*/
"uuid": string;
"uuid": string | null;
/**
* creation time
@@ -219,7 +219,7 @@ export class KeyBinding {
/** Creates a new KeyBinding instance. */
constructor($$source: Partial<KeyBinding> = {}) {
if (!("uuid" in $$source)) {
this["uuid"] = "";
this["uuid"] = null;
}
if (!("created_at" in $$source)) {
this["created_at"] = "";
@@ -261,7 +261,7 @@ export class Theme {
/**
* UUID for cross-device sync (UUIDv7)
*/
"uuid": string;
"uuid": string | null;
/**
* creation time
@@ -296,7 +296,7 @@ export class Theme {
/** Creates a new Theme instance. */
constructor($$source: Partial<Theme> = {}) {
if (!("uuid" in $$source)) {
this["uuid"] = "";
this["uuid"] = null;
}
if (!("created_at" in $$source)) {
this["created_at"] = "";

View File

@@ -15,11 +15,10 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as models$0 from "../models/models.js";
import * as helper$0 from "../common/helper/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as $models from "./models.js";
import * as models$0 from "../models/models.js";
/**
* Get 获取配置项
@@ -50,7 +49,7 @@ export function MigrateConfig(): Promise<void> & { cancel(): void } {
}
/**
* ResetConfig 强制重置所有配置为默认值
* ResetConfig 重置所有配置为默认值
*/
export function ResetConfig(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3593047389) as any;
@@ -66,7 +65,7 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
}
/**
* ServiceStartup initializes the service when the application starts
* ServiceStartup 服务启动时初始化
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3311949428, options) as any;
@@ -84,7 +83,7 @@ export function Set(key: string, value: any): Promise<void> & { cancel(): void }
/**
* Watch 注册配置变更监听器
*/
export function Watch(path: string, callback: $models.ObserverCallback): Promise<$models.CancelFunc> & { cancel(): void } {
export function Watch(path: string, callback: helper$0.ObserverCallback): Promise<helper$0.CancelFunc> & { cancel(): void } {
let $resultPromise = $Call.ByID(1143583035, path, callback) as any;
return $resultPromise;
}
@@ -92,7 +91,7 @@ export function Watch(path: string, callback: $models.ObserverCallback): Promise
/**
* WatchWithContext 使用 Context 注册监听器
*/
export function WatchWithContext(path: string, callback: $models.ObserverCallback): Promise<void> & { cancel(): void } {
export function WatchWithContext(path: string, callback: helper$0.ObserverCallback): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1454973098, path, callback) as any;
return $resultPromise;
}

View File

@@ -18,12 +18,12 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
import * as models$0 from "../models/models.js";
/**
* GetCurrentHotkey 获取当前热键
* GetSupportedKeys 返回系统支持的快捷键列表
*/
export function GetCurrentHotkey(): Promise<models$0.HotkeyCombo | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(2572811187) as any;
export function GetSupportedKeys(): Promise<string[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(1511528650) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
return $$createType0($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
@@ -86,5 +86,4 @@ export function UpdateHotkey(enable: boolean, combo: models$0.HotkeyCombo | null
}
// Private type creation functions
const $$createType0 = models$0.HotkeyCombo.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType0 = $Create.Array($Create.Any);

View File

@@ -12,12 +12,6 @@ import * as http$0 from "../../../net/http/models.js";
// @ts-ignore: Unused imports
import * as time$0 from "../../../time/models.js";
/**
* CancelFunc 取消订阅函数
* 调用此函数可以取消对配置的监听
*/
export type CancelFunc = any;
/**
* HttpRequest HTTP请求结构
*/
@@ -251,11 +245,6 @@ export class OSInfo {
}
}
/**
* ObserverCallback 观察者回调函数
*/
export type ObserverCallback = any;
/**
* SelfUpdateResult 自我更新结果
*/

View File

@@ -14,18 +14,6 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// @ts-ignore: Unused imports
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
/**
* GetOpenWindows 获取所有打开的文档窗口
*/
export function GetOpenWindows(): Promise<application$0.Window[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(1464997251) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType0($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* IsDocumentWindowOpen 检查指定文档的窗口是否已打开
*/
@@ -57,6 +45,3 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
let $resultPromise = $Call.ByID(2432987694, options) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = $Create.Array($Create.Any);

View File

@@ -71,7 +71,7 @@
"@lezer/generator": "^1.8.0",
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.2",
"@wailsio/runtime": "*",
"@wailsio/runtime": "latest",
"cross-env": "^10.1.0",
"eslint": "^9.39.1",
"eslint-plugin-vue": "^10.6.2",
@@ -142,7 +142,6 @@
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz",
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/types": "^7.28.5"
},
@@ -216,7 +215,6 @@
"resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz",
"integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
@@ -265,7 +263,6 @@
"resolved": "https://registry.npmmirror.com/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
@@ -292,7 +289,6 @@
"resolved": "https://registry.npmmirror.com/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
@@ -320,7 +316,6 @@
"resolved": "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
"integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
@@ -533,7 +528,6 @@
"resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.11.3.tgz",
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
@@ -610,7 +604,6 @@
"resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
@@ -620,7 +613,6 @@
"resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.38.8.tgz",
"integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
@@ -1395,7 +1387,7 @@
"version": "0.3.13",
"resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -1406,7 +1398,7 @@
"version": "2.3.5",
"resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -1417,7 +1409,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -1433,7 +1425,7 @@
"version": "0.3.31",
"resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -1444,8 +1436,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.3.0.tgz",
"integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@lezer/cpp": {
"version": "1.1.3",
@@ -1499,7 +1490,6 @@
"resolved": "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@lezer/common": "^1.3.0"
}
@@ -1531,7 +1521,6 @@
"resolved": "https://registry.npmmirror.com/@lezer/javascript/-/javascript-1.5.1.tgz",
"integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3",
@@ -1564,7 +1553,6 @@
"resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.4.tgz",
"integrity": "sha512-LHL17Mq0OcFXm1pGQssuGTQFPPdxARjKM8f7GA5+sGtHi0K3R84YaSbmche0+RKWHnCsx9asEe5OWOI4FHfe4A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@lezer/common": "^1.0.0"
}
@@ -2773,7 +2761,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/@types/geojson": {
@@ -2846,7 +2834,6 @@
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -2925,7 +2912,6 @@
"integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.48.0",
"@typescript-eslint/types": "8.48.0",
@@ -3619,7 +3605,6 @@
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4008,35 +3993,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/c12": {
"version": "3.0.4",
"resolved": "https://registry.npmmirror.com/c12/-/c12-3.0.4.tgz",
"integrity": "sha512-t5FaZTYbbCtvxuZq9xxIruYydrAGsJ+8UdP0pZzMiK2xl/gNiSOy0OxhLzHUEEb0m1QXYqfzfvyIFEmz/g9lqg==",
"license": "MIT",
"optional": true,
"dependencies": {
"chokidar": "^4.0.3",
"confbox": "^0.2.2",
"defu": "^6.1.4",
"dotenv": "^16.5.0",
"exsolve": "^1.0.5",
"giget": "^2.0.0",
"jiti": "^2.4.2",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"perfect-debounce": "^1.0.0",
"pkg-types": "^2.1.0",
"rc9": "^2.1.2"
},
"peerDependencies": {
"magicast": "^0.3.5"
},
"peerDependenciesMeta": {
"magicast": {
"optional": true
}
}
},
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz",
@@ -4162,7 +4118,6 @@
"resolved": "https://registry.npmmirror.com/chevrotain/-/chevrotain-11.0.3.tgz",
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@chevrotain/cst-dts-gen": "11.0.3",
"@chevrotain/gast": "11.0.3",
@@ -4213,16 +4168,6 @@
"node": ">= 0.10"
}
},
"node_modules/citty": {
"version": "0.1.6",
"resolved": "https://registry.npmmirror.com/citty/-/citty-0.1.6.tgz",
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"consola": "^3.2.3"
}
},
"node_modules/codemirror": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/codemirror/-/codemirror-6.0.2.tgz",
@@ -4324,16 +4269,6 @@
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
"license": "MIT"
},
"node_modules/consola": {
"version": "3.4.2",
"resolved": "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz",
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
"license": "MIT",
"optional": true,
"engines": {
"node": "^14.18.0 || >=16.10.0"
}
},
"node_modules/console-browserify": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/console-browserify/-/console-browserify-1.2.0.tgz",
@@ -4522,7 +4457,6 @@
"resolved": "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.33.1.tgz",
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10"
}
@@ -4932,7 +4866,6 @@
"resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -5129,13 +5062,6 @@
"minimalistic-assert": "^1.0.0"
}
},
"node_modules/destr": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz",
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
"license": "MIT",
"optional": true
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz",
@@ -5204,19 +5130,6 @@
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dotenv": {
"version": "16.5.0",
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.5.0.tgz",
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
"license": "BSD-2-Clause",
"optional": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -5267,13 +5180,6 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/errx": {
"version": "0.1.0",
"resolved": "https://registry.npmmirror.com/errx/-/errx-0.1.0.tgz",
"integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==",
"license": "MIT",
"optional": true
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -5374,7 +5280,6 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5689,7 +5594,6 @@
"integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"tabbable": "^6.3.0"
}
@@ -5774,24 +5678,6 @@
"node": ">= 0.4"
}
},
"node_modules/giget": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/giget/-/giget-2.0.0.tgz",
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
"license": "MIT",
"optional": true,
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.4.0",
"defu": "^6.1.4",
"node-fetch-native": "^1.6.6",
"nypm": "^0.6.0",
"pathe": "^2.0.3"
},
"bin": {
"giget": "dist/cli.mjs"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -6351,19 +6237,14 @@
"version": "2.4.2",
"resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.4.2.tgz",
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-tokens": {
"version": "9.0.1",
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz",
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
"license": "MIT",
"optional": true
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
@@ -6429,23 +6310,6 @@
"resolved": "https://registry.npmmirror.com/khroma/-/khroma-2.1.0.tgz",
"integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
},
"node_modules/klona": {
"version": "2.0.6",
"resolved": "https://registry.npmmirror.com/klona/-/klona-2.0.6.tgz",
"integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/knitwork": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/knitwork/-/knitwork-1.2.0.tgz",
"integrity": "sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==",
"license": "MIT",
"optional": true
},
"node_modules/kolorist": {
"version": "1.8.0",
"resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz",
@@ -6916,13 +6780,6 @@
"license": "MIT",
"optional": true
},
"node_modules/node-fetch-native": {
"version": "1.6.6",
"resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.6.tgz",
"integrity": "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==",
"license": "MIT",
"optional": true
},
"node_modules/node-stdlib-browser": {
"version": "1.3.1",
"resolved": "https://registry.npmmirror.com/node-stdlib-browser/-/node-stdlib-browser-1.3.1.tgz",
@@ -6982,26 +6839,6 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/nypm": {
"version": "0.6.0",
"resolved": "https://registry.npmmirror.com/nypm/-/nypm-0.6.0.tgz",
"integrity": "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==",
"license": "MIT",
"optional": true,
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.4.0",
"pathe": "^2.0.3",
"pkg-types": "^2.0.0",
"tinyexec": "^0.3.2"
},
"bin": {
"nypm": "dist/cli.mjs"
},
"engines": {
"node": "^14.16.0 || >=16.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -7074,13 +6911,6 @@
],
"license": "MIT"
},
"node_modules/ohash": {
"version": "2.0.11",
"resolved": "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz",
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"license": "MIT",
"optional": true
},
"node_modules/oniguruma-parser": {
"version": "0.12.1",
"resolved": "https://registry.npmmirror.com/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
@@ -7335,7 +7165,6 @@
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz",
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/devtools-api": "^7.7.7"
},
@@ -7484,7 +7313,6 @@
"resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.7.2.tgz",
"integrity": "sha512-n3HV2J6QhItCXndGa3oMWvWFAgN1ibnS7R9mt6iokScBOC0Ul9/iZORmU2IWUMcyAQaMPjTlY3uT34TqocUxMA==",
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -7617,17 +7445,6 @@
"safe-buffer": "^5.1.0"
}
},
"node_modules/rc9": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/rc9/-/rc9-2.1.2.tgz",
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
"license": "MIT",
"optional": true,
"dependencies": {
"defu": "^6.1.4",
"destr": "^2.0.3"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -7749,7 +7566,6 @@
"integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -7852,7 +7668,6 @@
"resolved": "https://registry.npmmirror.com/sass/-/sass-1.94.2.tgz",
"integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==",
"license": "MIT",
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@@ -7868,18 +7683,11 @@
"@parcel/watcher": "^2.4.1"
}
},
"node_modules/scule": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz",
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
"license": "MIT",
"optional": true
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"devOptional": true,
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -8121,7 +7929,7 @@
"version": "3.10.0",
"resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz",
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/stream-browserify": {
@@ -8186,19 +7994,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-literal": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-3.0.0.tgz",
"integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
"license": "MIT",
"optional": true,
"dependencies": {
"js-tokens": "^9.0.1"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.2.tgz",
@@ -8280,14 +8075,14 @@
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-0.3.2.tgz",
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -8304,7 +8099,7 @@
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -8322,9 +8117,8 @@
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -8444,7 +8238,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -8483,29 +8276,6 @@
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
"license": "MIT"
},
"node_modules/unctx": {
"version": "2.4.1",
"resolved": "https://registry.npmmirror.com/unctx/-/unctx-2.4.1.tgz",
"integrity": "sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg==",
"license": "MIT",
"optional": true,
"dependencies": {
"acorn": "^8.14.0",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.17",
"unplugin": "^2.1.0"
}
},
"node_modules/unctx/node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"license": "MIT",
"optional": true,
"dependencies": {
"@types/estree": "^1.0.0"
}
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz",
@@ -8513,68 +8283,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/unimport": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/unimport/-/unimport-5.0.1.tgz",
"integrity": "sha512-1YWzPj6wYhtwHE+9LxRlyqP4DiRrhGfJxdtH475im8ktyZXO3jHj/3PZ97zDdvkYoovFdi0K4SKl3a7l92v3sQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"acorn": "^8.14.1",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
"local-pkg": "^1.1.1",
"magic-string": "^0.30.17",
"mlly": "^1.7.4",
"pathe": "^2.0.3",
"picomatch": "^4.0.2",
"pkg-types": "^2.1.0",
"scule": "^1.3.0",
"strip-literal": "^3.0.0",
"tinyglobby": "^0.2.13",
"unplugin": "^2.3.2",
"unplugin-utils": "^0.2.4"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/unimport/node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/unimport/node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"license": "MIT",
"optional": true,
"dependencies": {
"@types/estree": "^1.0.0"
}
},
"node_modules/unimport/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/unist-util-is": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/unist-util-is/-/unist-util-is-6.0.1.tgz",
@@ -8652,7 +8360,7 @@
"version": "2.3.10",
"resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-2.3.10.tgz",
"integrity": "sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
@@ -8664,36 +8372,6 @@
"node": ">=18.12.0"
}
},
"node_modules/unplugin-utils": {
"version": "0.2.4",
"resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.2.4.tgz",
"integrity": "sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA==",
"license": "MIT",
"optional": true,
"dependencies": {
"pathe": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=18.12.0"
},
"funding": {
"url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/unplugin-utils/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/unplugin-vue-components": {
"version": "30.0.0",
"resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-30.0.0.tgz",
@@ -8764,7 +8442,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -8773,23 +8451,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/untyped": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/untyped/-/untyped-2.0.0.tgz",
"integrity": "sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==",
"license": "MIT",
"optional": true,
"dependencies": {
"citty": "^0.1.6",
"defu": "^6.1.4",
"jiti": "^2.4.2",
"knitwork": "^1.2.0",
"scule": "^1.3.0"
},
"bin": {
"untyped": "dist/cli.mjs"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz",
@@ -8891,7 +8552,6 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -9002,7 +8662,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -9252,7 +8911,6 @@
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.25.tgz",
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.25",
"@vue/compiler-sfc": "3.5.25",
@@ -9275,7 +8933,6 @@
"integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"debug": "^4.4.0",
"eslint-scope": "^8.2.0",
@@ -9381,7 +9038,7 @@
"version": "0.6.2",
"resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/whatwg-mimetype": {

View File

@@ -248,7 +248,7 @@ export const useConfigStore = defineStore('config', () => {
setFontWeight: (value: string) => updateConfig('fontWeight', value),
// 路径操作
setDataPath: (value: string) => updateConfig('dataPath', value),
setDataPath: (value: string) => updateConfigLocal('dataPath', value),
// 保存配置相关方法
setAutoSaveDelay: (value: number) => updateConfig('autoSaveDelay', value),

View File

@@ -2,16 +2,28 @@
import {useConfigStore} from '@/stores/configStore';
import {useTabStore} from '@/stores/tabStore';
import {useI18n} from 'vue-i18n';
import {computed, ref} from 'vue';
import {computed, ref, onMounted} from 'vue';
import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import ToggleSwitch from '../components/ToggleSwitch.vue';
import {DialogService, MigrationService} from '@/../bindings/voidraft/internal/services';
import {DialogService, HotkeyService, MigrationService} from '@/../bindings/voidraft/internal/services';
import {useSystemStore} from "@/stores/systemStore";
import {useConfirm, usePolling} from '@/composables';
const {t} = useI18n();
const configStore = useConfigStore();
const {
config: {general},
resetConfig,
setAlwaysOnTop,
setDataPath,
setEnableGlobalHotkey,
setEnableLoadingAnimation,
setEnableSystemTray,
setEnableTabs,
setEnableWindowSnap,
setGlobalHotkey,
setStartAtLogin
} = useConfigStore();
const systemStore = useSystemStore();
const tabStore = useTabStore();
@@ -60,57 +72,57 @@ const hideAll = () => {
const {isConfirming: isResetConfirming, requestConfirm: requestResetConfirm} = useConfirm({
timeout: 3000,
onConfirm: async () => {
await configStore.resetConfig();
await resetConfig();
}
});
// 可选键列表
const keyOptions = [
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12'
];
// 可选键列表 - 从后端获取系统支持的快捷键
const keyOptions = ref<string[]>([]);
// 初始化时从后端获取支持的键列表
onMounted(async () => {
keyOptions.value = await HotkeyService.GetSupportedKeys();
});
// 计算属性 - 启用全局热键
const enableGlobalHotkey = computed({
get: () => configStore.config.general.enableGlobalHotkey,
set: (value: boolean) => configStore.setEnableGlobalHotkey(value)
get: () => general.enableGlobalHotkey,
set: (value: boolean) => setEnableGlobalHotkey(value)
});
// 计算属性 - 窗口始终置顶
const alwaysOnTop = computed({
get: () => configStore.config.general.alwaysOnTop,
get: () => general.alwaysOnTop,
set: async (value: boolean) => {
// 先更新配置
await configStore.setAlwaysOnTop(value);
await setAlwaysOnTop(value);
await systemStore.setWindowOnTop(value);
}
});
// 计算属性 - 启用系统托盘
const enableSystemTray = computed({
get: () => configStore.config.general.enableSystemTray,
set: (value: boolean) => configStore.setEnableSystemTray(value)
get: () => general.enableSystemTray,
set: (value: boolean) => setEnableSystemTray(value)
});
// 计算属性 - 启用窗口吸附
const enableWindowSnap = computed({
get: () => configStore.config.general.enableWindowSnap,
set: (value: boolean) => configStore.setEnableWindowSnap(value)
get: () => general.enableWindowSnap,
set: (value: boolean) => setEnableWindowSnap(value)
});
// 计算属性 - 启用加载动画
const enableLoadingAnimation = computed({
get: () => configStore.config.general.enableLoadingAnimation,
set: (value: boolean) => configStore.setEnableLoadingAnimation(value)
get: () => general.enableLoadingAnimation,
set: (value: boolean) => setEnableLoadingAnimation(value)
});
// 计算属性 - 启用标签页
const enableTabs = computed({
get: () => configStore.config.general.enableTabs,
get: () => general.enableTabs,
set: async (value: boolean) => {
await configStore.setEnableTabs(value);
await setEnableTabs(value);
if (value) {
// 开启tabs功能时初始化当前文档到标签页
tabStore.initializeTab();
@@ -123,33 +135,33 @@ const enableTabs = computed({
// 计算属性 - 开机启动
const startAtLogin = computed({
get: () => configStore.config.general.startAtLogin,
set: (value: boolean) => configStore.setStartAtLogin(value)
get: () => general.startAtLogin,
set: (value: boolean) => setStartAtLogin(value)
});
// 修饰键配置 - 只读计算属性
const modifierKeys = computed(() => ({
ctrl: configStore.config.general.globalHotkey.ctrl,
shift: configStore.config.general.globalHotkey.shift,
alt: configStore.config.general.globalHotkey.alt,
win: configStore.config.general.globalHotkey.win
ctrl: general.globalHotkey.ctrl,
shift: general.globalHotkey.shift,
alt: general.globalHotkey.alt,
win: general.globalHotkey.win
}));
// 主键配置
const selectedKey = computed(() => configStore.config.general.globalHotkey.key);
const selectedKey = computed(() => general.globalHotkey.key);
// 切换修饰键
const toggleModifier = (key: 'ctrl' | 'shift' | 'alt' | 'win') => {
const currentHotkey = configStore.config.general.globalHotkey;
const currentHotkey = general.globalHotkey;
const newHotkey = {...currentHotkey, [key]: !currentHotkey[key]};
configStore.setGlobalHotkey(newHotkey);
setGlobalHotkey(newHotkey);
};
// 更新选择的键
const updateSelectedKey = (event: Event) => {
const select = event.target as HTMLSelectElement;
const newHotkey = {...configStore.config.general.globalHotkey, key: select.value};
configStore.setGlobalHotkey(newHotkey);
const newHotkey = {...general.globalHotkey, key: select.value};
setGlobalHotkey(newHotkey);
};
@@ -157,7 +169,7 @@ const updateSelectedKey = (event: Event) => {
const hotkeyPreview = computed(() => {
if (!enableGlobalHotkey.value) return '';
const {ctrl, shift, alt, win, key} = configStore.config.general.globalHotkey;
const {ctrl, shift, alt, win, key} = general.globalHotkey;
const modifiers = [
ctrl && 'Ctrl',
shift && 'Shift',
@@ -170,7 +182,7 @@ const hotkeyPreview = computed(() => {
});
// 数据路径配置
const currentDataPath = computed(() => configStore.config.general.dataPath);
const currentDataPath = computed(() => general.dataPath);
// 选择数据存储目录
const selectDataDirectory = async () => {
@@ -189,7 +201,7 @@ const selectDataDirectory = async () => {
try {
await MigrationService.MigrateDirectory(oldPath, newPath);
await configStore.setDataPath(newPath);
await setDataPath(newPath);
} catch (e) {
stop();
// 设置手动捕获的错误(当轮询还没获取到错误时)
@@ -314,10 +326,6 @@ const selectDataDirectory = async () => {
</template>
<style scoped lang="scss">
.settings-page {
//max-width: 800px;
}
.hotkey-selector {
padding: 15px 0 5px 20px;
transition: all 0.3s ease;

View File

@@ -1,4 +1,4 @@
package services
package helper
import (
"context"
@@ -10,6 +10,8 @@ import (
"github.com/wailsapp/wails/v3/pkg/services/log"
)
const pathSeparator = '.'
// ObserverCallback 观察者回调函数
type ObserverCallback func(oldValue, newValue interface{})
@@ -49,6 +51,8 @@ func NewConfigObserver(logger *log.LogService) *ConfigObserver {
}
// Watch 注册配置变更监听器
// 支持前缀监听:注册 "generate" 可以监听 "generate.xxx"、"generate.yyy" 等所有子路径的变化
// 返回取消函数,调用后停止监听
func (co *ConfigObserver) Watch(path string, callback ObserverCallback) CancelFunc {
// 生成唯一ID
id := fmt.Sprintf("obs_%d", co.nextObserverID.Add(1))
@@ -103,36 +107,91 @@ func (co *ConfigObserver) removeObserver(path, id string) {
}
}
// Notify 通知指定路径的所有观察者
// Notify 通知指定路径及其所有父路径的观察者
// 支持前缀监听:当 "generate.xxx" 变化时,同时通知监听 "generate" 的观察者
// 通知顺序:精确匹配 -> 父路径(从近到远)
func (co *ConfigObserver) Notify(path string, oldValue, newValue interface{}) {
// 获取该路径的所有观察者(拷贝以避免并发问题)
co.observerMu.RLock()
observers := co.observers[path]
if len(observers) == 0 {
co.observerMu.RUnlock()
callbacks := co.collectCallbacks(path)
co.observerMu.RUnlock()
if len(callbacks) == 0 {
return
}
// 拷贝观察者列表
callbacks := make([]ObserverCallback, len(observers))
for i, obs := range observers {
callbacks[i] = obs.callback
}
co.observerMu.RUnlock()
// 在独立 goroutine 中执行回调
// 执行所有回调
for _, callback := range callbacks {
co.executeCallback(callback, oldValue, newValue)
}
}
// NotifyAll 通知所有匹配前缀的观察者
// collectCallbacks 收集指定路径及其所有父路径的观察者回调
// 调用者必须持有读锁
func (co *ConfigObserver) collectCallbacks(path string) []ObserverCallback {
if path == "" {
return nil
}
var callbacks []ObserverCallback
// 1. 收集精确匹配的观察者
if observers := co.observers[path]; len(observers) > 0 {
callbacks = make([]ObserverCallback, 0, len(observers)*2)
for _, obs := range observers {
callbacks = append(callbacks, obs.callback)
}
}
// 2. 收集父路径的观察者(从后向前遍历,避免 strings.Split 的内存分配)
for i := len(path) - 1; i >= 0; i-- {
if path[i] == pathSeparator {
parentPath := path[:i]
if observers := co.observers[parentPath]; len(observers) > 0 {
if callbacks == nil {
callbacks = make([]ObserverCallback, 0, len(observers))
}
for _, obs := range observers {
callbacks = append(callbacks, obs.callback)
}
}
}
}
return callbacks
}
// NotifyAll 批量通知所有匹配路径的观察者
func (co *ConfigObserver) NotifyAll(changes map[string]struct {
OldValue interface{}
NewValue interface{}
}) {
if len(changes) == 0 {
return
}
type callbackTask struct {
callback ObserverCallback
oldValue interface{}
newValue interface{}
}
// 只获取一次读锁,收集所有回调
co.observerMu.RLock()
var tasks []callbackTask
for path, change := range changes {
co.Notify(path, change.OldValue, change.NewValue)
for _, cb := range co.collectCallbacks(path) {
tasks = append(tasks, callbackTask{
callback: cb,
oldValue: change.OldValue,
newValue: change.NewValue,
})
}
}
co.observerMu.RUnlock()
// 执行所有回调
for _, task := range tasks {
co.executeCallback(task.callback, task.oldValue, task.newValue)
}
}

View File

@@ -17,7 +17,7 @@ type Document struct {
// ID of the ent.
ID int `json:"id,omitempty"`
// UUID for cross-device sync (UUIDv7)
UUID string `json:"uuid"`
UUID *string `json:"uuid"`
// creation time
CreatedAt string `json:"created_at"`
// update time
@@ -69,7 +69,8 @@ func (_m *Document) assignValues(columns []string, values []any) error {
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field uuid", values[i])
} else if value.Valid {
_m.UUID = value.String
_m.UUID = new(string)
*_m.UUID = value.String
}
case document.FieldCreatedAt:
if value, ok := values[i].(*sql.NullString); !ok {
@@ -144,8 +145,10 @@ func (_m *Document) String() string {
var builder strings.Builder
builder.WriteString("Document(")
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
builder.WriteString("uuid=")
builder.WriteString(_m.UUID)
if v := _m.UUID; v != nil {
builder.WriteString("uuid=")
builder.WriteString(*v)
}
builder.WriteString(", ")
builder.WriteString("created_at=")
builder.WriteString(_m.CreatedAt)

View File

@@ -225,7 +225,7 @@ func (_c *DocumentCreate) createSpec() (*Document, *sqlgraph.CreateSpec) {
)
if value, ok := _c.mutation.UUID(); ok {
_spec.SetField(document.FieldUUID, field.TypeString, value)
_node.UUID = value
_node.UUID = &value
}
if value, ok := _c.mutation.CreatedAt(); ok {
_spec.SetField(document.FieldCreatedAt, field.TypeString, value)

View File

@@ -18,7 +18,7 @@ type Extension struct {
// ID of the ent.
ID int `json:"id,omitempty"`
// UUID for cross-device sync (UUIDv7)
UUID string `json:"uuid"`
UUID *string `json:"uuid"`
// creation time
CreatedAt string `json:"created_at"`
// update time
@@ -72,7 +72,8 @@ func (_m *Extension) assignValues(columns []string, values []any) error {
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field uuid", values[i])
} else if value.Valid {
_m.UUID = value.String
_m.UUID = new(string)
*_m.UUID = value.String
}
case extension.FieldCreatedAt:
if value, ok := values[i].(*sql.NullString); !ok {
@@ -149,8 +150,10 @@ func (_m *Extension) String() string {
var builder strings.Builder
builder.WriteString("Extension(")
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
builder.WriteString("uuid=")
builder.WriteString(_m.UUID)
if v := _m.UUID; v != nil {
builder.WriteString("uuid=")
builder.WriteString(*v)
}
builder.WriteString(", ")
builder.WriteString("created_at=")
builder.WriteString(_m.CreatedAt)

View File

@@ -213,7 +213,7 @@ func (_c *ExtensionCreate) createSpec() (*Extension, *sqlgraph.CreateSpec) {
)
if value, ok := _c.mutation.UUID(); ok {
_spec.SetField(extension.FieldUUID, field.TypeString, value)
_node.UUID = value
_node.UUID = &value
}
if value, ok := _c.mutation.CreatedAt(); ok {
_spec.SetField(extension.FieldCreatedAt, field.TypeString, value)

View File

@@ -17,7 +17,7 @@ type KeyBinding struct {
// ID of the ent.
ID int `json:"id,omitempty"`
// UUID for cross-device sync (UUIDv7)
UUID string `json:"uuid"`
UUID *string `json:"uuid"`
// creation time
CreatedAt string `json:"created_at"`
// update time
@@ -71,7 +71,8 @@ func (_m *KeyBinding) assignValues(columns []string, values []any) error {
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field uuid", values[i])
} else if value.Valid {
_m.UUID = value.String
_m.UUID = new(string)
*_m.UUID = value.String
}
case keybinding.FieldCreatedAt:
if value, ok := values[i].(*sql.NullString); !ok {
@@ -152,8 +153,10 @@ func (_m *KeyBinding) String() string {
var builder strings.Builder
builder.WriteString("KeyBinding(")
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
builder.WriteString("uuid=")
builder.WriteString(_m.UUID)
if v := _m.UUID; v != nil {
builder.WriteString("uuid=")
builder.WriteString(*v)
}
builder.WriteString(", ")
builder.WriteString("created_at=")
builder.WriteString(_m.CreatedAt)

View File

@@ -240,7 +240,7 @@ func (_c *KeyBindingCreate) createSpec() (*KeyBinding, *sqlgraph.CreateSpec) {
)
if value, ok := _c.mutation.UUID(); ok {
_spec.SetField(keybinding.FieldUUID, field.TypeString, value)
_node.UUID = value
_node.UUID = &value
}
if value, ok := _c.mutation.CreatedAt(); ok {
_spec.SetField(keybinding.FieldCreatedAt, field.TypeString, value)

View File

@@ -166,7 +166,7 @@ func (m *DocumentMutation) UUID() (r string, exists bool) {
// OldUUID returns the old "uuid" field's value of the Document entity.
// If the Document object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *DocumentMutation) OldUUID(ctx context.Context) (v string, err error) {
func (m *DocumentMutation) OldUUID(ctx context.Context) (v *string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldUUID is only allowed on UpdateOne operations")
}
@@ -876,7 +876,7 @@ func (m *ExtensionMutation) UUID() (r string, exists bool) {
// OldUUID returns the old "uuid" field's value of the Extension entity.
// If the Extension object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ExtensionMutation) OldUUID(ctx context.Context) (v string, err error) {
func (m *ExtensionMutation) OldUUID(ctx context.Context) (v *string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldUUID is only allowed on UpdateOne operations")
}
@@ -1587,7 +1587,7 @@ func (m *KeyBindingMutation) UUID() (r string, exists bool) {
// OldUUID returns the old "uuid" field's value of the KeyBinding entity.
// If the KeyBinding object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *KeyBindingMutation) OldUUID(ctx context.Context) (v string, err error) {
func (m *KeyBindingMutation) OldUUID(ctx context.Context) (v *string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldUUID is only allowed on UpdateOne operations")
}
@@ -2350,7 +2350,7 @@ func (m *ThemeMutation) UUID() (r string, exists bool) {
// OldUUID returns the old "uuid" field's value of the Theme entity.
// If the Theme object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ThemeMutation) OldUUID(ctx context.Context) (v string, err error) {
func (m *ThemeMutation) OldUUID(ctx context.Context) (v *string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldUUID is only allowed on UpdateOne operations")
}

View File

@@ -18,7 +18,7 @@ type Theme struct {
// ID of the ent.
ID int `json:"id,omitempty"`
// UUID for cross-device sync (UUIDv7)
UUID string `json:"uuid"`
UUID *string `json:"uuid"`
// creation time
CreatedAt string `json:"created_at"`
// update time
@@ -70,7 +70,8 @@ func (_m *Theme) assignValues(columns []string, values []any) error {
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field uuid", values[i])
} else if value.Valid {
_m.UUID = value.String
_m.UUID = new(string)
*_m.UUID = value.String
}
case theme.FieldCreatedAt:
if value, ok := values[i].(*sql.NullString); !ok {
@@ -147,8 +148,10 @@ func (_m *Theme) String() string {
var builder strings.Builder
builder.WriteString("Theme(")
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
builder.WriteString("uuid=")
builder.WriteString(_m.UUID)
if v := _m.UUID; v != nil {
builder.WriteString("uuid=")
builder.WriteString(*v)
}
builder.WriteString(", ")
builder.WriteString("created_at=")
builder.WriteString(_m.CreatedAt)

View File

@@ -206,7 +206,7 @@ func (_c *ThemeCreate) createSpec() (*Theme, *sqlgraph.CreateSpec) {
)
if value, ok := _c.mutation.UUID(); ok {
_spec.SetField(theme.FieldUUID, field.TypeString, value)
_node.UUID = value
_node.UUID = &value
}
if value, ok := _c.mutation.CreatedAt(); ok {
_spec.SetField(theme.FieldCreatedAt, field.TypeString, value)

View File

@@ -11,6 +11,7 @@ import (
"strings"
"sync"
"time"
"voidraft/internal/common/helper"
"github.com/go-git/go-git/v5"
gitConfig "github.com/go-git/go-git/v5/config"
@@ -61,7 +62,7 @@ type BackupService struct {
autoBackupStop chan bool
autoBackupWg sync.WaitGroup
mu sync.Mutex
cancelObserver CancelFunc
cancelObservers []helper.CancelFunc
}
// NewBackupService 创建新的备份服务实例
@@ -74,7 +75,11 @@ func NewBackupService(configService *ConfigService, dbService *DatabaseService,
}
func (s *BackupService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
s.cancelObserver = s.configService.Watch("backup.enabled", s.onBackupConfigChange)
// 监听 backup 配置变化
s.cancelObservers = []helper.CancelFunc{
s.configService.Watch("backup", s.onBackupConfigChange),
s.configService.Watch("general.dataPath", s.onDataPathChange),
}
if err := s.Initialize(); err != nil {
s.logger.Error("initializing backup service: %v", err)
}
@@ -89,6 +94,12 @@ func (s *BackupService) onBackupConfigChange(oldValue, newValue interface{}) {
_ = s.HandleConfigChange(&config.Backup)
}
func (s *BackupService) onDataPathChange(oldValue, newValue interface{}) {
if err := s.Reinitialize(); err != nil {
s.logger.Error("Failed to reinitialize backup service after data path change: %v", err)
}
}
// Initialize 初始化备份服务
func (s *BackupService) Initialize() error {
config, repoPath, err := s.getConfigAndPath()
@@ -100,7 +111,7 @@ func (s *BackupService) Initialize() error {
return nil
}
// 仓库地址为空时不初始化(等待用户配置)
// 仓库地址为空时不初始化
if strings.TrimSpace(config.RepoURL) == "" {
return nil
}
@@ -1196,8 +1207,10 @@ func (s *BackupService) HandleConfigChange(config *models.GitBackupConfig) error
// ServiceShutdown 服务关闭
func (s *BackupService) ServiceShutdown() {
if s.cancelObserver != nil {
s.cancelObserver()
for _, cancel := range s.cancelObservers {
if cancel != nil {
cancel()
}
}
s.StopAutoBackup()
}

View File

@@ -2,33 +2,31 @@ package services
import (
"context"
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v3/pkg/application"
"os"
"path/filepath"
"strings"
"reflect"
"sync"
"time"
"voidraft/internal/common/helper"
"voidraft/internal/models"
jsonparser "github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/structs"
"github.com/knadh/koanf/v2"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/services/log"
)
// ConfigService 应用配置服务
type ConfigService struct {
koanf *koanf.Koanf // koanf 实例
logger *log.LogService // 日志服务
configDir string // 配置目录
settingsPath string // 设置文件路径
mu sync.RWMutex // 读写锁
fileProvider *file.File // 文件提供器,用于监听
observer *ConfigObserver
koanf *koanf.Koanf
logger *log.LogService
configDir string
settingsPath string
mu sync.RWMutex
observer *helper.ConfigObserver
// 配置迁移器
configMigrator *ConfigMigrator
@@ -36,49 +34,29 @@ type ConfigService struct {
// NewConfigService 创建新的配置服务实例
func NewConfigService(logger *log.LogService) *ConfigService {
// 获取用户主目录
homeDir, err := os.UserHomeDir()
if err != nil {
panic(fmt.Errorf("unable to get the user's home directory: %w", err))
}
// 设置配置目录和设置文件路径
configDir := filepath.Join(homeDir, ".voidraft", "config")
settingsPath := filepath.Join(configDir, "settings.json")
observerService := NewConfigObserver(logger)
configMigrator := NewConfigMigrator(logger, configDir, "settings", settingsPath)
return &ConfigService{
logger: logger,
configDir: configDir,
settingsPath: settingsPath,
koanf: koanf.New("."),
observer: observerService,
configMigrator: configMigrator,
observer: helper.NewConfigObserver(logger),
configMigrator: NewConfigMigrator(logger, configDir, "settings", settingsPath),
}
}
// ServiceStartup initializes the service when the application starts
// ServiceStartup 服务启动时初始化
func (cs *ConfigService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
err := cs.initConfig()
if err != nil {
if err := cs.initConfig(); err != nil {
panic(err)
}
// 启动配置文件监听
cs.startWatching()
return nil
}
// setDefaults 设置默认配置
func (cs *ConfigService) setDefaults() error {
defaultConfig := models.NewDefaultAppConfig()
if err := cs.koanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
return err
}
return nil
}
@@ -87,15 +65,38 @@ func (cs *ConfigService) initConfig() error {
cs.mu.Lock()
defer cs.mu.Unlock()
// 检查配置文件是否存在
// 确保配置目录存在
if err := os.MkdirAll(cs.configDir, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
// 配置文件不存在,创建默认配置
if _, err := os.Stat(cs.settingsPath); os.IsNotExist(err) {
return cs.createDefaultConfig()
}
// 配置文件存在,直接加载现有配置
cs.fileProvider = file.Provider(cs.settingsPath)
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
return err
// 加载现有配置
if err := cs.koanf.Load(file.Provider(cs.settingsPath), jsonparser.Parser()); err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
return nil
}
// createDefaultConfig 创建默认配置
func (cs *ConfigService) createDefaultConfig() error {
// 重置 koanf 实例
cs.koanf = koanf.New(".")
// 加载默认配置
defaultConfig := models.NewDefaultAppConfig()
if err := cs.koanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
return fmt.Errorf("failed to load default config: %w", err)
}
// 写入配置文件
if err := cs.writeConfigToFile(); err != nil {
return fmt.Errorf("failed to write default config: %w", err)
}
return nil
@@ -107,65 +108,12 @@ func (cs *ConfigService) MigrateConfig() error {
return nil
}
cs.mu.Lock()
defer cs.mu.Unlock()
defaultConfig := models.NewDefaultAppConfig()
_, err := cs.configMigrator.AutoMigrate(defaultConfig, cs.koanf)
if err != nil {
return err
}
return nil
}
// createDefaultConfig 创建默认配置文件
func (cs *ConfigService) createDefaultConfig() error {
// 确保配置目录存在
if err := os.MkdirAll(cs.configDir, 0755); err != nil {
return err
}
if err := cs.setDefaults(); err != nil {
return err
}
if err := cs.writeConfigToFile(); err != nil {
return err
}
// 创建文件提供器
cs.fileProvider = file.Provider(cs.settingsPath)
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
return err
}
return nil
}
// startWatching 启动配置文件监听
func (cs *ConfigService) startWatching() {
if cs.fileProvider == nil {
return
}
cs.fileProvider.Watch(func(event interface{}, err error) {
if err != nil {
return
}
cs.mu.Lock()
oldSnapshot := cs.createConfigSnapshot()
cs.koanf.Load(cs.fileProvider, jsonparser.Parser())
newSnapshot := cs.createConfigSnapshot()
cs.mu.Unlock()
// 检测配置变更并通知观察者
cs.notifyChanges(oldSnapshot, newSnapshot)
})
}
// stopWatching 停止配置文件监听
func (cs *ConfigService) stopWatching() {
if cs.fileProvider != nil {
cs.fileProvider.Unwatch()
}
return err
}
// GetConfig 获取完整应用配置
@@ -177,47 +125,9 @@ func (cs *ConfigService) GetConfig() (*models.AppConfig, error) {
if err := cs.koanf.UnmarshalWithConf("", &config, koanf.UnmarshalConf{Tag: "json"}); err != nil {
return nil, err
}
return &config, nil
}
// Set 设置配置项
func (cs *ConfigService) Set(key string, value interface{}) error {
cs.mu.Lock()
// 获取旧值用于回滚
oldValue := cs.koanf.Get(key)
// 设置值到koanf
cs.koanf.Set(key, value)
// 更新时间戳
newTimestamp := time.Now().Format(time.RFC3339)
cs.koanf.Set("metadata.lastUpdated", newTimestamp)
// 将配置写回文件
err := cs.writeConfigToFile()
if err != nil {
// 写文件失败,回滚内存状态
if oldValue != nil {
cs.koanf.Set(key, oldValue)
} else {
cs.koanf.Delete(key)
}
cs.mu.Unlock()
return err
}
cs.mu.Unlock()
if cs.observer != nil {
cs.observer.Notify(key, oldValue, value)
}
return nil
}
// Get 获取配置项
func (cs *ConfigService) Get(key string) interface{} {
cs.mu.RLock()
@@ -225,118 +135,113 @@ func (cs *ConfigService) Get(key string) interface{} {
return cs.koanf.Get(key)
}
// ResetConfig 强制重置所有配置为默认值
// Set 设置配置项
func (cs *ConfigService) Set(key string, value interface{}) error {
cs.mu.Lock()
// 获取旧值
oldValue := cs.koanf.Get(key)
// 值未变化,直接返回
if reflect.DeepEqual(oldValue, value) {
cs.mu.Unlock()
return nil
}
// 设置新值
err := cs.koanf.Set(key, value)
if err != nil {
cs.mu.Unlock()
return err
}
err = cs.koanf.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339))
if err != nil {
cs.mu.Unlock()
return err
}
// 写入文件
if err = cs.writeConfigToFile(); err != nil {
cs.mu.Unlock()
return fmt.Errorf("failed to write config: %w", err)
}
cs.mu.Unlock()
// 通知观察者
if cs.observer != nil {
cs.observer.Notify(key, oldValue, value)
} else {
cs.logger.Error("config observer is nil")
}
return nil
}
// ResetConfig 重置所有配置为默认值
func (cs *ConfigService) ResetConfig() error {
cs.mu.Lock()
// 保存旧配置快照
oldSnapshot := cs.createConfigSnapshot()
oldSnapshot := cs.createSnapshot()
// 停止文件监听
if cs.fileProvider != nil {
cs.fileProvider.Unwatch()
cs.fileProvider = nil
}
// 设置默认配置
if err := cs.setDefaults(); err != nil {
// 重置为默认配置
cs.koanf = koanf.New(".")
defaultConfig := models.NewDefaultAppConfig()
if err := cs.koanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
cs.mu.Unlock()
return err
return fmt.Errorf("failed to load default config: %w", err)
}
// 写入配置文件
if err := cs.writeConfigToFile(); err != nil {
cs.mu.Unlock()
return err
return fmt.Errorf("failed to write config: %w", err)
}
// 重新创建koanf实例
cs.koanf = koanf.New(".")
// 重新加载默认配置到koanf
if err := cs.setDefaults(); err != nil {
cs.mu.Unlock()
return err
}
// 重新创建文件提供器
cs.fileProvider = file.Provider(cs.settingsPath)
// 重新加载配置文件
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
cs.mu.Unlock()
return err
}
newSnapshot := cs.createConfigSnapshot()
newSnapshot := cs.createSnapshot()
cs.mu.Unlock()
// 重新启动文件监听
cs.startWatching()
// 检测配置变更并通知观察者
// 通知配置变更
cs.notifyChanges(oldSnapshot, newSnapshot)
return nil
}
// writeConfigToFile 将配置写回JSON文件
// writeConfigToFile 将配置写文件
func (cs *ConfigService) writeConfigToFile() error {
configBytes, err := cs.koanf.Marshal(jsonparser.Parser())
if err != nil {
return err
}
if err := os.WriteFile(cs.settingsPath, configBytes, 0644); err != nil {
return err
}
return nil
return os.WriteFile(cs.settingsPath, configBytes, 0644)
}
// Watch 注册配置变更监听器
func (cs *ConfigService) Watch(path string, callback ObserverCallback) CancelFunc {
func (cs *ConfigService) Watch(path string, callback helper.ObserverCallback) helper.CancelFunc {
return cs.observer.Watch(path, callback)
}
// WatchWithContext 使用 Context 注册监听器
func (cs *ConfigService) WatchWithContext(ctx context.Context, path string, callback ObserverCallback) {
func (cs *ConfigService) WatchWithContext(ctx context.Context, path string, callback helper.ObserverCallback) {
cs.observer.WatchWithContext(ctx, path, callback)
}
// createConfigSnapshot 创建当前配置快照(调用者需确保已持有锁)
func (cs *ConfigService) createConfigSnapshot() map[string]interface{} {
// createSnapshotLocked 创建配置快照
func (cs *ConfigService) createSnapshot() map[string]interface{} {
snapshot := make(map[string]interface{})
allKeys := cs.koanf.All()
flattenMap("", allKeys, snapshot)
return snapshot
}
// flattenMap 递归展平嵌套的 map使用 strings.Builder 优化字符串拼接)
func flattenMap(prefix string, data map[string]interface{}, result map[string]interface{}) {
var builder strings.Builder
for key, value := range data {
builder.Reset()
if prefix != "" {
builder.WriteString(prefix)
builder.WriteString(".")
}
builder.WriteString(key)
fullKey := builder.String()
if valueMap, ok := value.(map[string]interface{}); ok {
// 递归处理嵌套 map
flattenMap(fullKey, valueMap, result)
} else {
// 保存叶子节点
result[fullKey] = value
}
for _, key := range cs.koanf.Keys() {
snapshot[key] = cs.koanf.Get(key)
}
return snapshot
}
// notifyChanges 检测配置变更并通知观察者
func (cs *ConfigService) notifyChanges(oldSnapshot, newSnapshot map[string]interface{}) {
// 检测变更
if cs.observer == nil {
return
}
changes := make(map[string]struct {
OldValue interface{}
NewValue interface{}
@@ -345,14 +250,11 @@ func (cs *ConfigService) notifyChanges(oldSnapshot, newSnapshot map[string]inter
// 检查新增和修改的键
for key, newValue := range newSnapshot {
oldValue, exists := oldSnapshot[key]
if !exists || !isEqual(oldValue, newValue) {
if !exists || !reflect.DeepEqual(oldValue, newValue) {
changes[key] = struct {
OldValue interface{}
NewValue interface{}
}{
OldValue: oldValue,
NewValue: newValue,
}
}{oldValue, newValue}
}
}
@@ -362,29 +264,18 @@ func (cs *ConfigService) notifyChanges(oldSnapshot, newSnapshot map[string]inter
changes[key] = struct {
OldValue interface{}
NewValue interface{}
}{
OldValue: oldValue,
NewValue: nil,
}
}{oldValue, nil}
}
}
// 通知所有变更
if cs.observer != nil && len(changes) > 0 {
// 批量通知
if len(changes) > 0 {
cs.observer.NotifyAll(changes)
}
}
// isEqual 值相等比较
func isEqual(a, b interface{}) bool {
aJSON, _ := json.Marshal(a)
bJSON, _ := json.Marshal(b)
return string(aJSON) == string(bJSON)
}
// ServiceShutdown 关闭服务
func (cs *ConfigService) ServiceShutdown() error {
cs.stopWatching()
if cs.observer != nil {
cs.observer.Shutdown()
}

View File

@@ -24,9 +24,10 @@ type MigrationProgress struct {
// MigrationService 迁移服务
type MigrationService struct {
logger *log.LogService
dbService *DatabaseService
progress atomic.Value // stores MigrationProgress
logger *log.LogService
dbService *DatabaseService
configService *ConfigService
progress atomic.Value // stores MigrationProgress
mu sync.Mutex
ctx context.Context
@@ -34,13 +35,14 @@ type MigrationService struct {
}
// NewMigrationService 创建迁移服务
func NewMigrationService(dbService *DatabaseService, logger *log.LogService) *MigrationService {
func NewMigrationService(dbService *DatabaseService, configService *ConfigService, logger *log.LogService) *MigrationService {
if logger == nil {
logger = log.New()
}
ms := &MigrationService{
logger: logger,
dbService: dbService,
logger: logger,
dbService: dbService,
configService: configService,
}
ms.progress.Store(MigrationProgress{})
return ms
@@ -94,9 +96,12 @@ func (ms *MigrationService) MigrateDirectory(srcPath, dstPath string) error {
if err := ms.dbService.ServiceShutdown(); err != nil {
ms.logger.Error("Failed to close database connection", "error", err)
}
// 等待文件句柄释放Windows 特有问题)
time.Sleep(200 * time.Millisecond)
}
// 确保失败时恢复数据库连接
// 确保恢复数据库连接
defer func() {
if ms.dbService != nil {
if err := ms.dbService.ServiceStartup(ctx, application.ServiceOptions{}); err != nil {
@@ -110,17 +115,59 @@ func (ms *MigrationService) MigrateDirectory(srcPath, dstPath string) error {
return ms.fail(err)
}
// 迁移成功后,立即更新配置到新路径
if ms.configService != nil {
if err := ms.configService.Set("general.dataPath", dstPath); err != nil {
return ms.fail(fmt.Errorf("migration succeeded but failed to update config: %w", err))
}
}
ms.setProgress(100)
return nil
}
// preCheck 预检查,返回是否需要迁移
func (ms *MigrationService) preCheck(srcPath, dstPath string) (bool, error) {
// 源目录不存在,无需迁移
if _, err := os.Stat(srcPath); os.IsNotExist(err) {
// 检查源目录状态
srcStat, srcErr := os.Stat(srcPath)
srcNotExist := os.IsNotExist(srcErr)
// 检查目标目录状态
dstStat, dstErr := os.Stat(dstPath)
dstNotExist := os.IsNotExist(dstErr)
// 1源目录不存在
if srcNotExist {
// 如果目标目录存在且有内容,说明迁移已经完成
if !dstNotExist && dstStat.IsDir() {
isEmpty, err := isDirEmpty(dstPath)
if err == nil && !isEmpty {
ms.logger.Info("Migration already completed, source not exist but target has content", "dst", dstPath)
return false, nil // 无需迁移
}
}
// 源不存在且目标也不存在/为空,无需迁移
return false, nil
}
// 2. 源目录存在但为空
if srcStat.IsDir() {
srcEmpty, err := isDirEmpty(srcPath)
if err == nil && srcEmpty {
// 源为空,目标有内容 → 迁移已完成
if !dstNotExist && dstStat.IsDir() {
dstEmpty, _ := isDirEmpty(dstPath)
if !dstEmpty {
ms.logger.Info("Migration already completed, source is empty but target has content", "dst", dstPath)
return false, nil
}
}
// 源为空,目标也为空 → 无需迁移
ms.logger.Info("Both source and target are empty, no migration needed")
return false, nil
}
}
// 路径相同,无需迁移
srcAbs, _ := filepath.Abs(srcPath)
dstAbs, _ := filepath.Abs(dstPath)

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"sync"
"sync/atomic"
"time"
"voidraft/internal/common/helper"
"voidraft/internal/common/hotkey"
"voidraft/internal/models"
@@ -33,7 +32,7 @@ type HotkeyService struct {
isShutdown atomic.Bool
// 配置观察者取消函数
cancelObservers []CancelFunc
cancelObservers []helper.CancelFunc
}
// NewHotkeyService 创建热键服务实例
@@ -61,7 +60,7 @@ func (hs *HotkeyService) ServiceStartup(ctx context.Context, options application
// Initialize 初始化热键服务
func (hs *HotkeyService) Initialize() error {
// 注册配置监听
hs.cancelObservers = []CancelFunc{
hs.cancelObservers = []helper.CancelFunc{
hs.configService.Watch("general.enableGlobalHotkey", hs.onHotkeyConfigChange),
hs.configService.Watch("general.globalHotkey", hs.onHotkeyConfigChange),
}
@@ -84,11 +83,14 @@ func (hs *HotkeyService) onHotkeyConfigChange(oldValue, newValue interface{}) {
// 重新加载配置
config, err := hs.configService.GetConfig()
if err != nil {
hs.logger.Error("failed to get config", "error", err)
return
}
// 更新热键
_ = hs.UpdateHotkey(config.General.EnableGlobalHotkey, &config.General.GlobalHotkey)
if err := hs.UpdateHotkey(config.General.EnableGlobalHotkey, &config.General.GlobalHotkey); err != nil {
hs.logger.Error("failed to update hotkey", "error", err)
}
}
// RegisterHotkey 注册全局热键
@@ -101,22 +103,34 @@ func (hs *HotkeyService) RegisterHotkey(combo *models.HotkeyCombo) error {
return errors.New("invalid hotkey combination")
}
// 如果已注册,先取消
if hs.registered.Load() {
_ = hs.UnregisterHotkey()
}
// 转换为 hotkey 库的格式
key, mods, err := hs.convertHotkey(combo)
if err != nil {
return fmt.Errorf("convert hotkey: %w", err)
}
hs.mu.Lock()
// 如果已注册,异步清理旧热键
if hs.registered.Load() {
hs.mu.RLock()
oldHk := hs.hk
hs.mu.RUnlock()
if oldHk != nil {
// 异步清理,不阻塞当前流程
go func() {
if err := oldHk.Close(); err != nil {
hs.logger.Error("failed to close old hotkey (ignored)", "error", err)
}
}()
}
}
// 创建新的热键实例
hs.mu.Lock()
hs.hk = hotkey.New(mods, key)
if err := hs.hk.Register(); err != nil {
hs.mu.Unlock()
hs.logger.Error("failed to register hotkey", "error", err)
return fmt.Errorf("register hotkey: %w", err)
}
@@ -141,35 +155,23 @@ func (hs *HotkeyService) UnregisterHotkey() error {
hs.registered.Store(false)
// 获取热键实例的引用
hs.mu.RLock()
hs.mu.Lock()
hk := hs.hk
hs.mu.RUnlock()
hs.hk = nil
hs.currentHotkey = nil
hs.mu.Unlock()
if hk == nil {
return nil
}
// 调用 Close() 确保完全清理
_ = hk.Close()
// 等待监听 goroutine 退出
done := make(chan struct{})
// 异步清理
go func() {
hs.wg.Wait()
close(done)
if err := hk.Close(); err != nil {
hs.logger.Error("failed to close hotkey (ignored)", "error", err)
}
}()
select {
case <-done:
case <-time.After(2 * time.Second):
}
// 清理状态
hs.mu.Lock()
hs.hk = nil
hs.currentHotkey = nil
hs.mu.Unlock()
return nil
}
@@ -321,6 +323,24 @@ func (hs *HotkeyService) showAllWindows() {
}
}
// GetSupportedKeys 返回系统支持的快捷键列表
func (hs *HotkeyService) GetSupportedKeys() []string {
// 返回当前系统支持的所有键
// 这个列表与 convertKey 方法中的 keyMap 保持一致
return []string{
// 字母键
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
// 数字键
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
// 功能键
"F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12",
// 特殊键
"Space", "Tab", "Enter", "Escape", "Delete",
"ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight",
}
}
// isValidHotkey 验证热键组合
func (hs *HotkeyService) isValidHotkey(combo *models.HotkeyCombo) bool {
if combo == nil || combo.Key == "" {
@@ -333,24 +353,6 @@ func (hs *HotkeyService) isValidHotkey(combo *models.HotkeyCombo) bool {
return true
}
// GetCurrentHotkey 获取当前热键
func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo {
hs.mu.RLock()
defer hs.mu.RUnlock()
if hs.currentHotkey == nil {
return nil
}
return &models.HotkeyCombo{
Ctrl: hs.currentHotkey.Ctrl,
Shift: hs.currentHotkey.Shift,
Alt: hs.currentHotkey.Alt,
Win: hs.currentHotkey.Win,
Key: hs.currentHotkey.Key,
}
}
// IsRegistered 检查是否已注册
func (hs *HotkeyService) IsRegistered() bool {
return hs.registered.Load()

View File

@@ -1,387 +0,0 @@
package services
import (
"testing"
"time"
"voidraft/internal/models"
"github.com/wailsapp/wails/v3/pkg/services/log"
)
// TestHotkeyServiceCreation 测试服务创建
func TestHotkeyServiceCreation(t *testing.T) {
logger := log.New()
configService := &ConfigService{} // Mock
service := NewHotkeyService(configService, logger)
if service == nil {
t.Fatal("Failed to create hotkey service")
}
if service.logger == nil {
t.Error("Logger should not be nil")
}
if service.registered.Load() {
t.Error("Service should not have registered hotkey initially")
}
}
// TestHotkeyValidation 测试热键验证
func TestHotkeyValidation(t *testing.T) {
logger := log.New()
service := NewHotkeyService(&ConfigService{}, logger)
tests := []struct {
name string
combo *models.HotkeyCombo
valid bool
}{
{
name: "Nil combo",
combo: nil,
valid: false,
},
{
name: "Empty key",
combo: &models.HotkeyCombo{
Ctrl: true,
Key: "",
},
valid: false,
},
{
name: "No modifiers",
combo: &models.HotkeyCombo{
Key: "A",
},
valid: false,
},
{
name: "Valid: Ctrl+A",
combo: &models.HotkeyCombo{
Ctrl: true,
Key: "A",
},
valid: true,
},
{
name: "Valid: Ctrl+Shift+F1",
combo: &models.HotkeyCombo{
Ctrl: true,
Shift: true,
Key: "F1",
},
valid: true,
},
{
name: "Valid: Alt+Space",
combo: &models.HotkeyCombo{
Alt: true,
Key: "Space",
},
valid: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := service.isValidHotkey(tt.combo)
if result != tt.valid {
t.Errorf("Expected valid=%v, got %v", tt.valid, result)
}
})
}
}
// TestHotkeyConversion 测试热键转换
func TestHotkeyConversion(t *testing.T) {
logger := log.New()
service := NewHotkeyService(&ConfigService{}, logger)
tests := []struct {
name string
combo *models.HotkeyCombo
wantErr bool
}{
{
name: "Valid letter key",
combo: &models.HotkeyCombo{
Ctrl: true,
Key: "A",
},
wantErr: false,
},
{
name: "Valid number key",
combo: &models.HotkeyCombo{
Shift: true,
Key: "1",
},
wantErr: false,
},
{
name: "Valid function key",
combo: &models.HotkeyCombo{
Alt: true,
Key: "F5",
},
wantErr: false,
},
{
name: "Invalid key",
combo: &models.HotkeyCombo{
Ctrl: true,
Key: "InvalidKey",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
key, mods, err := service.convertHotkey(tt.combo)
if tt.wantErr {
if err == nil {
t.Error("Expected error, got nil")
}
return
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
if key == 0 {
t.Error("Key should not be 0")
}
if len(mods) == 0 {
t.Error("Should have at least one modifier")
}
})
}
}
// TestHotkeyRegisterUnregister 测试注册和注销
func TestHotkeyRegisterUnregister(t *testing.T) {
logger := log.New()
service := NewHotkeyService(&ConfigService{}, logger)
combo := &models.HotkeyCombo{
Ctrl: true,
Shift: true,
Key: "F10",
}
// 测试注册
err := service.RegisterHotkey(combo)
if err != nil {
t.Logf("Register failed (may be expected in test environment): %v", err)
return
}
if !service.IsRegistered() {
t.Error("Service should be registered")
}
// 验证当前热键
current := service.GetCurrentHotkey()
if current == nil {
t.Error("Current hotkey should not be nil")
}
if current.Key != combo.Key {
t.Errorf("Expected key %s, got %s", combo.Key, current.Key)
}
// 测试注销
err = service.UnregisterHotkey()
if err != nil {
t.Fatalf("Unregister failed: %v", err)
}
if service.IsRegistered() {
t.Error("Service should not be registered after unregister")
}
current = service.GetCurrentHotkey()
if current != nil {
t.Error("Current hotkey should be nil after unregister")
}
}
// TestHotkeyUpdate 测试更新热键
func TestHotkeyUpdate(t *testing.T) {
logger := log.New()
service := NewHotkeyService(&ConfigService{}, logger)
combo1 := &models.HotkeyCombo{
Ctrl: true,
Key: "F11",
}
// 启用热键
err := service.UpdateHotkey(true, combo1)
if err != nil {
t.Logf("Update (enable) failed: %v", err)
return
}
defer service.UnregisterHotkey()
if !service.IsRegistered() {
t.Error("Should be registered after enable")
}
// 禁用热键
err = service.UpdateHotkey(false, combo1)
if err != nil {
t.Fatalf("Update (disable) failed: %v", err)
}
if service.IsRegistered() {
t.Error("Should not be registered after disable")
}
}
// TestHotkeyDoubleRegister 测试重复注册
func TestHotkeyDoubleRegister(t *testing.T) {
logger := log.New()
service := NewHotkeyService(&ConfigService{}, logger)
combo := &models.HotkeyCombo{
Ctrl: true,
Alt: true,
Key: "F12",
}
err := service.RegisterHotkey(combo)
if err != nil {
t.Skip("First registration failed")
}
defer service.UnregisterHotkey()
// 第二次注册应该先取消第一次注册,然后重新注册
combo2 := &models.HotkeyCombo{
Shift: true,
Key: "F12",
}
err = service.RegisterHotkey(combo2)
if err != nil {
t.Logf("Second registration failed: %v", err)
}
// 验证当前热键是新的
current := service.GetCurrentHotkey()
if current != nil && current.Shift != combo2.Shift {
t.Error("Should have updated to new hotkey")
}
}
// TestHotkeyConcurrentAccess 测试并发访问
func TestHotkeyConcurrentAccess(t *testing.T) {
logger := log.New()
service := NewHotkeyService(&ConfigService{}, logger)
combo := &models.HotkeyCombo{
Ctrl: true,
Key: "G",
}
const goroutines = 10
done := make(chan bool, goroutines)
// 并发读取
for i := 0; i < goroutines; i++ {
go func() {
for j := 0; j < 100; j++ {
_ = service.IsRegistered()
_ = service.GetCurrentHotkey()
time.Sleep(time.Millisecond)
}
done <- true
}()
}
// 主协程进行注册/注销操作
go func() {
for i := 0; i < 5; i++ {
service.RegisterHotkey(combo)
time.Sleep(50 * time.Millisecond)
service.UnregisterHotkey()
time.Sleep(50 * time.Millisecond)
}
}()
// 等待所有 goroutine 完成
for i := 0; i < goroutines; i++ {
<-done
}
t.Log("Concurrent access test completed without panics")
}
// TestHotkeyServiceShutdown 测试服务关闭
func TestHotkeyServiceShutdown(t *testing.T) {
logger := log.New()
service := NewHotkeyService(&ConfigService{}, logger)
combo := &models.HotkeyCombo{
Ctrl: true,
Shift: true,
Key: "H",
}
err := service.RegisterHotkey(combo)
if err != nil {
t.Skip("Registration failed")
}
// 测试 ServiceShutdown
err = service.ServiceShutdown()
if err != nil {
t.Fatalf("ServiceShutdown failed: %v", err)
}
if service.IsRegistered() {
t.Error("Should not be registered after shutdown")
}
}
// BenchmarkHotkeyRegistration 基准测试:热键注册
func BenchmarkHotkeyRegistration(b *testing.B) {
logger := log.New()
service := NewHotkeyService(&ConfigService{}, logger)
combo := &models.HotkeyCombo{
Ctrl: true,
Key: "B",
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
service.RegisterHotkey(combo)
service.UnregisterHotkey()
}
}
// BenchmarkHotkeyConversion 基准测试:热键转换
func BenchmarkHotkeyConversion(b *testing.B) {
logger := log.New()
service := NewHotkeyService(&ConfigService{}, logger)
combo := &models.HotkeyCombo{
Ctrl: true,
Shift: true,
Alt: true,
Key: "F5",
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
service.convertHotkey(combo)
}
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/wailsapp/wails/v3/pkg/services/dock"
"github.com/wailsapp/wails/v3/pkg/services/log"
"github.com/wailsapp/wails/v3/pkg/services/notifications"
"log/slog"
)
// ServiceManager 服务管理器,负责协调各个服务
@@ -36,7 +37,9 @@ type ServiceManager struct {
// NewServiceManager 创建新的服务管理器实例
func NewServiceManager() *ServiceManager {
// 初始化日志服务
logger := log.New()
logger := log.NewWithConfig(&log.Config{
LogLevel: slog.LevelDebug,
})
// 初始化badge服务
badgeService := dock.New()
@@ -51,7 +54,7 @@ func NewServiceManager() *ServiceManager {
databaseService := NewDatabaseService(configService, logger)
// 初始化迁移服务
migrationService := NewMigrationService(databaseService, logger)
migrationService := NewMigrationService(databaseService, configService, logger)
// 初始化文档服务
documentService := NewDocumentService(databaseService, logger)

View File

@@ -113,7 +113,7 @@ func (ws *WindowService) onWindowClosing(documentID int64) {
}
// GetOpenWindows 获取所有打开的文档窗口
func (ws *WindowService) GetOpenWindows() []application.Window {
func (ws *WindowService) getOpenWindows() []application.Window {
app := application.Get()
return app.Window.GetAll()
}
@@ -130,7 +130,7 @@ func (ws *WindowService) IsDocumentWindowOpen(documentID int64) bool {
func (ws *WindowService) ServiceShutdown() error {
// 从吸附服务中取消注册所有窗口
if ws.windowSnapService != nil {
windows := ws.GetOpenWindows()
windows := ws.getOpenWindows()
for _, window := range windows {
if documentID, err := strconv.ParseInt(window.Name(), 10, 64); err == nil {
ws.windowSnapService.UnregisterWindow(documentID)

View File

@@ -91,7 +91,7 @@ type WindowSnapService struct {
windowMoveUnhooks map[int64]func() // documentID -> 子窗口移动监听清理函数
// 配置观察者取消函数
cancelObserver CancelFunc
cancelObserver helper.CancelFunc
}
// NewWindowSnapService 创建新的窗口吸附服务实例