Merge pull request #22 from landaiqing/dev

Major backend/frontend updates for config and editor handling
This commit is contained in:
2026-01-03 23:32:30 +08:00
committed by GitHub
72 changed files with 3226 additions and 2719 deletions

View File

@@ -461,6 +461,11 @@ export class GeneralConfig {
*/ */
"enableTabs": boolean; "enableTabs": boolean;
/**
* 是否启用内存监视器
*/
"enableMemoryMonitor": boolean;
/** Creates a new GeneralConfig instance. */ /** Creates a new GeneralConfig instance. */
constructor($$source: Partial<GeneralConfig> = {}) { constructor($$source: Partial<GeneralConfig> = {}) {
if (!("alwaysOnTop" in $$source)) { if (!("alwaysOnTop" in $$source)) {
@@ -490,6 +495,9 @@ export class GeneralConfig {
if (!("enableTabs" in $$source)) { if (!("enableTabs" in $$source)) {
this["enableTabs"] = false; this["enableTabs"] = false;
} }
if (!("enableMemoryMonitor" in $$source)) {
this["enableMemoryMonitor"] = false;
}
Object.assign(this, $$source); Object.assign(this, $$source);
} }
@@ -556,49 +564,6 @@ export class GitBackupConfig {
} }
} }
/**
* GiteaConfig Gitea配置
*/
export class GiteaConfig {
/**
* Gitea服务器URL
*/
"baseURL": string;
/**
* 仓库所有者
*/
"owner": string;
/**
* 仓库名称
*/
"repo": string;
/** Creates a new GiteaConfig instance. */
constructor($$source: Partial<GiteaConfig> = {}) {
if (!("baseURL" in $$source)) {
this["baseURL"] = "";
}
if (!("owner" in $$source)) {
this["owner"] = "";
}
if (!("repo" in $$source)) {
this["repo"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new GiteaConfig instance from a string or object.
*/
static createFrom($$source: any = {}): GiteaConfig {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new GiteaConfig($$parsedSource as Partial<GiteaConfig>);
}
}
/** /**
* GithubConfig GitHub配置 * GithubConfig GitHub配置
*/ */
@@ -1264,26 +1229,6 @@ export enum TabType {
TabTypeTab = "tab", TabTypeTab = "tab",
}; };
/**
* UpdateSourceType 更新源类型
*/
export enum UpdateSourceType {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* UpdateSourceGithub GitHub更新源
*/
UpdateSourceGithub = "github",
/**
* UpdateSourceGitea Gitea更新源
*/
UpdateSourceGitea = "gitea",
};
/** /**
* UpdatesConfig 更新设置配置 * UpdatesConfig 更新设置配置
*/ */
@@ -1298,16 +1243,6 @@ export class UpdatesConfig {
*/ */
"autoUpdate": boolean; "autoUpdate": boolean;
/**
* 主要更新源
*/
"primarySource": UpdateSourceType;
/**
* 备用更新源
*/
"backupSource": UpdateSourceType;
/** /**
* 更新前是否备份 * 更新前是否备份
*/ */
@@ -1323,11 +1258,6 @@ export class UpdatesConfig {
*/ */
"github": GithubConfig; "github": GithubConfig;
/**
* Gitea配置
*/
"gitea": GiteaConfig;
/** Creates a new UpdatesConfig instance. */ /** Creates a new UpdatesConfig instance. */
constructor($$source: Partial<UpdatesConfig> = {}) { constructor($$source: Partial<UpdatesConfig> = {}) {
if (!("version" in $$source)) { if (!("version" in $$source)) {
@@ -1336,12 +1266,6 @@ export class UpdatesConfig {
if (!("autoUpdate" in $$source)) { if (!("autoUpdate" in $$source)) {
this["autoUpdate"] = false; this["autoUpdate"] = false;
} }
if (!("primarySource" in $$source)) {
this["primarySource"] = ("" as UpdateSourceType);
}
if (!("backupSource" in $$source)) {
this["backupSource"] = ("" as UpdateSourceType);
}
if (!("backupBeforeUpdate" in $$source)) { if (!("backupBeforeUpdate" in $$source)) {
this["backupBeforeUpdate"] = false; this["backupBeforeUpdate"] = false;
} }
@@ -1351,9 +1275,6 @@ export class UpdatesConfig {
if (!("github" in $$source)) { if (!("github" in $$source)) {
this["github"] = (new GithubConfig()); this["github"] = (new GithubConfig());
} }
if (!("gitea" in $$source)) {
this["gitea"] = (new GiteaConfig());
}
Object.assign(this, $$source); Object.assign(this, $$source);
} }
@@ -1362,14 +1283,10 @@ export class UpdatesConfig {
* Creates a new UpdatesConfig instance from a string or object. * Creates a new UpdatesConfig instance from a string or object.
*/ */
static createFrom($$source: any = {}): UpdatesConfig { static createFrom($$source: any = {}): UpdatesConfig {
const $$createField6_0 = $$createType9; const $$createField4_0 = $$createType9;
const $$createField7_0 = $$createType10;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("github" in $$parsedSource) { if ("github" in $$parsedSource) {
$$parsedSource["github"] = $$createField6_0($$parsedSource["github"]); $$parsedSource["github"] = $$createField4_0($$parsedSource["github"]);
}
if ("gitea" in $$parsedSource) {
$$parsedSource["gitea"] = $$createField7_0($$parsedSource["gitea"]);
} }
return new UpdatesConfig($$parsedSource as Partial<UpdatesConfig>); return new UpdatesConfig($$parsedSource as Partial<UpdatesConfig>);
} }
@@ -1391,4 +1308,3 @@ var $$createType6 = (function $$initCreateType6(...args): any {
const $$createType7 = $Create.Map($Create.Any, $Create.Any); const $$createType7 = $Create.Map($Create.Any, $Create.Any);
const $$createType8 = HotkeyCombo.createFrom; const $$createType8 = HotkeyCombo.createFrom;
const $$createType9 = GithubConfig.createFrom; const $$createType9 = GithubConfig.createFrom;
const $$createType10 = GiteaConfig.createFrom;

View File

@@ -89,13 +89,21 @@ export function UpdateKeyBindingEnabled(id: number, enabled: boolean): Promise<v
} }
/** /**
* UpdateKeyBindingKeys 更新快捷键绑定(根据操作系统自动判断更新哪个字段) * UpdateKeyBindingKeys 更新快捷键绑定
*/ */
export function UpdateKeyBindingKeys(id: number, key: string): Promise<void> & { cancel(): void } { export function UpdateKeyBindingKeys(id: number, key: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3432755175, id, key) as any; let $resultPromise = $Call.ByID(3432755175, id, key) as any;
return $resultPromise; return $resultPromise;
} }
/**
* UpdateKeyBindingPreventDefault 更新快捷键 PreventDefault 状态
*/
export function UpdateKeyBindingPreventDefault(id: number, preventDefault: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(202386744, id, preventDefault) as any;
return $resultPromise;
}
// Private type creation functions // Private type creation functions
const $$createType0 = models$0.KeyBinding.createFrom; const $$createType0 = models$0.KeyBinding.createFrom;
const $$createType1 = $Create.Array($$createType0); const $$createType1 = $Create.Array($$createType0);

View File

@@ -285,7 +285,7 @@ export class SelfUpdateResult {
"error": string; "error": string;
/** /**
* 更新源github/gitea * 更新源github
*/ */
"source": string; "source": string;

View File

@@ -11,6 +11,8 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AccordionContainer: typeof import('./src/components/accordion/AccordionContainer.vue')['default']
AccordionItem: typeof import('./src/components/accordion/AccordionItem.vue')['default']
BlockLanguageSelector: typeof import('./src/components/toolbar/BlockLanguageSelector.vue')['default'] BlockLanguageSelector: typeof import('./src/components/toolbar/BlockLanguageSelector.vue')['default']
DocumentSelector: typeof import('./src/components/toolbar/DocumentSelector.vue')['default'] DocumentSelector: typeof import('./src/components/toolbar/DocumentSelector.vue')['default']
LinuxTitleBar: typeof import('./src/components/titlebar/LinuxTitleBar.vue')['default'] LinuxTitleBar: typeof import('./src/components/titlebar/LinuxTitleBar.vue')['default']
@@ -22,6 +24,8 @@ declare module 'vue' {
TabContainer: typeof import('./src/components/tabs/TabContainer.vue')['default'] TabContainer: typeof import('./src/components/tabs/TabContainer.vue')['default']
TabContextMenu: typeof import('./src/components/tabs/TabContextMenu.vue')['default'] TabContextMenu: typeof import('./src/components/tabs/TabContextMenu.vue')['default']
TabItem: typeof import('./src/components/tabs/TabItem.vue')['default'] TabItem: typeof import('./src/components/tabs/TabItem.vue')['default']
Toast: typeof import('./src/components/toast/Toast.vue')['default']
ToastContainer: typeof import('./src/components/toast/ToastContainer.vue')['default']
Toolbar: typeof import('./src/components/toolbar/Toolbar.vue')['default'] Toolbar: typeof import('./src/components/toolbar/Toolbar.vue')['default']
WindowsTitleBar: typeof import('./src/components/titlebar/WindowsTitleBar.vue')['default'] WindowsTitleBar: typeof import('./src/components/titlebar/WindowsTitleBar.vue')['default']
WindowTitleBar: typeof import('./src/components/titlebar/WindowTitleBar.vue')['default'] WindowTitleBar: typeof import('./src/components/titlebar/WindowTitleBar.vue')['default']

View File

@@ -36,7 +36,7 @@
"@codemirror/lint": "^6.9.2", "@codemirror/lint": "^6.9.2",
"@codemirror/search": "^6.5.11", "@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.3", "@codemirror/state": "^6.5.3",
"@codemirror/view": "^6.39.6", "@codemirror/view": "^6.39.8",
"@cospaia/prettier-plugin-clojure": "^0.0.2", "@cospaia/prettier-plugin-clojure": "^0.0.2",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.5", "@lezer/lr": "^1.4.5",
@@ -63,7 +63,7 @@
"prettier": "^3.7.4", "prettier": "^3.7.4",
"sass": "^1.97.1", "sass": "^1.97.1",
"vue": "^3.5.26", "vue": "^3.5.26",
"vue-i18n": "^11.2.7", "vue-i18n": "^11.2.8",
"vue-pick-colors": "^1.8.0", "vue-pick-colors": "^1.8.0",
"vue-router": "^4.6.4" "vue-router": "^4.6.4"
}, },
@@ -77,7 +77,6 @@
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-plugin-vue": "^10.6.2", "eslint-plugin-vue": "^10.6.2",
"globals": "^16.5.0", "globals": "^16.5.0",
"happy-dom": "^20.0.11",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.50.1", "typescript-eslint": "^8.50.1",
"unplugin-vue-components": "^30.0.0", "unplugin-vue-components": "^30.0.0",
@@ -217,7 +216,6 @@
"resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", "resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz",
"integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
@@ -266,7 +264,6 @@
"resolved": "https://registry.npmmirror.com/@codemirror/lang-css/-/lang-css-6.3.1.tgz", "resolved": "https://registry.npmmirror.com/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
@@ -293,7 +290,6 @@
"resolved": "https://registry.npmmirror.com/@codemirror/lang-html/-/lang-html-6.4.11.tgz", "resolved": "https://registry.npmmirror.com/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0", "@codemirror/lang-css": "^6.0.0",
@@ -321,7 +317,6 @@
"resolved": "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", "resolved": "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
"integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0", "@codemirror/language": "^6.6.0",
@@ -534,7 +529,6 @@
"resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.12.1.tgz", "resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.12.1.tgz",
"integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0", "@codemirror/view": "^6.23.0",
@@ -611,7 +605,6 @@
"resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.3.tgz", "resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.3.tgz",
"integrity": "sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==", "integrity": "sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@marijn/find-cluster-break": "^1.0.0" "@marijn/find-cluster-break": "^1.0.0"
} }
@@ -621,7 +614,6 @@
"resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.39.6.tgz", "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.39.6.tgz",
"integrity": "sha512-/N+SoP5NndJjkGInp3BwlUa3KQKD6bDo0TV6ep37ueAdQ7BVu/PqlZNywmgjCq0MQoZadZd8T+MZucSr7fktyQ==", "integrity": "sha512-/N+SoP5NndJjkGInp3BwlUa3KQKD6bDo0TV6ep37ueAdQ7BVu/PqlZNywmgjCq0MQoZadZd8T+MZucSr7fktyQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@codemirror/state": "^6.5.0", "@codemirror/state": "^6.5.0",
"crelt": "^1.0.6", "crelt": "^1.0.6",
@@ -1445,8 +1437,7 @@
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.5.0.tgz", "resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.5.0.tgz",
"integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==", "integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@lezer/cpp": { "node_modules/@lezer/cpp": {
"version": "1.1.3", "version": "1.1.3",
@@ -1500,7 +1491,6 @@
"resolved": "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.3.tgz", "resolved": "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@lezer/common": "^1.3.0" "@lezer/common": "^1.3.0"
} }
@@ -1532,7 +1522,6 @@
"resolved": "https://registry.npmmirror.com/@lezer/javascript/-/javascript-1.5.1.tgz", "resolved": "https://registry.npmmirror.com/@lezer/javascript/-/javascript-1.5.1.tgz",
"integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==", "integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@lezer/common": "^1.2.0", "@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3", "@lezer/highlight": "^1.1.3",
@@ -1565,7 +1554,6 @@
"resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.5.tgz", "resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.5.tgz",
"integrity": "sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==", "integrity": "sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@lezer/common": "^1.0.0" "@lezer/common": "^1.0.0"
} }
@@ -2481,21 +2469,21 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@toml-tools/lexer": { "node_modules/@toml-tools/lexer": {
"version": "1.0.1", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@toml-tools/lexer/-/lexer-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/@toml-tools/lexer/-/lexer-1.0.0.tgz",
"integrity": "sha512-jn2fl8m/9QPcUD507Hbt2W3TVMKzF5HEY8xKIxqY2r2dTG2udeCKlo2ejJ5k/RSOJsWNIuw+Ir/nxW5PItUApA==", "integrity": "sha512-rVoOC9FibF2CICwCBWQnYcjAEOmLCJExer178K2AsY0Nk9FjJNVoVJuR5UAtuq42BZOajvH+ainf6Gj2GpCnXQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chevrotain": "^11.0.1" "chevrotain": "^11.0.1"
} }
}, },
"node_modules/@toml-tools/parser": { "node_modules/@toml-tools/parser": {
"version": "1.0.1", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@toml-tools/parser/-/parser-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/@toml-tools/parser/-/parser-1.0.0.tgz",
"integrity": "sha512-W+YdnB8KDgKjIqhoArEXjiTTPnKSXVvI/B+raHfou9+sip3rxhzVsELn46GG7dZyNHyu9pS+gYgYrdF9c5AQDg==", "integrity": "sha512-j8cd3A3ccLHppGoWI69urbiVJslrpwI6sZ61ySDUPxM/FTkQWRx/JkkF8aipnl0Ds0feWXyjyvmWzn70mIohYg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@toml-tools/lexer": "^1.0.1", "@toml-tools/lexer": "^1.0.0",
"chevrotain": "^11.0.1" "chevrotain": "^11.0.1"
} }
}, },
@@ -2847,7 +2835,6 @@
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@@ -2925,7 +2912,6 @@
"integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/scope-manager": "8.50.1",
"@typescript-eslint/types": "8.50.1", "@typescript-eslint/types": "8.50.1",
@@ -3617,7 +3603,6 @@
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -4131,7 +4116,6 @@
"resolved": "https://registry.npmmirror.com/chevrotain/-/chevrotain-11.0.3.tgz", "resolved": "https://registry.npmmirror.com/chevrotain/-/chevrotain-11.0.3.tgz",
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/cst-dts-gen": "11.0.3",
"@chevrotain/gast": "11.0.3", "@chevrotain/gast": "11.0.3",
@@ -4471,7 +4455,6 @@
"resolved": "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.33.1.tgz", "resolved": "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.33.1.tgz",
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10" "node": ">=0.10"
} }
@@ -4881,7 +4864,6 @@
"resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz", "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC", "license": "ISC",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
@@ -5296,7 +5278,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -5611,7 +5592,6 @@
"integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==", "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tabbable": "^6.3.0" "tabbable": "^6.3.0"
} }
@@ -6244,6 +6224,18 @@
"lodash": "4.17.21" "lodash": "4.17.21"
} }
}, },
"node_modules/jiti": {
"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-yaml": { "node_modules/js-yaml": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
@@ -7164,7 +7156,6 @@
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz", "resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz",
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vue/devtools-api": "^7.7.7" "@vue/devtools-api": "^7.7.7"
}, },
@@ -7313,7 +7304,6 @@
"resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.7.4.tgz", "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.7.4.tgz",
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@@ -7385,9 +7375,9 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.14.1", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"dev": true, "dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
@@ -7567,7 +7557,6 @@
"integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/estree": "1.0.8" "@types/estree": "1.0.8"
}, },
@@ -7670,7 +7659,6 @@
"resolved": "https://registry.npmmirror.com/sass/-/sass-1.97.1.tgz", "resolved": "https://registry.npmmirror.com/sass/-/sass-1.97.1.tgz",
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==", "integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"chokidar": "^4.0.0", "chokidar": "^4.0.0",
"immutable": "^5.0.2", "immutable": "^5.0.2",
@@ -8115,7 +8103,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -8235,7 +8222,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -8550,7 +8536,6 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -8661,7 +8646,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -8921,7 +8905,6 @@
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.26.tgz", "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.26.tgz",
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.26", "@vue/compiler-dom": "3.5.26",
"@vue/compiler-sfc": "3.5.26", "@vue/compiler-sfc": "3.5.26",
@@ -8944,7 +8927,6 @@
"integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==", "integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"debug": "^4.4.0", "debug": "^4.4.0",
"eslint-scope": "^8.2.0", "eslint-scope": "^8.2.0",

View File

@@ -50,7 +50,7 @@
"@codemirror/lint": "^6.9.2", "@codemirror/lint": "^6.9.2",
"@codemirror/search": "^6.5.11", "@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.3", "@codemirror/state": "^6.5.3",
"@codemirror/view": "^6.39.6", "@codemirror/view": "^6.39.8",
"@cospaia/prettier-plugin-clojure": "^0.0.2", "@cospaia/prettier-plugin-clojure": "^0.0.2",
"@lezer/highlight": "^1.2.3", "@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.5", "@lezer/lr": "^1.4.5",
@@ -77,7 +77,7 @@
"prettier": "^3.7.4", "prettier": "^3.7.4",
"sass": "^1.97.1", "sass": "^1.97.1",
"vue": "^3.5.26", "vue": "^3.5.26",
"vue-i18n": "^11.2.7", "vue-i18n": "^11.2.8",
"vue-pick-colors": "^1.8.0", "vue-pick-colors": "^1.8.0",
"vue-router": "^4.6.4" "vue-router": "^4.6.4"
}, },
@@ -91,9 +91,8 @@
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-plugin-vue": "^10.6.2", "eslint-plugin-vue": "^10.6.2",
"globals": "^16.5.0", "globals": "^16.5.0",
"happy-dom": "^20.0.11",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.50.1", "typescript-eslint": "^8.51.0",
"unplugin-vue-components": "^30.0.0", "unplugin-vue-components": "^30.0.0",
"vite": "npm:rolldown-vite@latest", "vite": "npm:rolldown-vite@latest",
"vite-plugin-node-polyfills": "^0.24.0", "vite-plugin-node-polyfills": "^0.24.0",

View File

@@ -0,0 +1 @@
<svg t="1767366893329" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="106998" width="200" height="200"><path d="M344.064 36.571429v95.085714H155.428571a23.771429 23.771429 0 0 0-23.259428 19.017143l-0.512 4.754285-0.073143 509.952L306.176 505.417143a95.085714 95.085714 0 0 1 121.270857-5.997714l4.827429 3.876571 152.137143 130.413714 102.546285-102.4a95.085714 95.085714 0 0 1 119.003429-12.507428l5.266286 3.657143 81.042285 60.781714 0.073143-118.784 0.512-7.021714a47.542857 47.542857 0 0 1 94.061714 0l0.512 7.021714v404.114286c0 62.171429-47.762286 113.225143-108.617142 118.418285l-10.24 0.438857h-713.142858l-10.24-0.438857a118.857143 118.857143 0 0 1-108.105142-107.52L36.571429 868.498286v-713.142857l0.438857-10.24A118.857143 118.857143 0 0 1 155.428571 36.571429h188.708572z m26.331429 538.916571L131.657143 794.404571l0.073143 74.166858c0 11.483429 8.118857 21.065143 19.017143 23.259428l4.754285 0.512h713.142857a23.771429 23.771429 0 0 0 23.259429-19.017143l0.512-4.754285-0.073143-166.473143-138.093714-103.570286-97.353143 97.28 76.288 65.316571a47.542857 47.542857 0 0 1-58.002286 75.190858l-3.876571-2.925715-300.836572-257.901714zM649.069714 60.269714a47.542857 47.542857 0 0 1 3.291429 63.634286l-3.291429 3.657143-61.44 61.44 61.44 61.44a47.542857 47.542857 0 0 1 3.291429 63.634286l-3.291429 3.657142a47.542857 47.542857 0 0 1-63.634285 3.218286l-3.584-3.291428-95.085715-95.085715a47.542857 47.542857 0 0 1-3.291428-63.634285l3.291428-3.584 95.085715-95.085715a47.542857 47.542857 0 0 1 67.291428 0zM855.259429 57.051429l3.584 3.218285 95.085714 95.085715a47.542857 47.542857 0 0 1 3.291428 63.634285l-3.291428 3.657143-95.085714 95.085714a47.542857 47.542857 0 0 1-70.509715-63.634285l3.291429-3.657143 61.44-61.44-61.44-61.44a47.542857 47.542857 0 0 1-3.291429-63.634286l3.291429-3.657143a47.542857 47.542857 0 0 1 63.634286-3.218285zM344.210286 36.571429a47.542857 47.542857 0 0 1 7.021714 94.573714l-7.021714 0.512V36.571429z" p-id="106999" fill="#e0620d"></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1 @@
<svg t="1767367606621" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15611" width="200" height="200"><path d="M511.966509 0.066982c96.036384 0 185.886507 26.451603 262.724147 72.443261L767.949763 68.670494l-127.948963 221.638835A254.788666 254.788666 0 0 0 511.966509 256.050237V0.066982z" fill="#E70212" p-id="15612"></path><path d="M767.949763 68.670494a509.577332 509.577332 0 0 1 191.304819 194.120635L955.329506 256.050237l-221.638835 127.991627a257.263171 257.263171 0 0 0-93.689871-93.689871L767.949763 68.670494z" fill="#EA6101" p-id="15613"></path><path d="M955.329506 256.050237A509.577332 509.577332 0 0 1 1023.933018 519.798317V512.033491h-255.983255a254.788666 254.788666 0 0 0-34.259092-127.991627l221.638835-127.991627z" fill="#F39801" p-id="15614"></path><path d="M1023.933018 512.033491c0 90.020778-23.251812 174.665907-64.038478 248.175765l-4.565034 7.80749-221.638835-127.948964c21.758577-37.672202 34.259092-81.402675 34.259092-128.034291v-0.042664L1023.933018 512.033491z" fill="#FCC902" p-id="15615"></path><path d="M733.690671 640.025118l221.638835 127.991628a509.66266 509.66266 0 0 1-179.52959 182.900035l-7.850153 4.479707-127.991627-221.638835A257.263171 257.263171 0 0 0 733.690671 640.025118z" fill="#FEF200" p-id="15616"></path><path d="M640.0008 733.757653L767.949763 955.396488A509.66266 509.66266 0 0 1 521.011251 1024H511.966509v-255.983254a254.788666 254.788666 0 0 0 128.034291-34.259093z" fill="#90C320" p-id="15617"></path><path d="M511.966509 768.016746v255.983254c-90.020778 0-174.665907-23.251812-248.175765-64.038477L255.983254 955.396488l127.991628-221.638835c37.672202 21.758577 81.402675 34.259092 128.034291 34.259093z" fill="#019A44" p-id="15618"></path><path d="M383.974882 733.757653l-127.991628 221.638835a509.66266 509.66266 0 0 1-182.900035-179.529589L68.603512 768.016746l221.638835-127.991628A257.263171 257.263171 0 0 0 383.974882 733.757653z" fill="#019E97" p-id="15619"></path><path d="M255.983254 512.033491c0 46.631616 12.457852 90.362089 34.259093 128.034291L68.603512 768.016746A509.66266 509.66266 0 0 1 0 521.078233V512.033491h255.983254z" fill="#0169B8" p-id="15620"></path><path d="M68.603512 256.050237l221.638835 127.991627A254.788666 254.788666 0 0 0 255.983254 512.033491H0c0-90.020778 23.251812-174.665907 64.038477-248.175765L68.603512 256.050237z" fill="#1C2089" p-id="15621"></path><path d="M262.681483 64.745418L255.983254 68.670494l127.991628 221.638835A257.263171 257.263171 0 0 0 290.242347 384.041864L68.603512 256.050237a509.577332 509.577332 0 0 1 194.120635-191.304819z" fill="#621988" p-id="15622"></path><path d="M519.731334 0.066982H511.966509v255.983255a254.788666 254.788666 0 0 0-128.034291 34.259092L255.983254 68.670494A509.577332 509.577332 0 0 1 519.731334 0.066982z" fill="#910783" p-id="15623"></path></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1 @@
<svg t="1767366808037" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="103196" width="200" height="200"><path d="M878.921143 96c27.099429 0 49.078857 21.942857 49.078857 49.078857v349.842286c0 27.099429-21.942857 49.078857-49.078857 49.078857h-221.842286c-27.099429 0-49.078857-21.942857-49.078857-49.078857V145.078857c0-27.099429 21.942857-49.078857 49.078857-49.078857z m-14.921143 320h-192v64h192v-64z m0-128h-192v64h192v-64z m0-128h-192v64h192v-64zM384 309.321143A202.678857 202.678857 0 0 1 586.678857 512v213.321143a202.678857 202.678857 0 0 1-202.678857 202.678857H298.678857a202.678857 202.678857 0 0 1-202.678857-202.678857V512a202.678857 202.678857 0 0 1 202.678857-202.678857z m138.642286 298.642286H160v117.394285a138.678857 138.678857 0 0 0 131.547429 138.459429l7.131428 0.182857H384a138.678857 138.678857 0 0 0 138.678857-138.678857l-0.036571-117.357714z m-213.321143-234.642286h-10.642286A138.678857 138.678857 0 0 0 160 512v31.963429h149.321143v-170.642286z" p-id="103197" fill="#8992c8"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg t="1767366707029" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="100833" width="200" height="200"><path d="M533.333 54c16.138 0 29.75 12.016 31.753 28.03l17.828 142.569L896 224.6c17.496 0 31.713 14.042 31.996 31.47l0.004 0.53V939c0 17.673-14.327 32-32 32H469.735a31.94 31.94 0 0 1-3.051-0.102l-0.09-0.009c-10.834-0.868-19.434-6.86-24.45-14.999a31.766 31.766 0 0 1-3.374-7.39 32.348 32.348 0 0 1-1.405-7.246L419.752 800.4H128c-17.496 0-31.713-14.042-31.996-31.47L96 768.4V86c0-17.673 14.327-32 32-32zM864 288.599H590.917l13.33 106.6L704 395.2c17.673 0 32 14.327 32 32 0 17.496-14.042 31.713-31.47 31.996l-0.53 0.004-91.75-0.001 13.331 106.6L704 565.8c17.673 0 32 14.327 32 32 0 17.496-14.042 31.713-31.47 31.996l-0.53 0.004-70.416-0.001 16.548 132.332 0.287 2.298c0.024 0.188 0.046 0.376 0.066 0.565 0.986 9.171-2.002 17.793-7.523 24.232l-0.217 0.25L539.872 907H864v-618.4zM548.127 800.4H484.25l7.985 63.851 55.892-63.851zM505.085 118H160v618.4h287.598c0.302-0.004 0.603-0.004 0.904 0h133.913l-0.001-0.004L569.01 629.21l-3.386-27.076c-0.03-0.225-0.058-0.45-0.084-0.676l-21.256-169.977c-0.03-0.219-0.056-0.438-0.081-0.659L505.085 118zM448 565.8c17.673 0 32 14.327 32 32 0 17.496-14.042 31.713-31.47 31.996l-0.53 0.004H256c-17.673 0-32-14.327-32-32 0-17.496 14.042-31.713 31.47-31.996l0.53-0.004h192z m-21.333-170.6c17.673 0 32 14.327 32 32 0 17.496-14.042 31.713-31.471 31.996l-0.53 0.004H256c-17.673 0-32-14.327-32-32 0-17.496 14.042-31.713 31.47-31.996l0.53-0.004h170.667z m-21.334-170.6c17.673 0 32 14.327 32 32 0 17.496-14.041 31.713-31.47 31.996l-0.53 0.004H256c-17.673 0-32-14.327-32-32 0-17.496 14.042-31.713 31.47-31.996l0.53-0.004h149.333z" p-id="100834" fill="#1aaba8"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<svg t="1767367226608" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5585" width="200" height="200"><path d="M416 64H768v64h-64v704h64v64H448v-64h64V512H416a224 224 0 1 1 0-448zM576 832h64V128H576v704zM416 128H512v320H416a160 160 0 0 1 0-320z" fill="#a4579d" p-id="5586"></path></svg>

After

Width:  |  Height:  |  Size: 330 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767366207284" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="95138" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M753.5 130.4v763.3h70.1v66.4H477v-66.4h70.1V547.1H443.9c-44.2 0-84.8-11.1-121.7-33.2-36.9-22.1-66.4-51.6-88.5-88.5s-33.2-76.8-33.2-119.8 11.1-83.6 33.2-121.7c22.1-38.1 51.6-67.6 88.5-88.5s77.4-31.3 121.7-31.3h379.8v66.4h-70.1z m-206.5 0H443.8c-49.2 0-90.3 17.2-123.5 51.6-33.2 34.4-49.8 75.6-49.8 123.5s16.6 88.5 49.8 121.7c33.2 33.2 74.4 49.8 123.5 49.8H547V130.4z m140.1 0H617v763.3h70.1V130.4z" p-id="95139"></path></svg>

After

Width:  |  Height:  |  Size: 758 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767365935803" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="81379" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M896 64a64 64 0 0 1 64 64v613.952l-121.28-95.68a83.2 83.2 0 0 0-110.848 7.04l-6.016 6.784-5.312 7.616a83.2 83.2 0 0 0-12.032 34.56l-0.064 1.728H595.2A83.2 83.2 0 0 0 512 787.2v44.8H128a64 64 0 0 1-64-64V128a64 64 0 0 1 64-64h768z" fill="#B5E3CC" p-id="81380"></path><path d="M640 256a32 32 0 1 1 0 64h-32v224a32 32 0 0 1-64 0V320h-64v224a32 32 0 0 1-64 0V320H384a32 32 0 0 1 0-64h256z m160 0a96 96 0 0 1 0 192H768v96a32 32 0 0 1-26.24 31.488L736 576a32 32 0 0 1-32-32v-256a32 32 0 0 1 32-32h64zM768 384h32a32 32 0 1 0 0-64H768v64zM288 256a32 32 0 0 1 32 32v256a32 32 0 0 1-64 0V448H192v96a32 32 0 0 1-64 0v-256a32 32 0 0 1 64 0V384h64V288a32 32 0 0 1 32-32zM772.096 699.712a19.2 19.2 0 0 0-4.096 11.904v56.32L595.2 768a19.2 19.2 0 0 0-19.2 19.2v89.6a19.2 19.2 0 0 0 19.2 19.2H768v56.384a19.2 19.2 0 0 0 31.104 15.104l152.576-120.384a19.2 19.2 0 0 0 0-30.208l-152.576-120.32a19.2 19.2 0 0 0-27.008 3.136z" fill="#129250" p-id="81381"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767365120766" class="icon" viewBox="0 0 1160 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12589" xmlns:xlink="http://www.w3.org/1999/xlink" width="226.5625" height="200"><path d="M398.5408 736.142222l435.768889-384.568889a44.009244 44.009244 0 0 0 0-67.675022 59.483022 59.483022 0 0 0-76.731733 0l-435.768889 384.568889a43.872711 43.872711 0 0 0 0 67.675022 59.164444 59.164444 0 0 0 76.731733 0z m143.7696-66.582755a39.867733 39.867733 0 0 1 9.102222 25.031111 38.866489 38.866489 0 0 1-13.653333 30.128355L308.929422 926.151111a52.8384 52.8384 0 0 1-68.266666 0L111.047111 811.781689a39.139556 39.139556 0 0 1 0-60.302222l228.192711-201.9328a50.335289 50.335289 0 0 1 34.178845-11.969423 53.748622 53.748622 0 0 1 22.755555 4.551112l69.632-60.848356c-57.617067-41.824711-141.7216-38.365867-194.696533 7.964444L42.052267 690.631111c-56.069689 50.3808-56.069689 131.117511 0 181.4528l130.207289 114.323911a158.651733 158.651733 0 0 0 204.8 0L605.297778 785.066667c54.613333-48.196267 57.025422-125.474133 5.779911-176.355556zM1117.980444 151.916089L988.410311 37.546667c-56.797867-50.062222-148.821333-50.062222-205.664711 0L554.552889 238.933333c-52.519822 46.739911-56.388267 120.968533-9.102222 171.804445L614.4 349.889422a41.688178 41.688178 0 0 1-5.142756-20.48 38.866489 38.866489 0 0 1 13.653334-30.128355l228.875378-201.386667a50.335289 50.335289 0 0 1 34.178844-12.515556 52.383289 52.383289 0 0 1 34.178844 12.515556l129.570134 114.323911a39.139556 39.139556 0 0 1 0 60.302222l-228.192711 201.9328a50.335289 50.335289 0 0 1-34.178845 11.969423 57.7536 57.7536 0 0 1-28.353422-7.418312l-68.266667 60.302223c57.389511 45.101511 144.543289 43.099022 199.202134-4.551111l228.192711-201.9328a117.418667 117.418667 0 0 0 0-180.906667z" fill="#1A97F0" p-id="12590"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<svg t="1767366749042" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="102155" width="200" height="200"><path d="M426.7008 256c0-23.552 19.0976-42.6496 42.6496-42.6496h384a42.6496 42.6496 0 1 1 0 85.2992h-384A42.6496 42.6496 0 0 1 426.7008 256zM426.7008 512c0-23.552 19.0976-42.6496 42.6496-42.6496h384a42.6496 42.6496 0 1 1 0 85.2992h-384A42.6496 42.6496 0 0 1 426.7008 512zM469.2992 768c0-23.552 19.1488-42.6496 42.7008-42.6496h341.2992a42.6496 42.6496 0 0 1 0 85.2992H512A42.6496 42.6496 0 0 1 469.2992 768zM256 640a42.6496 42.6496 0 0 0-42.6496 42.6496 42.6496 42.6496 0 0 1-85.3504 0 128 128 0 1 1 256 0c0 25.856-11.264 45.6192-22.4256 59.8528-8.0384 10.1888-18.5856 20.48-26.8288 28.5184l-5.888 5.8368a42.4448 42.4448 0 0 1-2.8672 2.56l-37.4784 31.232h52.8384a42.6496 42.6496 0 1 1 0 85.3504H170.6496a42.6496 42.6496 0 0 1-27.2896-75.4688l126.5152-105.472 6.5024-6.3488 0.4096-0.4096 7.2704-7.168c4.608-4.608 7.936-8.192 10.3936-11.3664a28.672 28.672 0 0 0 3.9424-6.0928c0.3072-0.7168 0.256-0.9728 0.256-1.024A42.6496 42.6496 0 0 0 256 640zM272.3328 131.2256a42.6496 42.6496 0 0 1 26.3168 39.424v256a42.6496 42.6496 0 0 1-85.2992 0V273.664l-12.4928 12.4928a42.6496 42.6496 0 1 1-60.3648-60.3136l85.3504-85.3504a42.6496 42.6496 0 0 1 46.4896-9.216z" p-id="102156" fill="#87c38f"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767365625477" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="51837" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M98.649225 16.619164h734.636904v990.162784h-734.636904z" fill="#FF8A90" p-id="51838"></path><path d="M881.197231 128.411736h-31.940735V32.589531c0-17.642265-14.29847-31.940735-31.940735-31.940735H114.619592c-17.642265 0-31.940735 14.29847-31.940735 31.940735v958.222049c0 17.642265 14.29847 31.940735 31.940735 31.940735h702.696169c17.642265 0 31.940735-14.29847 31.940735-31.940735V415.878351h31.940735c17.642265 0 31.940735-14.29847 31.940735-31.940735V160.352471c0-17.642265-14.29847-31.940735-31.940735-31.940735z m-63.88147 0h-383.288819c-17.642265 0-31.940735 14.29847-31.940735 31.940735V383.937616c0 17.642265 14.29847 31.940735 31.940735 31.940735h383.288819v574.933229H114.619592v-958.222049h702.696169v95.822205z" fill="#2B3139" p-id="51839"></path><path d="M434.026942 160.352471h447.170289V383.937616h-447.170289z" fill="#FFFFFF" p-id="51840"></path><path d="M544.808889 215.175748h49.969783l16.294765 67.025636h0.311922l16.294765-67.025636h49.957306v113.963544h-33.18842v-73.101861h-0.336875l-19.80076 73.101861h-26.176431l-19.788283-73.101861h-0.336875v73.101861h-33.200897zM698.947889 215.175748h57.618092c37.992007 0 51.392143 28.097865 51.392143 56.819573 0 34.947656-18.490691 57.143971-58.241934 57.143971h-50.768301v-113.963544z" fill="#2B3139" p-id="51841"></path><path d="M726.584111 299.918511h13.712058c21.85944 0 25.065991-17.717126 25.065991-28.409787 0-7.174189-2.233356-27.112194-27.611269-27.112194h-11.154303v55.521981z" fill="#FFFFFF" p-id="51842"></path><path d="M630.512369 785.841895l-94.998733-129.709328h57.318647V544.888976h76.445658v111.243591h57.30617L630.487415 785.841895z" fill="#1EB9B0" p-id="51843"></path><path d="M508.139428 785.754557l-76.445657 0.087338v-120.439029l-57.331124 77.194268-57.343601-77.194268v120.439029H240.573389V544.976313h76.445657l57.343601 80.288528 57.331124-80.288528 76.445657-0.087337z" fill="#1EB9B0" p-id="51844"></path></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1 @@
<svg t="1767367796950" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18541" width="200" height="200"><path d="M880.489336 512H993.882278v455.107765A56.786824 56.786824 0 0 1 937.185807 1024h-56.681412V512z" fill="#4AC3BB" fill-opacity=".603" p-id="18542"></path><path d="M143.510513 1024a113.182118 113.182118 0 0 1-80.188235-33.325176A113.980235 113.980235 0 0 1 30.117572 910.215529V113.784471C30.117572 83.606588 42.059219 54.663529 63.322278 33.325176A113.182118 113.182118 0 0 1 143.510513 0h510.238118C778.977807 0 880.489336 101.872941 880.489336 227.553882v739.553883A56.786824 56.786824 0 0 0 937.185807 1024H143.510513z" fill="#4AC3BB" p-id="18543"></path><path d="M575.653572 335.329882c64.331294 65.024 66.529882 169.758118 4.954353 237.477647l-4.954353 5.240471-98.063059 122.473412a28.175059 28.175059 0 0 1-21.985882 10.586353 28.175059 28.175059 0 0 1-21.985883-10.586353l-98.078117-122.473412-4.412236-4.638118c-63.503059-68.487529-60.777412-175.841882 6.098824-240.941176a168.478118 168.478118 0 0 1 238.426353 2.861176z m-120.048941 64.150589a56.470588 56.470588 0 0 0-49.016471 28.611764 57.735529 57.735529 0 0 0 0 57.193412 56.470588 56.470588 0 0 0 49.016471 28.611765c31.247059-0.015059 56.576-25.630118 56.576-57.22353 0-31.578353-25.328941-57.193412-56.576-57.193411z" fill="#FFFFFF" fill-opacity=".95" p-id="18544"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767364585914" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8006" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M960 128c0-35.36-28.704-64.224-63.936-61.728A896.48 896.48 0 0 0 326.4 326.4a896.48 896.48 0 0 0-260.128 569.664C63.776 931.296 92.64 960 128 960c0 0 19.52 0.576 32 0 64-272 84.48-383.68 234.24-533.76C544.32 276.16 752 192 960 160V128z" fill="#F8312F" p-id="8007"></path><path d="M960 256V160a800 800 0 0 0-565.76 234.24A801.344 801.344 0 0 0 160 960h96c32-128 106.24-333.76 238.08-465.92C625.92 362.24 752 304 960 256z" fill="#FFB02E" p-id="8008"></path><path d="M960 256v96c-160 16-284.16 96-398.08 209.92C448 676.16 384 800 352 960H256c0-186.56 74.24-365.76 206.08-497.92A704.32 704.32 0 0 1 960 256z" fill="#FFF478" p-id="8009"></path><path d="M630.08 630.08C534.08 726.08 480 832 448 960h-96c0-161.28 64-315.84 177.92-430.08C643.84 416 798.72 352 960 352v96c-144 32-233.92 86.08-329.92 182.08z" fill="#00D26A" p-id="8010"></path><path d="M960 544v-96c-135.68 0-265.92 54.08-361.92 150.08-96 96-150.08 226.24-150.08 361.92h96c0-110.4 43.84-216.32 121.92-294.08C744 587.84 849.92 544 960 544z" fill="#3F5FFF" p-id="8011"></path><path d="M960 576c0 35.36-28.928 63.36-63.584 70.336a320.384 320.384 0 0 0-250.112 250.08C639.36 931.04 611.392 960 576 960h-32c0-110.4 43.84-216.32 121.92-294.08C744 587.84 849.92 544 960 544v32z" fill="#8D65C5" p-id="8012"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<svg t="1767366992790" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="111373" width="200" height="200"><path d="M581.973333 846.933333a380.8 380.8 0 1 1 380.8-380.8A381.226667 381.226667 0 0 1 581.973333 846.933333z m0-688a307.2 307.2 0 1 0 307.2 307.2 307.413333 307.413333 0 0 0-307.2-307.2z" fill="#FA6302" p-id="111374"></path><path d="M146.56 938.666667a36.906667 36.906667 0 0 1-26.026667-64l192-190.933334a36.906667 36.906667 0 0 1 52.053334 52.266667l-192 192a37.333333 37.333333 0 0 1-26.026667 10.666667z" fill="#43D7B4" p-id="111375"></path><path d="M470.826667 274.773333m-49.066667 0a49.066667 49.066667 0 1 0 98.133333 0 49.066667 49.066667 0 1 0-98.133333 0Z" fill="#43D7B4" p-id="111376"></path><path d="M312.106667 684.8l-23.68 23.466667A388.693333 388.693333 0 0 0 341.333333 760.32l23.466667-23.253333a36.906667 36.906667 0 0 0-52.053333-52.266667z" fill="#425300" p-id="111377"></path></svg>

After

Width:  |  Height:  |  Size: 956 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -6,7 +6,10 @@ import {useKeybindingStore} from '@/stores/keybindingStore';
import {useThemeStore} from '@/stores/themeStore'; import {useThemeStore} from '@/stores/themeStore';
import {useUpdateStore} from '@/stores/updateStore'; import {useUpdateStore} from '@/stores/updateStore';
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue'; import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
import ToastContainer from '@/components/toast/ToastContainer.vue';
import {useTranslationStore} from "@/stores/translationStore"; import {useTranslationStore} from "@/stores/translationStore";
import {useI18n} from "vue-i18n";
import {LanguageType} from "../bindings/voidraft/internal/models";
const configStore = useConfigStore(); const configStore = useConfigStore();
const systemStore = useSystemStore(); const systemStore = useSystemStore();
@@ -14,6 +17,7 @@ const keybindingStore = useKeybindingStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const updateStore = useUpdateStore(); const updateStore = useUpdateStore();
const translationStore = useTranslationStore(); const translationStore = useTranslationStore();
const {locale} = useI18n();
onBeforeMount(async () => { onBeforeMount(async () => {
// 并行初始化配置、系统信息和快捷键配置 // 并行初始化配置、系统信息和快捷键配置
@@ -22,9 +26,8 @@ onBeforeMount(async () => {
systemStore.initSystemInfo(), systemStore.initSystemInfo(),
keybindingStore.loadKeyBindings(), keybindingStore.loadKeyBindings(),
]); ]);
// 初始化语言和主题 locale.value = configStore.config.appearance.language || LanguageType.LangEnUS;
await configStore.initLanguage();
await themeStore.initTheme(); await themeStore.initTheme();
await translationStore.loadTranslators(); await translationStore.loadTranslators();
@@ -39,6 +42,7 @@ onBeforeMount(async () => {
<div class="app-content"> <div class="app-content">
<router-view/> <router-view/>
</div> </div>
<ToastContainer/>
</div> </div>
</template> </template>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -5,7 +5,6 @@ import {
LanguageType, LanguageType,
SystemThemeType, SystemThemeType,
TabType, TabType,
UpdateSourceType
} from '@/../bindings/voidraft/internal/models/models'; } from '@/../bindings/voidraft/internal/models/models';
import {FONT_OPTIONS} from './fonts'; import {FONT_OPTIONS} from './fonts';
@@ -24,6 +23,7 @@ export const CONFIG_KEY_MAP = {
enableWindowSnap: 'general.enableWindowSnap', enableWindowSnap: 'general.enableWindowSnap',
enableLoadingAnimation: 'general.enableLoadingAnimation', enableLoadingAnimation: 'general.enableLoadingAnimation',
enableTabs: 'general.enableTabs', enableTabs: 'general.enableTabs',
enableMemoryMonitor: 'general.enableMemoryMonitor',
// editing // editing
fontSize: 'editing.fontSize', fontSize: 'editing.fontSize',
fontFamily: 'editing.fontFamily', fontFamily: 'editing.fontFamily',
@@ -88,6 +88,7 @@ export const DEFAULT_CONFIG: AppConfig = {
enableWindowSnap: true, enableWindowSnap: true,
enableLoadingAnimation: true, enableLoadingAnimation: true,
enableTabs: false, enableTabs: false,
enableMemoryMonitor: true,
}, },
editing: { editing: {
fontSize: CONFIG_LIMITS.fontSize.default, fontSize: CONFIG_LIMITS.fontSize.default,
@@ -108,19 +109,12 @@ export const DEFAULT_CONFIG: AppConfig = {
updates: { updates: {
version: "1.0.0", version: "1.0.0",
autoUpdate: true, autoUpdate: true,
primarySource: UpdateSourceType.UpdateSourceGithub,
backupSource: UpdateSourceType.UpdateSourceGitea,
backupBeforeUpdate: true, backupBeforeUpdate: true,
updateTimeout: 30, updateTimeout: 120,
github: { github: {
owner: "landaiqing", owner: "landaiqing",
repo: "voidraft", repo: "voidraft",
}, },
gitea: {
baseURL: "https://git.landaiqing.cn",
owner: "landaiqing",
repo: "voidraft",
}
}, },
backup: { backup: {
enabled: false, enabled: false,

View File

@@ -5,7 +5,7 @@
// 编辑器实例管理 // 编辑器实例管理
export const EDITOR_CONFIG = { export const EDITOR_CONFIG = {
/** 最多缓存的编辑器实例数量 */ /** 最多缓存的编辑器实例数量 */
MAX_INSTANCES: 5, MAX_INSTANCES: 10,
/** 语法树缓存过期时间(毫秒) */ /** 语法树缓存过期时间(毫秒) */
SYNTAX_TREE_CACHE_TIMEOUT: 30000, SYNTAX_TREE_CACHE_TIMEOUT: 30000,
/** 加载状态延迟时间(毫秒) */ /** 加载状态延迟时间(毫秒) */

View File

@@ -35,7 +35,6 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
// Helper methods // Helper methods
public mapVisit: (elements: TomlCstNode[] | undefined) => (Doc | string)[]; public mapVisit: (elements: TomlCstNode[] | undefined) => (Doc | string)[];
public visitSingle: (ctx: TomlContext) => Doc | string; public visitSingle: (ctx: TomlContext) => Doc | string;
public visit: (ctx: TomlCstNode, inParam?: any) => Doc | string;
constructor() { constructor() {
super(); super();
@@ -57,26 +56,38 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
const singleElement = getSingle(ctx); const singleElement = getSingle(ctx);
return this.visit(singleElement); return this.visit(singleElement);
}; };
}
// Store reference to inherited visit method and override it /**
const originalVisit = Object.getPrototypeOf(this).visit?.bind(this); * Override visit method to handle TOML CST nodes
this.visit = (ctx: TomlCstNode, inParam?: any): Doc | string => { * Accepts both single node and array of nodes as per base class signature
if (!ctx) { */
return ''; visit(cstNode: any, param?: any): any {
} // Handle array of nodes
if (Array.isArray(cstNode)) {
return cstNode.map(node => this.visit(node, param));
}
const ctx = cstNode;
if (!ctx) {
return '';
}
// 确保节点有name属性才调用基类方法
if (ctx.name) {
// Try to use the inherited visit method first // Try to use the inherited visit method first
const originalVisit = super.visit;
if (originalVisit) { if (originalVisit) {
try { try {
return originalVisit(ctx, inParam); return originalVisit.call(this, ctx, param);
} catch (error) { } catch (error) {
console.warn('Original visit method failed:', error); // Fallback to manual dispatch
} }
} }
// Fallback: manually dispatch based on node name/type // Fallback: manually dispatch based on node name/type
const methodName = ctx.name; const methodName = ctx.name;
if (methodName && typeof (this as any)[methodName] === 'function') { if (typeof (this as any)[methodName] === 'function') {
const visitMethod = (this as any)[methodName]; const visitMethod = (this as any)[methodName];
try { try {
if (ctx.children) { if (ctx.children) {
@@ -88,16 +99,16 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
console.warn(`Visit method ${methodName} failed:`, error); console.warn(`Visit method ${methodName} failed:`, error);
} }
} }
}
// Final fallback: return image if available
return ctx.image || ''; // Final fallback: return image if available
}; return ctx.image || '';
} }
/** /**
* Visit the root TOML document * Visit the root TOML document
*/ */
toml(ctx: TomlDocument): Doc { toml(ctx: any): Doc {
// Handle empty toml document // Handle empty toml document
if (!ctx.expression) { if (!ctx.expression) {
return [line]; return [line];
@@ -164,7 +175,7 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
/** /**
* Visit an expression (keyval, table, or comment) * Visit an expression (keyval, table, or comment)
*/ */
expression(ctx: TomlExpression): Doc | string { expression(ctx: any): Doc | string {
if (ctx.keyval) { if (ctx.keyval) {
let keyValDoc = this.visit(ctx.keyval[0]); let keyValDoc = this.visit(ctx.keyval[0]);
if (ctx.Comment) { if (ctx.Comment) {
@@ -189,7 +200,7 @@ class TomlBeautifierVisitor extends BaseTomlCstVisitor {
/** /**
* Visit a key-value pair * Visit a key-value pair
*/ */
keyval(ctx: TomlKeyVal): Doc { keyval(ctx: any): Doc {
const keyDoc = this.visit(ctx.key[0]); const keyDoc = this.visit(ctx.key[0]);
const valueDoc = this.visit(ctx.val[0]); const valueDoc = this.visit(ctx.val[0]);
return [keyDoc, ' = ', valueDoc]; return [keyDoc, ' = ', valueDoc];

View File

@@ -1,265 +0,0 @@
/**
* 操作信息接口
*/
interface OperationInfo {
controller: AbortController;
createdAt: number;
timeout?: number;
timeoutId?: NodeJS.Timeout;
}
/**
* 异步操作管理器
* 用于管理异步操作的竞态条件,确保只有最新的操作有效
* 支持操作超时和自动清理机制
*
* @template T 操作上下文的类型
*/
export class AsyncManager<T = any> {
private operationSequence = 0;
private pendingOperations = new Map<number, OperationInfo>();
private currentContext: T | null = null;
private defaultTimeout: number;
/**
* 创建异步操作管理器
*
* @param defaultTimeout 默认超时时间毫秒0表示不设置超时
*/
constructor(defaultTimeout: number = 0) {
this.defaultTimeout = defaultTimeout;
}
/**
* 生成新的操作ID
*
* @returns 新的操作ID
*/
getNextOperationId(): number {
return ++this.operationSequence;
}
/**
* 开始新的操作
*
* @param context 操作上下文
* @param options 操作选项
* @returns 操作ID和AbortController
*/
startOperation(
context: T,
options?: {
excludeId?: number;
timeout?: number;
}
): { operationId: number; abortController: AbortController } {
const operationId = this.getNextOperationId();
const abortController = new AbortController();
const timeout = options?.timeout ?? this.defaultTimeout;
// 取消之前的操作
this.cancelPreviousOperations(options?.excludeId);
// 创建操作信息
const operationInfo: OperationInfo = {
controller: abortController,
createdAt: Date.now(),
timeout: timeout > 0 ? timeout : undefined
};
// 设置超时处理
if (timeout > 0) {
operationInfo.timeoutId = setTimeout(() => {
this.cancelOperation(operationId, 'timeout');
}, timeout);
}
// 设置当前上下文和操作
this.currentContext = context;
this.pendingOperations.set(operationId, operationInfo);
return { operationId, abortController };
}
/**
* 检查操作是否仍然有效
*
* @param operationId 操作ID
* @param context 操作上下文
* @returns 操作是否有效
*/
isOperationValid(operationId: number, context?: T): boolean {
const operationInfo = this.pendingOperations.get(operationId);
const contextValid = context === undefined || this.currentContext === context;
return (
operationInfo !== undefined &&
!operationInfo.controller.signal.aborted &&
contextValid
);
}
/**
* 完成操作
*
* @param operationId 操作ID
*/
completeOperation(operationId: number): void {
const operationInfo = this.pendingOperations.get(operationId);
if (operationInfo) {
// 清理超时定时器
if (operationInfo.timeoutId) {
clearTimeout(operationInfo.timeoutId);
}
this.pendingOperations.delete(operationId);
}
}
/**
* 取消指定操作
*
* @param operationId 操作ID
* @param reason 取消原因
*/
cancelOperation(operationId: number, reason?: string): void {
const operationInfo = this.pendingOperations.get(operationId);
if (operationInfo) {
// 清理超时定时器
if (operationInfo.timeoutId) {
clearTimeout(operationInfo.timeoutId);
}
// 取消操作
operationInfo.controller.abort(reason);
this.pendingOperations.delete(operationId);
}
}
/**
* 取消之前的操作修复并发bug
*
* @param excludeId 要排除的操作ID不取消该操作
*/
cancelPreviousOperations(excludeId?: number): void {
// 创建要取消的操作ID数组避免在遍历时修改Map
const operationIdsToCancel: number[] = [];
for (const [operationId] of this.pendingOperations) {
if (excludeId === undefined || operationId !== excludeId) {
operationIdsToCancel.push(operationId);
}
}
// 批量取消操作
for (const operationId of operationIdsToCancel) {
this.cancelOperation(operationId, 'superseded');
}
}
/**
* 取消所有操作
*/
cancelAllOperations(): void {
// 创建要取消的操作ID数组避免在遍历时修改Map
const operationIdsToCancel = Array.from(this.pendingOperations.keys());
// 批量取消操作
for (const operationId of operationIdsToCancel) {
this.cancelOperation(operationId, 'cancelled');
}
this.currentContext = null;
}
/**
* 清理过期操作(手动清理超时操作)
*
* @param maxAge 最大存活时间(毫秒)
* @returns 清理的操作数量
*/
cleanupExpiredOperations(maxAge: number): number {
const now = Date.now();
const expiredOperationIds: number[] = [];
for (const [operationId, operationInfo] of this.pendingOperations) {
if (now - operationInfo.createdAt > maxAge) {
expiredOperationIds.push(operationId);
}
}
// 批量取消过期操作
for (const operationId of expiredOperationIds) {
this.cancelOperation(operationId, 'expired');
}
return expiredOperationIds.length;
}
/**
* 获取操作统计信息
*
* @returns 操作统计信息
*/
getOperationStats(): {
total: number;
withTimeout: number;
averageAge: number;
oldestAge: number;
} {
const now = Date.now();
let withTimeout = 0;
let totalAge = 0;
let oldestAge = 0;
for (const operationInfo of this.pendingOperations.values()) {
const age = now - operationInfo.createdAt;
totalAge += age;
oldestAge = Math.max(oldestAge, age);
if (operationInfo.timeout) {
withTimeout++;
}
}
return {
total: this.pendingOperations.size,
withTimeout,
averageAge: this.pendingOperations.size > 0 ? totalAge / this.pendingOperations.size : 0,
oldestAge
};
}
/**
* 获取当前上下文
*
* @returns 当前上下文
*/
getCurrentContext(): T | null {
return this.currentContext;
}
/**
* 设置当前上下文
*
* @param context 新的上下文
*/
setCurrentContext(context: T | null): void {
this.currentContext = context;
}
/**
* 获取待处理操作数量
*
* @returns 待处理操作数量
*/
get pendingCount(): number {
return this.pendingOperations.size;
}
/**
* 检查是否有待处理的操作
*
* @returns 是否有待处理的操作
*/
hasPendingOperations(): boolean {
return this.pendingOperations.size > 0;
}
}

View File

@@ -1,42 +1,13 @@
import { LanguageType } from '@/../bindings/voidraft/internal/models/models';
import type { SupportedLocaleType } from '@/common/constant/locales';
/** /**
* 配置工具类 * 配置工具类
*/ */
export class ConfigUtils { export class ConfigUtils {
/**
* 将后端语言类型转换为前端语言代码
*/
static backendLanguageToFrontend(language: LanguageType): SupportedLocaleType {
return language === LanguageType.LangZhCN ? 'zh-CN' : 'en-US';
}
/** /**
* 将前端语言代码转换为后端语言类型 * 验证数值是否在指定范围内
*/ */
static frontendLanguageToBackend(locale: SupportedLocaleType): LanguageType { static clamp(value: number, min: number, max: number): number {
return locale === 'zh-CN' ? LanguageType.LangZhCN : LanguageType.LangEnUS; return Math.max(min, Math.min(max, value));
} }
/**
* 验证数值是否在指定范围内
*/
static clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
/**
* 验证配置值是否有效
*/
static isValidConfigValue<T>(value: T, validValues: readonly T[]): boolean {
return validValues.includes(value);
}
/**
* 获取配置的默认值
*/
static getDefaultValue<T>(key: string, defaults: Record<string, { default: T }>): T {
return defaults[key]?.default;
}
} }

View File

@@ -1,329 +0,0 @@
/**
* DOM Diff 算法单元测试
*/
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
import { morphNode, morphHTML, morphWithKeys } from './domDiff';
describe('DOM Diff Algorithm', () => {
let container: HTMLElement;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
describe('morphNode - 基础功能', () => {
test('应该更新文本节点内容', () => {
const fromNode = document.createTextNode('Hello');
const toNode = document.createTextNode('World');
container.appendChild(fromNode);
morphNode(fromNode, toNode);
expect(fromNode.nodeValue).toBe('World');
});
test('应该保持相同的文本节点不变', () => {
const fromNode = document.createTextNode('Hello');
const toNode = document.createTextNode('Hello');
container.appendChild(fromNode);
const originalNode = fromNode;
morphNode(fromNode, toNode);
expect(fromNode).toBe(originalNode);
expect(fromNode.nodeValue).toBe('Hello');
});
test('应该替换不同类型的节点', () => {
const fromNode = document.createElement('span');
fromNode.textContent = 'Hello';
const toNode = document.createElement('div');
toNode.textContent = 'World';
container.appendChild(fromNode);
morphNode(fromNode, toNode);
expect(container.firstChild?.nodeName).toBe('DIV');
expect(container.firstChild?.textContent).toBe('World');
});
});
describe('morphNode - 属性更新', () => {
test('应该添加新属性', () => {
const fromEl = document.createElement('div');
const toEl = document.createElement('div');
toEl.setAttribute('class', 'test');
toEl.setAttribute('id', 'myid');
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.getAttribute('class')).toBe('test');
expect(fromEl.getAttribute('id')).toBe('myid');
});
test('应该更新已存在的属性', () => {
const fromEl = document.createElement('div');
fromEl.setAttribute('class', 'old');
const toEl = document.createElement('div');
toEl.setAttribute('class', 'new');
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.getAttribute('class')).toBe('new');
});
test('应该删除不存在的属性', () => {
const fromEl = document.createElement('div');
fromEl.setAttribute('class', 'test');
fromEl.setAttribute('id', 'myid');
const toEl = document.createElement('div');
toEl.setAttribute('class', 'test');
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.getAttribute('class')).toBe('test');
expect(fromEl.hasAttribute('id')).toBe(false);
});
});
describe('morphNode - 子节点更新', () => {
test('应该添加新子节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = '<li>1</li><li>2</li>';
const toEl = document.createElement('ul');
toEl.innerHTML = '<li>1</li><li>2</li><li>3</li>';
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.children.length).toBe(3);
expect(fromEl.children[2].textContent).toBe('3');
});
test('应该删除多余的子节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = '<li>1</li><li>2</li><li>3</li>';
const toEl = document.createElement('ul');
toEl.innerHTML = '<li>1</li><li>2</li>';
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.children.length).toBe(2);
expect(fromEl.textContent).toBe('12');
});
test('应该更新子节点内容', () => {
const fromEl = document.createElement('div');
fromEl.innerHTML = '<p>Old</p>';
const toEl = document.createElement('div');
toEl.innerHTML = '<p>New</p>';
container.appendChild(fromEl);
const originalP = fromEl.querySelector('p');
morphNode(fromEl, toEl);
// 应该保持同一个 p 元素,只更新内容
expect(fromEl.querySelector('p')).toBe(originalP);
expect(fromEl.querySelector('p')?.textContent).toBe('New');
});
});
describe('morphHTML - HTML 字符串更新', () => {
test('应该从 HTML 字符串更新元素', () => {
const element = document.createElement('div');
element.innerHTML = '<p>Old</p>';
container.appendChild(element);
morphHTML(element, '<p>New</p>');
expect(element.innerHTML).toBe('<p>New</p>');
});
test('应该处理复杂的 HTML 结构', () => {
const element = document.createElement('div');
element.innerHTML = '<h1>Title</h1><p>Paragraph</p>';
container.appendChild(element);
morphHTML(element, '<h1>New Title</h1><p>New Paragraph</p><span>Extra</span>');
expect(element.children.length).toBe(3);
expect(element.querySelector('h1')?.textContent).toBe('New Title');
expect(element.querySelector('p')?.textContent).toBe('New Paragraph');
expect(element.querySelector('span')?.textContent).toBe('Extra');
});
});
describe('morphWithKeys - 基于 key 的智能 diff', () => {
test('应该保持相同 key 的节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="b">B</li>
<li data-key="c">C</li>
`;
const toEl = document.createElement('ul');
toEl.innerHTML = `
<li data-key="a">A Updated</li>
<li data-key="b">B</li>
<li data-key="c">C</li>
`;
container.appendChild(fromEl);
const originalA = fromEl.querySelector('[data-key="a"]');
morphWithKeys(fromEl, toEl);
expect(fromEl.querySelector('[data-key="a"]')).toBe(originalA);
expect(originalA?.textContent).toBe('A Updated');
});
test('应该重新排序节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="b">B</li>
<li data-key="c">C</li>
`;
const toEl = document.createElement('ul');
toEl.innerHTML = `
<li data-key="c">C</li>
<li data-key="a">A</li>
<li data-key="b">B</li>
`;
container.appendChild(fromEl);
morphWithKeys(fromEl, toEl);
const keys = Array.from(fromEl.children).map(child => child.getAttribute('data-key'));
expect(keys).toEqual(['c', 'a', 'b']);
});
test('应该添加新的 key 节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="b">B</li>
`;
const toEl = document.createElement('ul');
toEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="b">B</li>
<li data-key="c">C</li>
`;
container.appendChild(fromEl);
morphWithKeys(fromEl, toEl);
expect(fromEl.children.length).toBe(3);
expect(fromEl.querySelector('[data-key="c"]')?.textContent).toBe('C');
});
test('应该删除不存在的 key 节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="b">B</li>
<li data-key="c">C</li>
`;
const toEl = document.createElement('ul');
toEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="c">C</li>
`;
container.appendChild(fromEl);
morphWithKeys(fromEl, toEl);
expect(fromEl.children.length).toBe(2);
expect(fromEl.querySelector('[data-key="b"]')).toBeNull();
});
});
describe('性能测试', () => {
test('应该高效处理大量节点', () => {
const fromEl = document.createElement('ul');
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fromEl.appendChild(li);
}
const toEl = document.createElement('ul');
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Updated Item ${i}`;
toEl.appendChild(li);
}
container.appendChild(fromEl);
const startTime = performance.now();
morphNode(fromEl, toEl);
const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(100); // 应该在 100ms 内完成
expect(fromEl.children.length).toBe(1000);
expect(fromEl.children[0].textContent).toBe('Updated Item 0');
});
});
describe('边界情况', () => {
test('应该处理空节点', () => {
const fromEl = document.createElement('div');
const toEl = document.createElement('div');
container.appendChild(fromEl);
expect(() => morphNode(fromEl, toEl)).not.toThrow();
});
test('应该处理只有文本的节点', () => {
const fromEl = document.createElement('div');
fromEl.textContent = 'Hello';
const toEl = document.createElement('div');
toEl.textContent = 'World';
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.textContent).toBe('World');
});
test('应该处理嵌套的复杂结构', () => {
const fromEl = document.createElement('div');
fromEl.innerHTML = `
<div class="outer">
<div class="inner">
<span>Text</span>
</div>
</div>
`;
const toEl = document.createElement('div');
toEl.innerHTML = `
<div class="outer modified">
<div class="inner">
<span>Updated Text</span>
<strong>New</strong>
</div>
</div>
`;
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.querySelector('.outer')?.classList.contains('modified')).toBe(true);
expect(fromEl.querySelector('span')?.textContent).toBe('Updated Text');
expect(fromEl.querySelector('strong')?.textContent).toBe('New');
});
});
});

View File

@@ -1,180 +0,0 @@
/**
* 轻量级 DOM Diff 算法实现
* 基于 morphdom 思路,只更新变化的节点,保持未变化的节点不动
*/
/**
* 比较并更新两个 DOM 节点
* @param fromNode 原节点
* @param toNode 目标节点
*/
export function morphNode(fromNode: Node, toNode: Node): void {
// 节点类型不同,直接替换
if (fromNode.nodeType !== toNode.nodeType || fromNode.nodeName !== toNode.nodeName) {
fromNode.parentNode?.replaceChild(toNode.cloneNode(true), fromNode);
return;
}
// 文本节点:比较内容
if (fromNode.nodeType === Node.TEXT_NODE) {
if (fromNode.nodeValue !== toNode.nodeValue) {
fromNode.nodeValue = toNode.nodeValue;
}
return;
}
// 元素节点:更新属性和子节点
if (fromNode.nodeType === Node.ELEMENT_NODE) {
const fromEl = fromNode as Element;
const toEl = toNode as Element;
// 更新属性
morphAttributes(fromEl, toEl);
// 更新子节点
morphChildren(fromEl, toEl);
}
}
/**
* 更新元素属性
*/
function morphAttributes(fromEl: Element, toEl: Element): void {
// 移除旧属性
const fromAttrs = fromEl.attributes;
for (let i = fromAttrs.length - 1; i >= 0; i--) {
const attr = fromAttrs[i];
if (!toEl.hasAttribute(attr.name)) {
fromEl.removeAttribute(attr.name);
}
}
// 添加/更新新属性
const toAttrs = toEl.attributes;
for (let i = 0; i < toAttrs.length; i++) {
const attr = toAttrs[i];
const fromValue = fromEl.getAttribute(attr.name);
if (fromValue !== attr.value) {
fromEl.setAttribute(attr.name, attr.value);
}
}
}
/**
* 更新子节点(核心 diff 算法)
*/
function morphChildren(fromEl: Element, toEl: Element): void {
const fromChildren = Array.from(fromEl.childNodes);
const toChildren = Array.from(toEl.childNodes);
const fromLen = fromChildren.length;
const toLen = toChildren.length;
const minLen = Math.min(fromLen, toLen);
// 1. 更新公共部分
for (let i = 0; i < minLen; i++) {
morphNode(fromChildren[i], toChildren[i]);
}
// 2. 移除多余的旧节点
if (fromLen > toLen) {
for (let i = fromLen - 1; i >= toLen; i--) {
fromEl.removeChild(fromChildren[i]);
}
}
// 3. 添加新节点
if (toLen > fromLen) {
for (let i = fromLen; i < toLen; i++) {
fromEl.appendChild(toChildren[i].cloneNode(true));
}
}
}
/**
* 优化版:使用 key 进行更智能的 diff可选
* 适用于有 data-key 属性的元素
*/
export function morphWithKeys(fromEl: Element, toEl: Element): void {
const toChildren = Array.from(toEl.children) as Element[];
// 构建 from 的 key 映射
const fromKeyMap = new Map<string, Element>();
Array.from(fromEl.children).forEach((child) => {
const key = child.getAttribute('data-key');
if (key) {
fromKeyMap.set(key, child);
}
});
const processedKeys = new Set<string>();
// 按照 toChildren 的顺序处理
toChildren.forEach((toChild, toIndex) => {
const key = toChild.getAttribute('data-key');
if (!key) return;
processedKeys.add(key);
const fromChild = fromKeyMap.get(key);
if (fromChild) {
// 找到对应节点,更新内容
morphNode(fromChild, toChild);
// 确保节点在正确的位置
const currentNode = fromEl.children[toIndex];
if (currentNode !== fromChild) {
// 将 fromChild 移动到正确位置
fromEl.insertBefore(fromChild, currentNode);
}
} else {
// 新节点,插入到正确位置
const currentNode = fromEl.children[toIndex];
fromEl.insertBefore(toChild.cloneNode(true), currentNode || null);
}
});
// 删除不再存在的节点(从后往前删除,避免索引问题)
const childrenToRemove: Element[] = [];
fromKeyMap.forEach((child, key) => {
if (!processedKeys.has(key)) {
childrenToRemove.push(child);
}
});
childrenToRemove.forEach(child => {
fromEl.removeChild(child);
});
}
/**
* 高级 API直接从 HTML 字符串更新元素
*/
export function morphHTML(element: Element, htmlString: string): void {
const tempContainer = document.createElement('div');
tempContainer.innerHTML = htmlString;
// 更新元素的子节点列表
morphChildren(element, tempContainer);
}
/**
* 批量更新(使用 DocumentFragment
*/
export function batchMorph(element: Element, htmlString: string): void {
const tempContainer = document.createElement('div');
tempContainer.innerHTML = htmlString;
const fragment = document.createDocumentFragment();
Array.from(tempContainer.childNodes).forEach(node => {
fragment.appendChild(node);
});
// 清空原内容
while (element.firstChild) {
element.removeChild(element.firstChild);
}
// 批量插入
element.appendChild(fragment);
}

View File

@@ -0,0 +1,105 @@
<script setup lang="ts">
import { provide, ref } from 'vue';
interface Props {
/**
* 是否允许多个面板同时展开
* @default false - 单选模式(手风琴效果)
*/
multiple?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
multiple: false,
});
// 当前展开的项(单选模式)或展开项列表(多选模式)
const expandedItems = ref<Set<string | number>>(new Set());
/**
* 切换展开状态
*/
const toggleItem = (id: string | number) => {
if (props.multiple) {
// 多选模式:切换单个项
if (expandedItems.value.has(id)) {
expandedItems.value.delete(id);
} else {
expandedItems.value.add(id);
}
} else {
// 单选模式:只能展开一个
if (expandedItems.value.has(id)) {
expandedItems.value.clear();
} else {
expandedItems.value.clear();
expandedItems.value.add(id);
}
}
// 触发响应式更新
expandedItems.value = new Set(expandedItems.value);
};
/**
* 检查项是否展开
*/
const isExpanded = (id: string | number): boolean => {
return expandedItems.value.has(id);
};
/**
* 展开指定项
*/
const expand = (id: string | number) => {
if (!props.multiple) {
expandedItems.value.clear();
}
expandedItems.value.add(id);
expandedItems.value = new Set(expandedItems.value);
};
/**
* 收起指定项
*/
const collapse = (id: string | number) => {
expandedItems.value.delete(id);
expandedItems.value = new Set(expandedItems.value);
};
/**
* 收起所有项
*/
const collapseAll = () => {
expandedItems.value.clear();
expandedItems.value = new Set(expandedItems.value);
};
// 通过 provide 向子组件提供状态和方法
provide('accordion', {
toggleItem,
isExpanded,
expand,
collapse,
});
// 暴露方法供父组件使用
defineExpose({
expand,
collapse,
collapseAll,
});
</script>
<template>
<div class="accordion-container">
<slot></slot>
</div>
</template>
<style scoped lang="scss">
.accordion-container {
display: flex;
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,187 @@
<script setup lang="ts">
import { inject, computed, ref } from 'vue';
interface Props {
/**
* 唯一标识符
*/
id: string | number;
/**
* 标题
*/
title?: string;
/**
* 是否禁用
*/
disabled?: boolean;
}
const props = defineProps<Props>();
const accordion = inject<{
toggleItem: (id: string | number) => void;
isExpanded: (id: string | number) => boolean;
}>('accordion');
if (!accordion) {
throw new Error('AccordionItem must be used within AccordionContainer');
}
const isExpanded = computed(() => accordion.isExpanded(props.id));
const toggle = () => {
if (!props.disabled) {
accordion.toggleItem(props.id);
}
};
// 内容容器的引用,用于计算高度
const contentRef = ref<HTMLElement>();
const contentHeight = computed(() => {
if (!contentRef.value) return '0px';
return isExpanded.value ? `${contentRef.value.scrollHeight}px` : '0px';
});
</script>
<template>
<div
class="accordion-item"
:class="{
'is-expanded': isExpanded,
'is-disabled': disabled
}"
>
<!-- 标题栏 -->
<div
class="accordion-header"
@click="toggle"
:aria-expanded="isExpanded"
:aria-disabled="disabled"
role="button"
tabindex="0"
@keydown.enter="toggle"
@keydown.space.prevent="toggle"
>
<div class="accordion-title">
<slot name="title">
{{ title }}
</slot>
</div>
<div class="accordion-icon">
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 4.5L6 7.5L9 4.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
</div>
<!-- 内容区域 -->
<div
class="accordion-content-wrapper"
:style="{ height: contentHeight }"
>
<div
ref="contentRef"
class="accordion-content"
>
<div class="accordion-content-inner">
<slot></slot>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.accordion-item {
border-bottom: 1px solid var(--settings-border);
transition: background-color 0.2s ease;
&:last-child {
border-bottom: none;
}
&.is-expanded {
background-color: var(--settings-hover);
}
&.is-disabled {
opacity: 0.5;
cursor: not-allowed;
.accordion-header {
cursor: not-allowed;
}
}
}
.accordion-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
cursor: pointer;
user-select: none;
transition: background-color 0.2s ease;
&:hover:not([aria-disabled="true"]) {
background-color: var(--settings-hover);
}
&:focus-visible {
outline: 2px solid #4a9eff;
outline-offset: -2px;
}
}
.accordion-title {
flex: 1;
font-size: 13px;
font-weight: 500;
color: var(--settings-text);
display: flex;
align-items: center;
gap: 8px;
}
.accordion-icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
color: var(--text-muted);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
.is-expanded & {
transform: rotate(180deg);
}
}
.accordion-content-wrapper {
overflow: hidden;
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.accordion-content {
// 用于测量实际内容高度
}
.accordion-content-inner {
padding: 0 16px 12px 16px;
color: var(--settings-text);
font-size: 13px;
}
</style>

View File

@@ -0,0 +1,3 @@
export { default as AccordionContainer } from './AccordionContainer.vue';
export { default as AccordionItem } from './AccordionItem.vue';

View File

@@ -7,7 +7,7 @@
v-for="tab in tabStore.tabs" v-for="tab in tabStore.tabs"
:key="tab.documentId" :key="tab.documentId"
:tab="tab" :tab="tab"
:isActive="tab.documentId === tabStore.currentDocumentId" :isActive="tab.documentId === documentStore.currentDocumentId"
:canClose="tabStore.canCloseTab" :canClose="tabStore.canCloseTab"
@click="switchToTab" @click="switchToTab"
@close="closeTab" @close="closeTab"
@@ -35,8 +35,14 @@ import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import TabItem from './TabItem.vue'; import TabItem from './TabItem.vue';
import TabContextMenu from './TabContextMenu.vue'; import TabContextMenu from './TabContextMenu.vue';
import { useTabStore } from '@/stores/tabStore'; import { useTabStore } from '@/stores/tabStore';
import { useDocumentStore } from '@/stores/documentStore';
import { useEditorStore } from '@/stores/editorStore';
import { useEditorStateStore } from '@/stores/editorStateStore';
const tabStore = useTabStore(); const tabStore = useTabStore();
const documentStore = useDocumentStore();
const editorStore = useEditorStore();
const editorStateStore = useEditorStateStore();
// DOM 引用 // DOM 引用
const tabBarRef = ref<HTMLElement>(); const tabBarRef = ref<HTMLElement>();
@@ -50,8 +56,36 @@ const contextMenuTargetId = ref<number | null>(null);
// 标签页操作 // 标签页操作
const switchToTab = (documentId: number) => { const switchToTab = async (documentId: number) => {
tabStore.switchToTabAndDocument(documentId);
// 保存旧文档的光标位置
const oldDocId = documentStore.currentDocumentId;
if (oldDocId) {
const cursorPos = editorStore.getCurrentCursorPosition();
editorStateStore.saveCursorPosition(oldDocId, cursorPos);
}
// 如果旧文档有未保存修改,保存它
if (oldDocId && editorStore.hasUnsavedChanges(oldDocId)) {
try {
const content = editorStore.getCurrentContent();
await documentStore.saveDocument(oldDocId, content);
editorStore.syncAfterSave(oldDocId);
} catch (error) {
console.error('save document error:', error);
}
}
// 切换文档
await tabStore.switchToTabAndDocument(documentId);
// 切换到新编辑器
await editorStore.switchToEditor(documentId);
// 更新标签页
if (documentStore.currentDocument && tabStore.isTabsEnabled) {
tabStore.addOrActivateTab(documentStore.currentDocument);
}
}; };
const closeTab = (documentId: number) => { const closeTab = (documentId: number) => {
@@ -150,7 +184,7 @@ onUnmounted(() => {
}); });
// 监听当前活跃标签页的变化 // 监听当前活跃标签页的变化
watch(() => tabStore.currentDocumentId, () => { watch(() => documentStore.currentDocumentId, () => {
scrollToActiveTab(); scrollToActiveTab();
}); });

View File

@@ -0,0 +1,292 @@
<template>
<div
:class="['toast-item', `toast-${type}`]"
@mouseenter="pauseTimer"
@mouseleave="resumeTimer"
>
<!-- 图标 -->
<div class="toast-icon">
<svg v-if="type === 'success'" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10 0C4.48 0 0 4.48 0 10s4.48 10 10 10 10-4.48 10-10S15.52 0 10 0zm-2 15l-5-5 1.41-1.41L8 12.17l7.59-7.59L17 6l-9 9z" fill="currentColor"/>
</svg>
<svg v-else-if="type === 'error'" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10 0C4.48 0 0 4.48 0 10s4.48 10 10 10 10-4.48 10-10S15.52 0 10 0zm1 15H9v-2h2v2zm0-4H9V5h2v6z" fill="currentColor"/>
</svg>
<svg v-else-if="type === 'warning'" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M1 19h18L10 1 1 19zm10-3H9v-2h2v2zm0-4H9v-4h2v4z" fill="currentColor"/>
</svg>
<svg v-else-if="type === 'info'" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10 0C4.48 0 0 4.48 0 10s4.48 10 10 10 10-4.48 10-10S15.52 0 10 0zm1 15H9V9h2v6zm0-8H9V5h2v2z" fill="currentColor"/>
</svg>
</div>
<!-- 内容 -->
<div class="toast-content">
<div v-if="title" class="toast-title">{{ title }}</div>
<div class="toast-message">{{ message }}</div>
</div>
<!-- 关闭按钮 -->
<button
v-if="closable"
class="toast-close"
@click="close"
aria-label="Close"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M12 4L4 12M4 4L12 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import type { Toast } from './types';
const props = withDefaults(
defineProps<{
toast: Toast;
}>(),
{}
);
const emit = defineEmits<{
close: [id: string];
}>();
const timer = ref<number | null>(null);
const remainingTime = ref(props.toast.duration);
const pausedAt = ref<number | null>(null);
const { id, message, title, type, duration, closable } = props.toast;
const close = () => {
emit('close', id);
};
const startTimer = () => {
if (duration > 0) {
timer.value = window.setTimeout(() => {
close();
}, remainingTime.value);
}
};
const clearTimer = () => {
if (timer.value) {
clearTimeout(timer.value);
timer.value = null;
}
};
const pauseTimer = () => {
if (timer.value && duration > 0) {
clearTimer();
pausedAt.value = Date.now();
}
};
const resumeTimer = () => {
if (pausedAt.value && duration > 0) {
const elapsed = Date.now() - pausedAt.value;
remainingTime.value = Math.max(0, remainingTime.value - elapsed);
pausedAt.value = null;
startTimer();
}
};
onMounted(() => {
startTimer();
});
onUnmounted(() => {
clearTimer();
});
</script>
<style scoped lang="scss">
.toast-item {
display: flex;
align-items: flex-start;
gap: 12px;
min-width: 300px;
max-width: 420px;
padding: 16px 18px;
margin-bottom: 12px;
transform-origin: center center;
// 毛玻璃效果
// 亮色主题
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.5);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.08),
0 1px 3px rgba(0, 0, 0, 0.06),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
cursor: default;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
transform: translateY(-2px);
box-shadow:
0 6px 16px rgba(0, 0, 0, 0.1),
0 2px 6px rgba(0, 0, 0, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
}
// 深色主题适配 - 使用应用的 data-theme 属性
:root[data-theme="dark"] .toast-item,
:root[data-theme="auto"] .toast-item {
background: rgba(45, 45, 45, 0.9);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.3),
0 1px 3px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
&:hover {
box-shadow:
0 6px 16px rgba(0, 0, 0, 0.4),
0 2px 6px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
}
// 跟随系统主题时的浅色偏好
@media (prefers-color-scheme: light) {
:root[data-theme="auto"] .toast-item {
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(255, 255, 255, 0.5);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.08),
0 1px 3px rgba(0, 0, 0, 0.06),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
&:hover {
box-shadow:
0 6px 16px rgba(0, 0, 0, 0.1),
0 2px 6px rgba(0, 0, 0, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
}
}
.toast-icon {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
margin-top: 2px;
svg {
width: 20px;
height: 20px;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
}
}
// 有标题时,图标与标题对齐(不需要 margin-top
.toast-item:has(.toast-title) .toast-icon {
margin-top: 0;
}
.toast-success .toast-icon {
color: #16a34a;
}
.toast-error .toast-icon {
color: #dc2626;
}
.toast-warning .toast-icon {
color: #f59e0b;
}
.toast-info .toast-icon {
color: #3b82f6;
}
.toast-content {
flex: 1;
min-width: 0;
}
.toast-title {
font-size: 13px;
font-weight: 600;
color: var(--settings-text);
margin-bottom: 4px;
line-height: 1.4;
}
.toast-message {
font-size: 12px;
color: var(--settings-text-secondary);
line-height: 1.5;
word-wrap: break-word;
}
.toast-close {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
margin: 0;
margin-top: 0px;
background: rgba(0, 0, 0, 0.05);
border: none;
border-radius: 6px;
color: var(--settings-text-secondary);
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
svg {
width: 16px;
height: 16px;
}
&:hover {
background: rgba(0, 0, 0, 0.1);
color: var(--settings-text);
transform: rotate(90deg);
}
&:active {
transform: rotate(90deg) scale(0.9);
}
}
:root[data-theme="dark"] .toast-close,
:root[data-theme="auto"] .toast-close {
background: rgba(255, 255, 255, 0.08);
&:hover {
background: rgba(255, 255, 255, 0.15);
}
}
@media (prefers-color-scheme: light) {
:root[data-theme="auto"] .toast-close {
background: rgba(0, 0, 0, 0.05);
&:hover {
background: rgba(0, 0, 0, 0.1);
}
}
}
</style>

View File

@@ -0,0 +1,168 @@
<template>
<Teleport to="body">
<TransitionGroup
v-for="position in positions"
:key="position"
:class="['toast-container', `toast-container-${position}`]"
name="toast-list"
tag="div"
>
<ToastItem
v-for="toast in getToastsByPosition(position)"
:key="toast.id"
:toast="toast"
@close="removeToast"
/>
</TransitionGroup>
</Teleport>
</template>
<script setup lang="ts">
import ToastItem from './Toast.vue';
import { useToastStore } from './toastStore';
import type { ToastPosition } from './types';
const toastStore = useToastStore();
const positions: ToastPosition[] = [
'top-left',
'top-center',
'top-right',
'bottom-left',
'bottom-center',
'bottom-right',
];
const getToastsByPosition = (position: ToastPosition) => {
return toastStore.toasts.filter(toast => toast.position === position);
};
const removeToast = (id: string) => {
toastStore.remove(id);
};
</script>
<style scoped lang="scss">
.toast-container {
position: fixed;
z-index: 9999;
pointer-events: none;
display: flex;
flex-direction: column;
> * {
pointer-events: auto;
}
}
// 顶部位置 - 增加间距避免覆盖标题栏
.toast-container-top-left {
top: 35px;
left: 20px;
align-items: flex-start;
}
.toast-container-top-center {
top: 35px;
left: 50%;
transform: translateX(-50%);
align-items: center;
}
.toast-container-top-right {
top: 35px;
right: 20px;
align-items: flex-end;
}
// 底部位置
.toast-container-bottom-left {
bottom: 20px;
left: 20px;
align-items: flex-start;
flex-direction: column-reverse;
}
.toast-container-bottom-center {
bottom: 20px;
left: 50%;
transform: translateX(-50%);
align-items: center;
flex-direction: column-reverse;
}
.toast-container-bottom-right {
bottom: 20px;
right: 20px;
align-items: flex-end;
flex-direction: column-reverse;
}
// TransitionGroup 列表动画 - 从哪来回哪去,收缩滑出
.toast-list-move {
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.toast-list-enter-active {
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.toast-list-leave-active {
transition: all 0.3s cubic-bezier(0.6, 0, 0.8, 0.4);
position: absolute !important;
}
// 右侧位置:从右滑入,收缩向右滑出
.toast-container-top-right,
.toast-container-bottom-right {
.toast-list-enter-from {
opacity: 0;
transform: translateX(100%) scale(0.8);
}
.toast-list-leave-to {
opacity: 0;
transform: translateX(100%) scale(0.8);
}
}
// 左侧位置:从左滑入,收缩向左滑出
.toast-container-top-left,
.toast-container-bottom-left {
.toast-list-enter-from {
opacity: 0;
transform: translateX(-100%) scale(0.8);
}
.toast-list-leave-to {
opacity: 0;
transform: translateX(-100%) scale(0.8);
}
}
// 居中位置:从上/下滑入,收缩向上/下滑出
.toast-container-top-center {
.toast-list-enter-from {
opacity: 0;
transform: translateY(-100%) scale(0.8);
}
.toast-list-leave-to {
opacity: 0;
transform: translateY(-100%) scale(0.8);
}
}
.toast-container-bottom-center {
.toast-list-enter-from {
opacity: 0;
transform: translateY(100%) scale(0.8);
}
.toast-list-leave-to {
opacity: 0;
transform: translateY(100%) scale(0.8);
}
}
</style>

View File

@@ -0,0 +1,80 @@
import { useToastStore } from './toastStore';
import type { ToastOptions } from './types';
class ToastService {
private getStore() {
return useToastStore();
}
/**
* 显示一个通知
*/
show(options: ToastOptions): string {
return this.getStore().add(options);
}
/**
* 显示成功通知
*/
success(message: string, title?: string, options?: Partial<ToastOptions>): string {
return this.show({
message,
title,
type: 'success',
...options,
});
}
/**
* 显示错误通知
*/
error(message: string, title?: string, options?: Partial<ToastOptions>): string {
return this.show({
message,
title,
type: 'error',
...options,
});
}
/**
* 显示警告通知
*/
warning(message: string, title?: string, options?: Partial<ToastOptions>): string {
return this.show({
message,
title,
type: 'warning',
...options,
});
}
/**
* 显示信息通知
*/
info(message: string, title?: string, options?: Partial<ToastOptions>): string {
return this.show({
message,
title,
type: 'info',
...options,
});
}
/**
* 关闭指定的通知
*/
close(id: string): void {
this.getStore().remove(id);
}
/**
* 清空所有通知
*/
clear(): void {
this.getStore().clear();
}
}
export const toast = new ToastService();
export default toast;

View File

@@ -0,0 +1,55 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import type { Toast, ToastOptions } from './types';
export const useToastStore = defineStore('toast', () => {
const toasts = ref<Toast[]>([]);
let idCounter = 0;
/**
* 添加一个 Toast
*/
const add = (options: ToastOptions): string => {
const id = `toast-${Date.now()}-${idCounter++}`;
const toast: Toast = {
id,
message: options.message,
type: options.type || 'info',
title: options.title,
duration: options.duration ?? 4000,
position: options.position || 'top-right',
closable: options.closable ?? true,
createdAt: Date.now(),
};
toasts.value.push(toast);
return id;
};
/**
* 移除指定 Toast
*/
const remove = (id: string) => {
const index = toasts.value.findIndex(t => t.id === id);
if (index > -1) {
toasts.value.splice(index, 1);
}
};
/**
* 清空所有 Toast
*/
const clear = () => {
toasts.value = [];
};
return {
toasts,
add,
remove,
clear,
};
});

View File

@@ -0,0 +1,52 @@
/**
* Toast 通知类型定义
*/
export type ToastType = 'success' | 'error' | 'warning' | 'info';
export type ToastPosition =
| 'top-right'
| 'top-left'
| 'bottom-right'
| 'bottom-left'
| 'top-center'
| 'bottom-center';
export interface ToastOptions {
/**
* Toast 消息内容
*/
message: string;
/**
* Toast 类型
*/
type?: ToastType;
/**
* 标题(可选)
*/
title?: string;
/**
* 持续时间毫秒0 表示不自动关闭
*/
duration?: number;
/**
* 显示位置
*/
position?: ToastPosition;
/**
* 是否可关闭
*/
closable?: boolean;
}
export interface Toast extends Required<Omit<ToastOptions, 'title'>> {
id: string;
title?: string;
createdAt: number;
}

View File

@@ -51,13 +51,13 @@ let editorScope: ReturnType<typeof effectScope> | null = null;
// 更新当前块语言信息 // 更新当前块语言信息
const updateCurrentBlockLanguage = () => { const updateCurrentBlockLanguage = () => {
if (!editorStore.editorView) { if (!editorStore.currentEditor) {
currentBlockLanguage.value = { name: 'text', auto: false }; currentBlockLanguage.value = { name: 'text', auto: false };
return; return;
} }
try { try {
const state = editorStore.editorView.state; const state = editorStore.currentEditor.state;
const activeBlock = getActiveNoteBlock(state as any); const activeBlock = getActiveNoteBlock(state as any);
if (activeBlock) { if (activeBlock) {
const newLanguage = { const newLanguage = {
@@ -128,7 +128,7 @@ const setupEventListeners = (view: any) => {
// 监听编辑器状态变化 // 监听编辑器状态变化
watch( watch(
() => editorStore.editorView, () => editorStore.currentEditor,
(newView) => { (newView) => {
if (newView) { if (newView) {
setupEventListeners(newView); setupEventListeners(newView);
@@ -175,13 +175,13 @@ const closeLanguageMenu = () => {
// 选择语言 - 优化性能 // 选择语言 - 优化性能
const selectLanguage = (languageId: SupportedLanguage) => { const selectLanguage = (languageId: SupportedLanguage) => {
if (!editorStore.editorView) { if (!editorStore.currentEditor) {
closeLanguageMenu(); closeLanguageMenu();
return; return;
} }
try { try {
const view = editorStore.editorView; const view = editorStore.currentEditor;
const state = view.state; const state = view.state;
const dispatch = view.dispatch; const dispatch = view.dispatch;
@@ -294,9 +294,11 @@ const scrollToCurrentLanguage = () => {
<span class="arrow" :class="{ 'open': showLanguageMenu }"></span> <span class="arrow" :class="{ 'open': showLanguageMenu }"></span>
</button> </button>
<div class="language-menu" v-if="showLanguageMenu"> <!-- 菜单 -->
<!-- 搜索框 --> <Transition name="slide-up">
<div class="search-container"> <div class="language-menu" v-if="showLanguageMenu">
<!-- 搜索框 -->
<div class="search-container">
<input <input
ref="searchInputRef" ref="searchInputRef"
v-model="searchQuery" v-model="searchQuery"
@@ -330,11 +332,23 @@ const scrollToCurrentLanguage = () => {
{{ t('toolbar.noLanguageFound') }} {{ t('toolbar.noLanguageFound') }}
</div> </div>
</div> </div>
</div> </div>
</Transition>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.slide-up-enter-active,
.slide-up-leave-active {
transition: opacity 0.15s ease, transform 0.15s ease;
}
.slide-up-enter-from,
.slide-up-leave-to {
opacity: 0;
transform: translateY(8px);
}
.block-language-selector { .block-language-selector {
position: relative; position: relative;
@@ -386,15 +400,17 @@ const scrollToCurrentLanguage = () => {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 3px; border-radius: 3px;
margin-bottom: 4px; margin-bottom: 4px;
width: 220px; width: 280px;
max-height: 280px; max-height: 400px;
z-index: 1000; z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
overflow: hidden; overflow: hidden;
display: flex;
flex-direction: column;
.search-container { .search-container {
position: relative; position: relative;
padding: 8px; padding: 10px;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
.search-input { .search-input {
@@ -403,11 +419,11 @@ const scrollToCurrentLanguage = () => {
background-color: var(--bg-primary); background-color: var(--bg-primary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 2px; border-radius: 2px;
padding: 5px 8px 5px 26px; padding: 6px 10px 6px 30px;
font-size: 11px; font-size: 12px;
color: var(--text-primary); color: var(--text-primary);
outline: none; outline: none;
line-height: 1.2; line-height: 1.4;
&:focus { &:focus {
border-color: var(--text-muted); border-color: var(--text-muted);
@@ -420,7 +436,7 @@ const scrollToCurrentLanguage = () => {
.search-icon { .search-icon {
position: absolute; position: absolute;
left: 14px; left: 16px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
color: var(--text-muted); color: var(--text-muted);
@@ -429,20 +445,21 @@ const scrollToCurrentLanguage = () => {
} }
.language-list { .language-list {
max-height: 200px; max-height: 320px;
overflow-y: auto; overflow-y: auto;
flex: 1;
.language-option { .language-option {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 6px 8px; padding: 8px 10px;
cursor: pointer; cursor: pointer;
font-size: 11px; font-size: 12px;
border-bottom: 1px solid var(--border-color);
&:hover { &:hover {
background-color: var(--border-color); background-color: var(--bg-hover);
opacity: 0.8;
} }
&.active { &.active {
@@ -460,17 +477,17 @@ const scrollToCurrentLanguage = () => {
} }
.language-alias { .language-alias {
font-size: 10px; font-size: 11px;
color: var(--text-muted); color: var(--text-muted);
opacity: 0.6; opacity: 0.6;
} }
} }
.no-results { .no-results {
padding: 12px 8px; padding: 14px 10px;
text-align: center; text-align: center;
color: var(--text-muted); color: var(--text-muted);
font-size: 11px; font-size: 12px;
} }
} }
} }
@@ -478,7 +495,7 @@ const scrollToCurrentLanguage = () => {
/* 自定义滚动条 */ /* 自定义滚动条 */
.language-list::-webkit-scrollbar { .language-list::-webkit-scrollbar {
width: 4px; width: 6px;
} }
.language-list::-webkit-scrollbar-track { .language-list::-webkit-scrollbar-track {
@@ -487,7 +504,7 @@ const scrollToCurrentLanguage = () => {
.language-list::-webkit-scrollbar-thumb { .language-list::-webkit-scrollbar-thumb {
background-color: var(--border-color); background-color: var(--border-color);
border-radius: 2px; border-radius: 3px;
&:hover { &:hover {
background-color: var(--text-muted); background-color: var(--text-muted);

View File

@@ -2,12 +2,15 @@
import {computed, nextTick, reactive, ref, watch} from 'vue'; import {computed, nextTick, reactive, ref, watch} from 'vue';
import {useDocumentStore} from '@/stores/documentStore'; import {useDocumentStore} from '@/stores/documentStore';
import {useTabStore} from '@/stores/tabStore'; import {useTabStore} from '@/stores/tabStore';
import {useEditorStore} from '@/stores/editorStore';
import {useEditorStateStore} from '@/stores/editorStateStore';
import {useWindowStore} from '@/stores/windowStore'; import {useWindowStore} from '@/stores/windowStore';
import {useI18n} from 'vue-i18n'; import {useI18n} from 'vue-i18n';
import {useConfirm} from '@/composables'; import {useConfirm} from '@/composables';
import {validateDocumentTitle} from '@/common/utils/validation'; import {validateDocumentTitle} from '@/common/utils/validation';
import {formatDateTime, truncateString} from '@/common/utils/formatter'; import {formatDateTime, truncateString} from '@/common/utils/formatter';
import type {Document} from '@/../bindings/voidraft/internal/models/ent/models'; import {Document} from '@/../bindings/voidraft/internal/models/ent/models';
import toast from '@/components/toast';
// 类型定义 // 类型定义
interface DocumentItem extends Document { interface DocumentItem extends Document {
@@ -16,6 +19,8 @@ interface DocumentItem extends Document {
const documentStore = useDocumentStore(); const documentStore = useDocumentStore();
const tabStore = useTabStore(); const tabStore = useTabStore();
const editorStore = useEditorStore();
const editorStateStore = useEditorStateStore();
const windowStore = useWindowStore(); const windowStore = useWindowStore();
const {t} = useI18n(); const {t} = useI18n();
@@ -27,6 +32,7 @@ const editInputRef = ref<HTMLInputElement>();
const state = reactive({ const state = reactive({
isLoaded: false, isLoaded: false,
searchQuery: '', searchQuery: '',
documentList: [] as Document[], // 缓存文档列表
editing: { editing: {
id: null as number | null, id: null as number | null,
title: '' title: ''
@@ -44,7 +50,7 @@ const currentDocName = computed(() => {
}); });
const filteredItems = computed<DocumentItem[]>(() => { const filteredItems = computed<DocumentItem[]>(() => {
const docs = documentStore.documentList; const docs = state.documentList;
const query = state.searchQuery.trim(); const query = state.searchQuery.trim();
if (!query) return docs; if (!query) return docs;
@@ -67,7 +73,7 @@ const filteredItems = computed<DocumentItem[]>(() => {
// 核心操作 // 核心操作
const openMenu = async () => { const openMenu = async () => {
await documentStore.getDocumentMetaList(); state.documentList = await documentStore.getDocumentList();
documentStore.openDocumentSelector(); documentStore.openDocumentSelector();
state.isLoaded = true; state.isLoaded = true;
await nextTick(); await nextTick();
@@ -88,10 +94,10 @@ const closeMenu = () => {
resetDeleteConfirm(); resetDeleteConfirm();
}; };
const selectDoc = async (doc: Document) => { const selectDoc = async (doc: DocumentItem) => {
if (doc.id === undefined) return; if (doc.id === undefined) return;
// 如果选择的就是当前文档直接关闭菜单 // 如果选择的就是当前文档,直接关闭菜单
if (documentStore.currentDocument?.id === doc.id) { if (documentStore.currentDocument?.id === doc.id) {
closeMenu(); closeMenu();
return; return;
@@ -99,17 +105,40 @@ const selectDoc = async (doc: Document) => {
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id); const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
if (hasOpen) { if (hasOpen) {
documentStore.setError(doc.id, t('toolbar.alreadyOpenInNewWindow')); toast.warning(t('toolbar.alreadyOpenInNewWindow'));
return; return;
} }
const success = await documentStore.openDocument(doc.id);
if (success) { // 保存旧文档的光标位置
if (tabStore.isTabsEnabled) { const oldDocId = documentStore.currentDocumentId;
tabStore.addOrActivateTab(doc); if (oldDocId) {
} const cursorPos = editorStore.getCurrentCursorPosition();
closeMenu(); editorStateStore.saveCursorPosition(oldDocId, cursorPos);
} }
// 如果旧文档有未保存修改,保存它
if (oldDocId && editorStore.hasUnsavedChanges(oldDocId)) {
const content = editorStore.getCurrentContent();
await documentStore.saveDocument(oldDocId, content);
editorStore.syncAfterSave(oldDocId);
}
// 打开新文档
const success = await documentStore.openDocument(doc.id);
if (!success) return;
// 切换到新编辑器
await editorStore.switchToEditor(doc.id);
// 更新标签页
if (documentStore.currentDocument && tabStore.isTabsEnabled) {
tabStore.addOrActivateTab(documentStore.currentDocument);
}
closeMenu();
}; };
const createDoc = async (title: string) => { const createDoc = async (title: string) => {
@@ -119,7 +148,9 @@ const createDoc = async (title: string) => {
try { try {
const newDoc = await documentStore.createNewDocument(trimmedTitle); const newDoc = await documentStore.createNewDocument(trimmedTitle);
if (newDoc) await selectDoc(newDoc); if (newDoc && newDoc.id) {
await selectDoc(newDoc);
}
} catch (error) { } catch (error) {
console.error('Failed to create document:', error); console.error('Failed to create document:', error);
} }
@@ -142,7 +173,7 @@ const handleSearchEnter = () => {
}; };
// 编辑操作 // 编辑操作
const renameDoc = (doc: Document, event: Event) => { const renameDoc = (doc: DocumentItem, event: Event) => {
event.stopPropagation(); event.stopPropagation();
state.editing.id = doc.id ?? null; state.editing.id = doc.id ?? null;
state.editing.title = doc.title || ''; state.editing.title = doc.title || '';
@@ -165,8 +196,8 @@ const saveEdit = async () => {
if (error) return; if (error) return;
try { try {
await documentStore.updateDocumentMetadata(state.editing.id, trimmedTitle); await documentStore.updateDocumentTitle(state.editing.id, trimmedTitle);
await documentStore.getDocumentMetaList(); state.documentList = await documentStore.getDocumentList();
// 如果tabs功能开启且该文档有标签页更新标签页标题 // 如果tabs功能开启且该文档有标签页更新标签页标题
if (tabStore.isTabsEnabled && tabStore.hasTab(state.editing.id)) { if (tabStore.isTabsEnabled && tabStore.hasTab(state.editing.id)) {
@@ -186,17 +217,21 @@ const cancelEdit = () => {
}; };
// 其他操作 // 其他操作
const openInNewWindow = async (doc: Document, event: Event) => { const openInNewWindow = async (doc: DocumentItem, event: Event) => {
event.stopPropagation(); event.stopPropagation();
if (doc.id === undefined) return; if (doc.id === undefined) return;
try { try {
// 在打开新窗口前,如果启用了标签且该文档有标签,先关闭标签
if (tabStore.isTabsEnabled && tabStore.hasTab(doc.id)) {
await tabStore.closeTab(doc.id);
}
await documentStore.openDocumentInNewWindow(doc.id); await documentStore.openDocumentInNewWindow(doc.id);
} catch (error) { } catch (error) {
console.error('Failed to open document in new window:', error); console.error('Failed to open document in new window:', error);
} }
}; };
const handleDelete = async (doc: Document, event: Event) => { const handleDelete = async (doc: DocumentItem, event: Event) => {
event.stopPropagation(); event.stopPropagation();
if (doc.id === undefined) return; if (doc.id === undefined) return;
@@ -204,17 +239,17 @@ const handleDelete = async (doc: Document, event: Event) => {
// 确认删除前检查文档是否在其他窗口打开 // 确认删除前检查文档是否在其他窗口打开
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id); const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
if (hasOpen) { if (hasOpen) {
documentStore.setError(doc.id, t('toolbar.alreadyOpenInNewWindow')); toast.warning(t('toolbar.alreadyOpenInNewWindow'));
resetDeleteConfirm(); resetDeleteConfirm();
return; return;
} }
const deleteSuccess = await documentStore.deleteDocument(doc.id); const deleteSuccess = await documentStore.deleteDocument(doc.id);
if (deleteSuccess) { if (deleteSuccess) {
await documentStore.getDocumentMetaList(); state.documentList = await documentStore.getDocumentList();
// 如果删除的是当前文档切换到第一个文档 // 如果删除的是当前文档,切换到第一个文档
if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) { if (documentStore.currentDocument?.id === doc.id && state.documentList.length > 0) {
const firstDoc = documentStore.documentList[0]; const firstDoc = state.documentList[0];
if (firstDoc) await selectDoc(firstDoc); if (firstDoc) await selectDoc(firstDoc);
} }
} }
@@ -307,11 +342,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
<!-- 普通显示 --> <!-- 普通显示 -->
<div v-if="state.editing.id !== item.id" class="doc-info"> <div v-if="state.editing.id !== item.id" class="doc-info">
<div class="doc-title">{{ item.title }}</div> <div class="doc-title">{{ item.title }}</div>
<!-- 根据状态显示错误信息或时间 --> <div class="doc-date">{{ formatDateTime(item.updated_at) }}</div>
<div v-if="documentStore.selectorError?.docId === item.id" class="doc-error">
{{ documentStore.selectorError?.message }}
</div>
<div v-else class="doc-date">{{ formatDateTime(item.updated_at) }}</div>
</div> </div>
<!-- 编辑状态 --> <!-- 编辑状态 -->
@@ -353,7 +384,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
</svg> </svg>
</button> </button>
<button <button
v-if="documentStore.documentList.length > 1 && item.id !== 1" v-if="state.documentList.length > 1"
class="action-btn delete-btn" class="action-btn delete-btn"
:class="{ 'delete-confirm': isDeleting(item.id!) }" :class="{ 'delete-confirm': isDeleting(item.id!) }"
@click="handleDelete(item, $event)" @click="handleDelete(item, $event)"
@@ -444,7 +475,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 3px; border-radius: 3px;
margin-bottom: 4px; margin-bottom: 4px;
width: 300px; width: 340px;
max-height: calc(100vh - 40px); max-height: calc(100vh - 40px);
z-index: 1000; z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
@@ -454,7 +485,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
.input-box { .input-box {
position: relative; position: relative;
padding: 8px; padding: 10px;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
.main-input { .main-input {
@@ -463,8 +494,8 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
background-color: var(--bg-primary); background-color: var(--bg-primary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 2px; border-radius: 2px;
padding: 5px 8px 5px 26px; padding: 6px 10px 6px 30px;
font-size: 11px; font-size: 12px;
color: var(--text-primary); color: var(--text-primary);
outline: none; outline: none;
@@ -479,7 +510,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
.input-icon { .input-icon {
position: absolute; position: absolute;
left: 14px; left: 16px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
color: var(--text-muted); color: var(--text-muted);
@@ -500,7 +531,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
background-color: var(--bg-hover); background-color: var(--bg-hover);
} }
&.active { &.active {
background-color: var(--selection-bg); background-color: var(--selection-bg);
.doc-item-content .doc-info { .doc-item-content .doc-info {
@@ -508,7 +539,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
color: var(--selection-text); color: var(--selection-text);
} }
.doc-date, .doc-error { .doc-date {
color: var(--selection-text); color: var(--selection-text);
opacity: 0.7; opacity: 0.7;
} }
@@ -520,8 +551,8 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 8px 8px; padding: 10px 10px;
font-size: 11px; font-size: 12px;
font-weight: normal; font-weight: normal;
svg { svg {
@@ -535,15 +566,15 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 8px 8px; padding: 10px 10px;
.doc-info { .doc-info {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
.doc-title { .doc-title {
font-size: 12px; font-size: 13px;
margin-bottom: 2px; margin-bottom: 3px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@@ -551,17 +582,10 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
} }
.doc-date { .doc-date {
font-size: 10px; font-size: 11px;
color: var(--text-muted); color: var(--text-muted);
opacity: 0.6; opacity: 0.6;
} }
.doc-error {
font-size: 10px;
color: var(--text-danger);
font-weight: 500;
animation: fadeInOut 3s forwards;
}
} }
.doc-edit { .doc-edit {
@@ -573,8 +597,8 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
background-color: var(--bg-primary); background-color: var(--bg-primary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 2px; border-radius: 2px;
padding: 4px 6px; padding: 5px 8px;
font-size: 11px; font-size: 12px;
color: var(--text-primary); color: var(--text-primary);
outline: none; outline: none;
@@ -586,7 +610,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
.doc-actions { .doc-actions {
display: flex; display: flex;
gap: 6px; gap: 8px;
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
@@ -595,7 +619,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
border: none; border: none;
color: var(--text-muted); color: var(--text-muted);
cursor: pointer; cursor: pointer;
padding: 4px; padding: 5px;
border-radius: 2px; border-radius: 2px;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -616,7 +640,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
color: white; color: white;
.confirm-text { .confirm-text {
font-size: 9px; font-size: 10px;
font-weight: 500; font-weight: 500;
} }
} }
@@ -631,27 +655,12 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
} }
.empty { .empty {
padding: 16px 8px; padding: 18px 10px;
text-align: center; text-align: center;
font-size: 11px; font-size: 12px;
color: var(--text-muted); color: var(--text-muted);
} }
} }
} }
} }
@keyframes fadeInOut {
0% {
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
opacity: 0;
}
}
</style> </style>

View File

@@ -3,6 +3,7 @@ import {useI18n} from 'vue-i18n';
import {computed, onMounted, onUnmounted, ref, watch, shallowRef, readonly, toRefs, effectScope, onScopeDispose} from 'vue'; import {computed, onMounted, onUnmounted, ref, watch, shallowRef, readonly, toRefs, effectScope, onScopeDispose} from 'vue';
import {useConfigStore} from '@/stores/configStore'; import {useConfigStore} from '@/stores/configStore';
import {useEditorStore} from '@/stores/editorStore'; import {useEditorStore} from '@/stores/editorStore';
import {useEditorStateStore} from '@/stores/editorStateStore';
import {useUpdateStore} from '@/stores/updateStore'; import {useUpdateStore} from '@/stores/updateStore';
import {useWindowStore} from '@/stores/windowStore'; import {useWindowStore} from '@/stores/windowStore';
import {useSystemStore} from '@/stores/systemStore'; import {useSystemStore} from '@/stores/systemStore';
@@ -15,6 +16,7 @@ import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode
import {createDebounce} from '@/common/utils/debounce'; import {createDebounce} from '@/common/utils/debounce';
const editorStore = useEditorStore(); const editorStore = useEditorStore();
const editorStateStore = useEditorStateStore();
const configStore = useConfigStore(); const configStore = useConfigStore();
const updateStore = useUpdateStore(); const updateStore = useUpdateStore();
const windowStore = useWindowStore(); const windowStore = useWindowStore();
@@ -25,7 +27,6 @@ const router = useRouter();
const canFormatCurrentBlock = ref(false); const canFormatCurrentBlock = ref(false);
const isLoaded = shallowRef(false); const isLoaded = shallowRef(false);
const { documentStats } = toRefs(editorStore);
const { config } = toRefs(configStore); const { config } = toRefs(configStore);
// 窗口置顶状态 // 窗口置顶状态
@@ -57,14 +58,14 @@ const goToSettings = () => {
// 执行格式化 // 执行格式化
const formatCurrentBlock = () => { const formatCurrentBlock = () => {
if (!canFormatCurrentBlock.value || !editorStore.editorView) return; if (!canFormatCurrentBlock.value || !editorStore.currentEditor) return;
formatBlockContent(editorStore.editorView); formatBlockContent(editorStore.currentEditor);
}; };
// 统一更新按钮状态 // 统一更新按钮状态
const updateButtonStates = () => { const updateButtonStates = () => {
const view: any = editorStore.editorView; const view: any = editorStore.currentEditor;
if (!view) { if (!view) {
canFormatCurrentBlock.value = false; canFormatCurrentBlock.value = false;
return; return;
@@ -125,7 +126,7 @@ const setupEditorListeners = (view: any) => {
// 监听编辑器视图变化 // 监听编辑器视图变化
watch( watch(
() => editorStore.editorView, () => editorStore.currentEditor,
(newView) => { (newView) => {
// 在 scope 中管理副作用 // 在 scope 中管理副作用
editorScope.run(() => { editorScope.run(() => {
@@ -191,11 +192,13 @@ const updateButtonTitle = computed(() => {
}); });
// 统计数据的计算属性 // 统计数据的计算属性
const statsData = computed(() => ({ const statsData = computed(() => {
lines: documentStats.value.lines, const docId = editorStore.currentEditorId;
characters: documentStats.value.characters, if (!docId) {
selectedCharacters: documentStats.value.selectedCharacters return { lines: 0, characters: 0, selectedCharacters: 0 };
})); }
return editorStateStore.getDocumentStats(docId);
});
</script> </script>
<template> <template>

View File

@@ -1,5 +1,14 @@
export default { export default {
locale: 'en-US', locale: 'en-US',
common: {
ok: 'OK',
cancel: 'Cancel',
edit: 'Edit',
delete: 'Delete',
confirm: 'Confirm',
save: 'Save',
reset: 'Reset'
},
titlebar: { titlebar: {
minimize: 'Minimize', minimize: 'Minimize',
maximize: 'Maximize', maximize: 'Maximize',
@@ -56,6 +65,19 @@ export default {
}, },
resetToDefault: 'Reset to Default', resetToDefault: 'Reset to Default',
confirmReset: 'Confirm Reset?', confirmReset: 'Confirm Reset?',
noKeybinding: 'Not Set',
waitingForKey: 'Waiting...',
clickToSet: 'Click to set keybinding',
editKeybinding: 'Edit keybinding',
config: {
enabled: 'Enabled',
preventDefault: 'Prevent Default',
keybinding: 'Keybinding'
},
keyPlaceholder: 'Enter key, press Enter to add',
invalidFormat: 'Invalid format',
conflict: 'Conflict: {command}',
maxKeysReached: 'Maximum 4 keys allowed',
commands: { commands: {
showSearch: 'Show search panel', showSearch: 'Show search panel',
hideSearch: 'Hide search panel', hideSearch: 'Hide search panel',
@@ -178,6 +200,7 @@ export default {
enableWindowSnap: 'Enable Window Snapping', enableWindowSnap: 'Enable Window Snapping',
enableLoadingAnimation: 'Enable Loading Animation', enableLoadingAnimation: 'Enable Loading Animation',
enableTabs: 'Enable Tabs', enableTabs: 'Enable Tabs',
enableMemoryMonitor: 'Enable Memory Monitor',
startup: 'Startup Settings', startup: 'Startup Settings',
startAtLogin: 'Start at Login', startAtLogin: 'Start at Login',
dataStorage: 'Data Storage', dataStorage: 'Data Storage',
@@ -223,6 +246,7 @@ export default {
categoryEditing: 'Editing Enhancement', categoryEditing: 'Editing Enhancement',
categoryUI: 'UI Enhancement', categoryUI: 'UI Enhancement',
categoryTools: 'Tools', categoryTools: 'Tools',
enabled: 'Enabled',
configuration: 'Configuration', configuration: 'Configuration',
resetToDefault: 'Reset to Default Configuration', resetToDefault: 'Reset to Default Configuration',
}, },

View File

@@ -1,5 +1,14 @@
export default { export default {
locale: 'zh-CN', locale: 'zh-CN',
common: {
ok: '确定',
cancel: '取消',
edit: '编辑',
delete: '删除',
confirm: '确认',
save: '保存',
reset: '重置'
},
titlebar: { titlebar: {
minimize: '最小化', minimize: '最小化',
maximize: '最大化', maximize: '最大化',
@@ -56,6 +65,19 @@ export default {
}, },
resetToDefault: '重置为默认', resetToDefault: '重置为默认',
confirmReset: '确认重置?', confirmReset: '确认重置?',
noKeybinding: '未设置',
waitingForKey: '等待输入...',
clickToSet: '点击设置快捷键',
editKeybinding: '编辑快捷键',
config: {
enabled: '启用',
preventDefault: '阻止默认',
keybinding: '快捷键'
},
keyPlaceholder: '输入键名, 回车添加',
invalidFormat: '格式错误',
conflict: '冲突: {command}',
maxKeysReached: '最多只能添加4个键',
commands: { commands: {
showSearch: '显示搜索面板', showSearch: '显示搜索面板',
hideSearch: '隐藏搜索面板', hideSearch: '隐藏搜索面板',
@@ -179,6 +201,7 @@ export default {
enableWindowSnap: '启用窗口吸附', enableWindowSnap: '启用窗口吸附',
enableLoadingAnimation: '启用加载动画', enableLoadingAnimation: '启用加载动画',
enableTabs: '启用标签页', enableTabs: '启用标签页',
enableMemoryMonitor: '启用内存监视器',
startup: '启动设置', startup: '启动设置',
startAtLogin: '开机自启动', startAtLogin: '开机自启动',
dataStorage: '数据存储', dataStorage: '数据存储',
@@ -226,6 +249,7 @@ export default {
categoryEditing: '编辑增强', categoryEditing: '编辑增强',
categoryUI: '界面增强', categoryUI: '界面增强',
categoryTools: '工具扩展', categoryTools: '工具扩展',
enabled: '启用',
configuration: '配置', configuration: '配置',
resetToDefault: '重置为默认配置', resetToDefault: '重置为默认配置',
}, },

View File

@@ -4,7 +4,6 @@ import { BackupService } from '@/../bindings/voidraft/internal/services';
export const useBackupStore = defineStore('backup', () => { export const useBackupStore = defineStore('backup', () => {
const isSyncing = ref(false); const isSyncing = ref(false);
const error = ref<string | null>(null);
const sync = async (): Promise<void> => { const sync = async (): Promise<void> => {
if (isSyncing.value) { if (isSyncing.value) {
@@ -12,12 +11,11 @@ export const useBackupStore = defineStore('backup', () => {
} }
isSyncing.value = true; isSyncing.value = true;
error.value = null;
try { try {
await BackupService.Sync(); await BackupService.Sync();
} catch (e) { } catch (e) {
error.value = e instanceof Error ? e.message : String(e); throw e;
} finally { } finally {
isSyncing.value = false; isSyncing.value = false;
} }
@@ -25,7 +23,6 @@ export const useBackupStore = defineStore('backup', () => {
return { return {
isSyncing, isSyncing,
error,
sync sync
}; };
}); });

View File

@@ -1,18 +1,10 @@
import {defineStore} from 'pinia'; import {defineStore} from 'pinia';
import {computed, reactive} from 'vue'; import {computed, reactive} from 'vue';
import {ConfigService, StartupService} from '@/../bindings/voidraft/internal/services'; import {ConfigService, StartupService} from '@/../bindings/voidraft/internal/services';
import { import {AppConfig, AuthMethod, LanguageType, SystemThemeType, TabType} from '@/../bindings/voidraft/internal/models/models';
AppConfig,
AuthMethod,
EditingConfig,
LanguageType,
SystemThemeType,
TabType
} from '@/../bindings/voidraft/internal/models/models';
import {useI18n} from 'vue-i18n'; import {useI18n} from 'vue-i18n';
import {ConfigUtils} from '@/common/utils/configUtils'; import {ConfigUtils} from '@/common/utils/configUtils';
import {FONT_OPTIONS} from '@/common/constant/fonts'; import {FONT_OPTIONS} from '@/common/constant/fonts';
import {SUPPORTED_LOCALES} from '@/common/constant/locales';
import { import {
CONFIG_KEY_MAP, CONFIG_KEY_MAP,
CONFIG_LIMITS, CONFIG_LIMITS,
@@ -36,12 +28,6 @@ export const useConfigStore = defineStore('config', () => {
// Font options (no longer localized) // Font options (no longer localized)
const fontOptions = computed(() => FONT_OPTIONS); const fontOptions = computed(() => FONT_OPTIONS);
// 计算属性
const createLimitComputed = (key: NumberConfigKey) => computed(() => CONFIG_LIMITS[key]);
const limits = Object.fromEntries(
(['fontSize', 'tabSize', 'lineHeight'] as const).map(key => [key, createLimitComputed(key)])
) as Record<NumberConfigKey, ReturnType<typeof createLimitComputed>>;
// 统一配置更新方法 // 统一配置更新方法
const updateConfig = async <K extends ConfigKey>(key: K, value: any): Promise<void> => { const updateConfig = async <K extends ConfigKey>(key: K, value: any): Promise<void> => {
if (!state.configLoaded && !state.isLoading) { if (!state.configLoaded && !state.isLoading) {
@@ -99,39 +85,12 @@ export const useConfigStore = defineStore('config', () => {
} }
}; };
// 通用数值调整器工厂
const createAdjuster = <T extends NumberConfigKey>(key: T) => {
const limit = CONFIG_LIMITS[key];
const clamp = (value: number) => ConfigUtils.clamp(value, limit.min, limit.max);
return {
increase: async () => await updateConfig(key, clamp(state.config.editing[key] + 1)),
decrease: async () => await updateConfig(key, clamp(state.config.editing[key] - 1)),
set: async (value: number) => await updateConfig(key, clamp(value)),
reset: async () => await updateConfig(key, limit.default),
increaseLocal: () => updateConfigLocal(key, clamp(state.config.editing[key] + 1)),
decreaseLocal: () => updateConfigLocal(key, clamp(state.config.editing[key] - 1))
};
};
const createEditingToggler = <T extends keyof EditingConfig>(key: T) =>
async () => await updateConfig(key as ConfigKey, !state.config.editing[key] as EditingConfig[T]);
// 枚举值切换器
const createEnumToggler = <T extends TabType>(key: 'tabType', values: readonly T[]) =>
async () => {
const currentIndex = values.indexOf(state.config.editing[key] as T);
const nextIndex = (currentIndex + 1) % values.length;
return await updateConfig(key, values[nextIndex]);
};
// 重置配置 // 重置配置
const resetConfig = async (): Promise<void> => { const resetConfig = async (): Promise<void> => {
if (state.isLoading) return; if (state.isLoading) return;
state.isLoading = true; state.isLoading = true;
try { try {
await ConfigService.ResetConfig(); await ConfigService.ResetConfig();
const appConfig = await ConfigService.GetConfig(); const appConfig = await ConfigService.GetConfig();
if (appConfig) { if (appConfig) {
@@ -142,57 +101,25 @@ export const useConfigStore = defineStore('config', () => {
} }
}; };
// 语言设置方法 // 辅助函数:限制数值范围
const setLanguage = async (language: LanguageType): Promise<void> => { const clampValue = (value: number, key: NumberConfigKey): number => {
await updateConfig('language', language); const limit = CONFIG_LIMITS[key];
const frontendLocale = ConfigUtils.backendLanguageToFrontend(language); return ConfigUtils.clamp(value, limit.min, limit.max);
locale.value = frontendLocale as any;
}; };
// 系统主题设置方法 // 计算属性
const setSystemTheme = async (systemTheme: SystemThemeType): Promise<void> => { const fontConfig = computed(() => ({
await updateConfig('systemTheme', systemTheme); fontSize: state.config.editing.fontSize,
}; fontFamily: state.config.editing.fontFamily,
lineHeight: state.config.editing.lineHeight,
fontWeight: state.config.editing.fontWeight
}));
// 当前主题设置方法 const tabConfig = computed(() => ({
const setCurrentTheme = async (themeName: string): Promise<void> => { tabSize: state.config.editing.tabSize,
await updateConfig('currentTheme', themeName); enableTabIndent: state.config.editing.enableTabIndent,
}; tabType: state.config.editing.tabType
}));
// 初始化语言设置
const initLanguage = async (): Promise<void> => {
try {
// 如果配置未加载,先加载配置
if (!state.configLoaded) {
await initConfig();
}
// 同步前端语言设置
const frontendLocale = ConfigUtils.backendLanguageToFrontend(state.config.appearance.language);
locale.value = frontendLocale as any;
} catch (_error) {
const browserLang = SUPPORTED_LOCALES[0].code;
locale.value = browserLang as any;
}
};
// 创建数值调整器实例
const adjusters = {
fontSize: createAdjuster('fontSize'),
tabSize: createAdjuster('tabSize'),
lineHeight: createAdjuster('lineHeight')
};
// 创建切换器实例
const togglers = {
tabIndent: createEditingToggler('enableTabIndent'),
alwaysOnTop: async () => {
await updateConfig('alwaysOnTop', !state.config.general.alwaysOnTop);
await runtime.Window.SetAlwaysOnTop(state.config.general.alwaysOnTop);
},
tabType: createEnumToggler('tabType', CONFIG_LIMITS.tabType.values)
};
return { return {
// 状态 // 状态
@@ -200,53 +127,84 @@ export const useConfigStore = defineStore('config', () => {
configLoaded: computed(() => state.configLoaded), configLoaded: computed(() => state.configLoaded),
isLoading: computed(() => state.isLoading), isLoading: computed(() => state.isLoading),
fontOptions, fontOptions,
fontConfig,
// 限制常量 tabConfig,
...limits,
// 核心方法 // 核心方法
initConfig, initConfig,
resetConfig, resetConfig,
// 语言相关方法 // 语言相关方法
setLanguage, setLanguage: (value: LanguageType) => {
initLanguage, updateConfig('language', value);
locale.value = value as any;
},
// 主题相关方法 // 主题相关方法
setSystemTheme, setSystemTheme: (value: SystemThemeType) => updateConfig('systemTheme', value),
setCurrentTheme, setCurrentTheme: (value: string) => updateConfig('currentTheme', value),
// 字体大小操作 // 字体大小操作
...adjusters.fontSize, setFontSize: async (value: number) => {
increaseFontSize: adjusters.fontSize.increase, await updateConfig('fontSize', clampValue(value, 'fontSize'));
decreaseFontSize: adjusters.fontSize.decrease, },
resetFontSize: adjusters.fontSize.reset, increaseFontSize: async () => {
setFontSize: adjusters.fontSize.set, const newValue = state.config.editing.fontSize + 1;
// 字体大小操作 await updateConfig('fontSize', clampValue(newValue, 'fontSize'));
increaseFontSizeLocal: adjusters.fontSize.increaseLocal, },
decreaseFontSizeLocal: adjusters.fontSize.decreaseLocal, decreaseFontSize: async () => {
saveFontSize: () => saveConfig('fontSize'), const newValue = state.config.editing.fontSize - 1;
await updateConfig('fontSize', clampValue(newValue, 'fontSize'));
// Tab操作 },
toggleTabIndent: togglers.tabIndent, resetFontSize: async () => {
setEnableTabIndent: (value: boolean) => updateConfig('enableTabIndent', value), await updateConfig('fontSize', CONFIG_LIMITS.fontSize.default);
...adjusters.tabSize, },
increaseTabSize: adjusters.tabSize.increase, increaseFontSizeLocal: () => {
decreaseTabSize: adjusters.tabSize.decrease, updateConfigLocal('fontSize', clampValue(state.config.editing.fontSize + 1, 'fontSize'));
setTabSize: adjusters.tabSize.set, },
toggleTabType: togglers.tabType, decreaseFontSizeLocal: () => {
updateConfigLocal('fontSize', clampValue(state.config.editing.fontSize - 1, 'fontSize'));
// 行高操作 },
setLineHeight: adjusters.lineHeight.set, saveFontSize: async () => {
await saveConfig('fontSize');
// 窗口操作 },
toggleAlwaysOnTop: togglers.alwaysOnTop,
setAlwaysOnTop: (value: boolean) => updateConfig('alwaysOnTop', value),
// 字体操作 // 字体操作
setFontFamily: (value: string) => updateConfig('fontFamily', value), setFontFamily: (value: string) => updateConfig('fontFamily', value),
setFontWeight: (value: string) => updateConfig('fontWeight', value), setFontWeight: (value: string) => updateConfig('fontWeight', value),
// 行高操作
setLineHeight: async (value: number) => {
await updateConfig('lineHeight', clampValue(value, 'lineHeight'));
},
// Tab操作
setEnableTabIndent: (value: boolean) => updateConfig('enableTabIndent', value),
setTabSize: async (value: number) => {
await updateConfig('tabSize', clampValue(value, 'tabSize'));
},
increaseTabSize: async () => {
const newValue = state.config.editing.tabSize + 1;
await updateConfig('tabSize', clampValue(newValue, 'tabSize'));
},
decreaseTabSize: async () => {
const newValue = state.config.editing.tabSize - 1;
await updateConfig('tabSize', clampValue(newValue, 'tabSize'));
},
toggleTabType: async () => {
const values = CONFIG_LIMITS.tabType.values;
const currentIndex = values.indexOf(state.config.editing.tabType as typeof values[number]);
const nextIndex = (currentIndex + 1) % values.length;
await updateConfig('tabType', values[nextIndex]);
},
// 窗口操作
toggleAlwaysOnTop: async () => {
await updateConfig('alwaysOnTop', !state.config.general.alwaysOnTop);
await runtime.Window.SetAlwaysOnTop(state.config.general.alwaysOnTop);
},
setAlwaysOnTop: (value: boolean) => updateConfig('alwaysOnTop', value),
// 路径操作 // 路径操作
setDataPath: (value: string) => updateConfigLocal('dataPath', value), setDataPath: (value: string) => updateConfigLocal('dataPath', value),
@@ -275,6 +233,9 @@ export const useConfigStore = defineStore('config', () => {
// 标签页配置相关方法 // 标签页配置相关方法
setEnableTabs: (value: boolean) => updateConfig('enableTabs', value), setEnableTabs: (value: boolean) => updateConfig('enableTabs', value),
// 内存监视器配置相关方法
setEnableMemoryMonitor: (value: boolean) => updateConfig('enableMemoryMonitor', value),
// 快捷键模式配置相关方法 // 快捷键模式配置相关方法
setKeymapMode: (value: any) => updateConfig('keymapMode', value), setKeymapMode: (value: any) => updateConfig('keymapMode', value),

View File

@@ -1,79 +1,70 @@
import {defineStore} from 'pinia'; import {defineStore} from 'pinia';
import {computed, ref} from 'vue'; import {ref} from 'vue';
import {DocumentService} from '@/../bindings/voidraft/internal/services'; import {DocumentService} from '@/../bindings/voidraft/internal/services';
import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice'; import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice';
import {Document} from '@/../bindings/voidraft/internal/models/ent/models'; import {Document} from '@/../bindings/voidraft/internal/models/ent/models';
import {useTabStore} from "@/stores/tabStore"; import type {TimerManager} from '@/common/utils/timerUtils';
import type {EditorViewState} from '@/stores/editorStore'; import {createTimerManager} from '@/common/utils/timerUtils';
export const useDocumentStore = defineStore('document', () => { export const useDocumentStore = defineStore('document', () => {
// === 核心状态 ===
const documents = ref<Record<number, Document>>({});
const currentDocumentId = ref<number | null>(null); const currentDocumentId = ref<number | null>(null);
const currentDocument = ref<Document | null>(null); const currentDocument = ref<Document | null>(null);
// === 编辑器状态持久化 === // 自动保存定时器
const documentStates = ref<Record<number, EditorViewState>>({}); const autoSaveTimers = ref<Map<number, TimerManager>>(new Map());
// === UI状态 === // === UI状态 ===
const showDocumentSelector = ref(false); const showDocumentSelector = ref(false);
const selectorError = ref<{ docId: number; message: string } | null>(null);
const isLoading = ref(false); const isLoading = ref(false);
// === 计算属性 ===
const documentList = computed(() =>
Object.values(documents.value).sort((a, b) => {
const timeA = a.updated_at ? new Date(a.updated_at).getTime() : 0;
const timeB = b.updated_at ? new Date(b.updated_at).getTime() : 0;
return timeB - timeA;
})
);
const setDocuments = (docs: Document[]) => {
documents.value = {};
docs.forEach(doc => {
if (doc.id !== undefined) {
documents.value[doc.id] = doc;
}
});
};
// === 错误处理 ===
const setError = (docId: number, message: string) => {
selectorError.value = {docId, message};
// 3秒后自动清除错误状态
setTimeout(() => {
if (selectorError.value?.docId === docId) {
selectorError.value = null;
}
}, 3000);
};
const clearError = () => {
selectorError.value = null;
};
// === UI控制方法 === // === UI控制方法 ===
const openDocumentSelector = () => { const openDocumentSelector = () => {
showDocumentSelector.value = true; showDocumentSelector.value = true;
clearError();
}; };
const closeDocumentSelector = () => { const closeDocumentSelector = () => {
showDocumentSelector.value = false; showDocumentSelector.value = false;
clearError();
}; };
// 获取文档列表
const getDocumentList = async (): Promise<Document[]> => {
try {
isLoading.value = true;
const docs = await DocumentService.ListAllDocumentsMeta();
return docs?.filter((doc): doc is Document => doc !== null) || [];
} catch (_error) {
return [];
} finally {
isLoading.value = false;
}
};
// 获取单个文档
const getDocument = async (docId: number): Promise<Document | null> => {
try {
return await DocumentService.GetDocumentByID(docId);
} catch (error) {
console.error('Failed to get document:', error);
return null;
}
};
// 保存文档内容
const saveDocument = async (docId: number, content: string): Promise<Document | null> => {
try {
await DocumentService.UpdateDocumentContent(docId, content);
return await DocumentService.GetDocumentByID(docId);
} catch (error) {
console.error('Failed to save document:', error);
throw error;
}
};
// 在新窗口中打开文档 // 在新窗口中打开文档
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => { const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
try { try {
const tabStore = useTabStore();
if (tabStore.isTabsEnabled && tabStore.hasTab(docId)) {
tabStore.closeTab(docId);
}
await OpenDocumentWindow(docId); await OpenDocumentWindow(docId);
return true; return true;
} catch (error) { } catch (error) {
@@ -86,36 +77,16 @@ export const useDocumentStore = defineStore('document', () => {
const createNewDocument = async (title: string): Promise<Document | null> => { const createNewDocument = async (title: string): Promise<Document | null> => {
try { try {
const doc = await DocumentService.CreateDocument(title); const doc = await DocumentService.CreateDocument(title);
if (doc && doc.id !== undefined) { return doc || null;
documents.value[doc.id] = doc;
return doc;
}
return null;
} catch (error) { } catch (error) {
console.error('Failed to create document:', error); console.error('Failed to create document:', error);
return null; return null;
} }
}; };
// 获取文档列表
const getDocumentMetaList = async () => {
try {
isLoading.value = true;
const docs = await DocumentService.ListAllDocumentsMeta();
if (docs) {
setDocuments(docs.filter((doc): doc is Document => doc !== null));
}
} catch (error) {
console.error('Failed to update documents:', error);
} finally {
isLoading.value = false;
}
};
// 打开文档 // 打开文档
const openDocument = async (docId: number): Promise<boolean> => { const openDocument = async (docId: number): Promise<boolean> => {
try { try {
// 获取完整文档数据
const doc = await DocumentService.GetDocumentByID(docId); const doc = await DocumentService.GetDocumentByID(docId);
if (!doc) { if (!doc) {
throw new Error(`Document ${docId} not found`); throw new Error(`Document ${docId} not found`);
@@ -123,7 +94,6 @@ export const useDocumentStore = defineStore('document', () => {
currentDocumentId.value = docId; currentDocumentId.value = docId;
currentDocument.value = doc; currentDocument.value = doc;
return true; return true;
} catch (error) { } catch (error) {
console.error('Failed to open document:', error); console.error('Failed to open document:', error);
@@ -131,30 +101,20 @@ export const useDocumentStore = defineStore('document', () => {
} }
}; };
// 更新文档元数据 // 更新文档标题
const updateDocumentMetadata = async (docId: number, title: string): Promise<boolean> => { const updateDocumentTitle = async (docId: number, title: string): Promise<boolean> => {
try { try {
await DocumentService.UpdateDocumentTitle(docId, title); await DocumentService.UpdateDocumentTitle(docId, title);
// 更新本地状态 // 更新当前文档状态
const doc = documents.value[docId];
if (doc) {
doc.title = title;
doc.updated_at = new Date().toISOString();
}
if (currentDocument.value?.id === docId) { if (currentDocument.value?.id === docId) {
currentDocument.value.title = title; currentDocument.value.title = title;
currentDocument.value.updated_at = new Date().toISOString(); currentDocument.value.updated_at = new Date().toISOString();
} }
// 同步更新标签页标题
const tabStore = useTabStore();
tabStore.updateTabTitle(docId, title);
return true; return true;
} catch (error) { } catch (error) {
console.error('Failed to update document metadata:', error); console.error('Failed to update document title:', error);
return false; return false;
} }
}; };
@@ -164,20 +124,18 @@ export const useDocumentStore = defineStore('document', () => {
try { try {
await DocumentService.DeleteDocument(docId); await DocumentService.DeleteDocument(docId);
// 更新本地状态 // 清理定时器
delete documents.value[docId]; const timer = autoSaveTimers.value.get(docId);
if (timer) {
// 同步清理标签页 timer.clear();
const tabStore = useTabStore(); autoSaveTimers.value.delete(docId);
if (tabStore.hasTab(docId)) {
tabStore.closeTab(docId);
} }
// 如果删除的是当前文档,切换到第一个可用文档 // 如果删除的是当前文档,切换到第一个可用文档
if (currentDocumentId.value === docId) { if (currentDocumentId.value === docId) {
const availableDocs = Object.values(documents.value); const docs = await getDocumentList();
if (availableDocs.length > 0 && availableDocs[0].id !== undefined) { if (docs.length > 0 && docs[0].id !== undefined) {
await openDocument(availableDocs[0].id); await openDocument(docs[0].id);
} else { } else {
currentDocumentId.value = null; currentDocumentId.value = null;
currentDocument.value = null; currentDocument.value = null;
@@ -190,23 +148,46 @@ export const useDocumentStore = defineStore('document', () => {
return false; return false;
} }
}; };
// 调度自动保存
const scheduleAutoSave = (docId: number, saveCallback: () => Promise<void>, delay: number = 2000) => {
let timer = autoSaveTimers.value.get(docId);
if (!timer) {
timer = createTimerManager();
autoSaveTimers.value.set(docId, timer);
}
timer.set(async () => {
try {
await saveCallback();
} catch (error) {
console.error(`auto save for document ${docId} failed:`, error);
}
}, delay);
};
// 取消自动保存
const cancelAutoSave = (docId: number) => {
const timer = autoSaveTimers.value.get(docId);
if (timer) {
timer.clear();
}
};
// === 初始化 === // 初始化文档
const initialize = async (urlDocumentId?: number): Promise<void> => { const initDocument = async (urlDocumentId?: number): Promise<void> => {
try { try {
await getDocumentMetaList(); const docs = await getDocumentList();
// 优先使用URL参数中的文档ID // 优先使用URL参数中的文档ID
if (urlDocumentId && documents.value[urlDocumentId]) { if (urlDocumentId) {
await openDocument(urlDocumentId); await openDocument(urlDocumentId);
} else if (currentDocumentId.value && documents.value[currentDocumentId.value]) { } else if (currentDocumentId.value) {
// 如果URL中没有指定文档ID使用持久化的文档ID // 使用持久化的文档ID
await openDocument(currentDocumentId.value); await openDocument(currentDocumentId.value);
} else { } else if (docs.length > 0 && docs[0].id !== undefined) {
// 否则打开第一个文档 // 打开第一个文档
if (documentList.value[0].id) { await openDocument(docs[0].id);
await openDocument(documentList.value[0].id);
}
} }
} catch (error) { } catch (error) {
console.error('Failed to initialize document store:', error); console.error('Failed to initialize document store:', error);
@@ -215,32 +196,35 @@ export const useDocumentStore = defineStore('document', () => {
return { return {
// 状态 // 状态
documents,
documentList,
currentDocumentId, currentDocumentId,
currentDocument, currentDocument,
documentStates,
showDocumentSelector, showDocumentSelector,
selectorError,
isLoading, isLoading,
// 方法 getDocumentList,
getDocumentMetaList, getDocument,
saveDocument,
createNewDocument,
updateDocumentTitle,
deleteDocument,
openDocument, openDocument,
openDocumentInNewWindow, openDocumentInNewWindow,
createNewDocument,
updateDocumentMetadata, // 自动保存
deleteDocument, scheduleAutoSave,
cancelAutoSave,
// UI 控制
openDocumentSelector, openDocumentSelector,
closeDocumentSelector, closeDocumentSelector,
setError,
clearError, // 初始化
initialize, initDocument,
}; };
}, { }, {
persist: { persist: {
key: 'voidraft-document', key: 'voidraft-document',
storage: localStorage, storage: localStorage,
pick: ['currentDocumentId', 'documents', 'documentStates'] pick: ['currentDocumentId']
} }
}); });

View File

@@ -0,0 +1,98 @@
import {defineStore} from 'pinia';
import {ref} from 'vue';
export interface DocumentStats {
lines: number;
characters: number;
selectedCharacters: number;
}
export interface FoldRange {
// 字符偏移(备用)
from: number;
to: number;
// 行号
fromLine: number;
toLine: number;
}
export const useEditorStateStore = defineStore('editorState', () => {
// 光标位置存储 Record<docId, cursorPosition>
const cursorPositions = ref<Record<number, number>>({});
// 文档统计数据存储 Record<docId, DocumentStats>
const documentStats = ref<Record<number, DocumentStats>>({});
// 折叠状态存储 Record<docId, FoldRange[]>
const foldStates = ref<Record<number, FoldRange[]>>({});
// 保存光标位置
const saveCursorPosition = (docId: number, position: number) => {
cursorPositions.value[docId] = position;
};
// 获取光标位置
const getCursorPosition = (docId: number): number | undefined => {
return cursorPositions.value[docId];
};
// 保存文档统计数据
const saveDocumentStats = (docId: number, stats: DocumentStats) => {
documentStats.value[docId] = stats;
};
// 获取文档统计数据
const getDocumentStats = (docId: number): DocumentStats => {
return documentStats.value[docId] || {
lines: 0,
characters: 0,
selectedCharacters: 0
};
};
// 保存折叠状态
const saveFoldState = (docId: number, foldRanges: FoldRange[]) => {
foldStates.value[docId] = foldRanges;
};
// 获取折叠状态
const getFoldState = (docId: number): FoldRange[] => {
return foldStates.value[docId] || [];
};
// 清除文档状态
const clearDocumentState = (docId: number) => {
delete cursorPositions.value[docId];
delete documentStats.value[docId];
delete foldStates.value[docId];
};
// 清除所有状态
const clearAllStates = () => {
cursorPositions.value = {};
documentStats.value = {};
foldStates.value = {};
};
return {
cursorPositions,
documentStats,
foldStates,
saveCursorPosition,
getCursorPosition,
saveDocumentStats,
getDocumentStats,
saveFoldState,
getFoldState,
clearDocumentState,
clearAllStates
};
}, {
persist: {
key: 'voidraft-editor-state',
storage: localStorage,
pick: ['cursorPositions', 'foldStates']
}
});

View File

@@ -1,11 +1,10 @@
import {defineStore} from 'pinia'; import {defineStore} from 'pinia';
import {computed, nextTick, ref, watch} from 'vue'; import {computed, readonly, ref} from 'vue';
import {EditorView} from '@codemirror/view'; import {EditorView} from '@codemirror/view';
import {EditorState, Extension} from '@codemirror/state'; import {EditorState, Extension} from '@codemirror/state';
import {Document} from '@/../bindings/voidraft/internal/models/ent/models';
import {useConfigStore} from './configStore'; import {useConfigStore} from './configStore';
import {useDocumentStore} from './documentStore'; import {useDocumentStore} from './documentStore';
import {DocumentService, ExtensionService} from '@/../bindings/voidraft/internal/services';
import {ensureSyntaxTree} from "@codemirror/language";
import {createBasicSetup} from '@/views/editor/basic/basicSetup'; import {createBasicSetup} from '@/views/editor/basic/basicSetup';
import {createThemeExtension, updateEditorTheme} from '@/views/editor/basic/themeExtension'; import {createThemeExtension, updateEditorTheme} from '@/views/editor/basic/themeExtension';
import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtension'; import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtension';
@@ -14,141 +13,69 @@ import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension'; import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension'; import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension';
import {createCursorPositionExtension, scrollToCursor} from '@/views/editor/basic/cursorPositionExtension'; import {createCursorPositionExtension, scrollToCursor} from '@/views/editor/basic/cursorPositionExtension';
import {createFoldStateExtension, restoreFoldState} from '@/views/editor/basic/foldStateExtension';
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap'; import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
import { import {
createDynamicExtensions, createDynamicExtensions,
getExtensionManager,
removeExtensionManagerView, removeExtensionManagerView,
setExtensionManagerView setExtensionManagerView
} from '@/views/editor/manager'; } from '@/views/editor/manager';
import {useExtensionStore} from './extensionStore';
import createCodeBlockExtension from "@/views/editor/extensions/codeblock"; import createCodeBlockExtension from "@/views/editor/extensions/codeblock";
import {LruCache} from '@/common/utils/lruCache'; import {LruCache} from '@/common/utils/lruCache';
import {AsyncManager} from '@/common/utils/asyncManager';
import {generateContentHash} from "@/common/utils/hashUtils";
import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils';
import {EDITOR_CONFIG} from '@/common/constant/editor'; import {EDITOR_CONFIG} from '@/common/constant/editor';
import {createDebounce} from '@/common/utils/debounce'; import {useEditorStateStore, type DocumentStats} from './editorStateStore';
import {useKeybindingStore} from "@/stores/keybindingStore";
export interface DocumentStats {
lines: number;
characters: number;
selectedCharacters: number;
}
// 修复:只保存光标位置,恢复时自动滚动到光标处
export interface EditorViewState {
cursorPos: number;
}
// 编辑器实例
interface EditorInstance { interface EditorInstance {
view: EditorView; view: EditorView;
documentId: number; documentId: number;
content: string; contentTimestamp: string; // 文档时间戳
contentLength: number; // 内容长度
isDirty: boolean; isDirty: boolean;
lastModified: Date; lastModified: number;
autoSaveTimer: TimerManager;
syntaxTreeCache: {
lastDocLength: number;
lastContentHash: string;
lastParsed: Date;
} | null;
// 修复:使用统一的类型,可选但不是 undefined | {...}
editorState?: EditorViewState;
} }
export const useEditorStore = defineStore('editor', () => { export const useEditorStore = defineStore('editor', () => {
// === 依赖store ===
const configStore = useConfigStore(); const configStore = useConfigStore();
const documentStore = useDocumentStore(); const documentStore = useDocumentStore();
const extensionStore = useExtensionStore(); const editorStateStore = useEditorStateStore();
// === 核心状态 ===
const editorCache = new LruCache<number, EditorInstance>(EDITOR_CONFIG.MAX_INSTANCES); const editorCache = new LruCache<number, EditorInstance>(EDITOR_CONFIG.MAX_INSTANCES);
const containerElement = ref<HTMLElement | null>(null); const containerElement = ref<HTMLElement | null>(null);
const currentEditorId = ref<number | null>(null);
const currentEditor = ref<EditorView | null>(null);
const documentStats = ref<DocumentStats>({
lines: 0,
characters: 0,
selectedCharacters: 0
});
// 编辑器加载状态
const isLoading = ref(false); const isLoading = ref(false);
// 修复:使用操作计数器精确管理加载状态
const loadingOperations = ref(0);
// 异步操作管理器
const operationManager = new AsyncManager<number>();
// 自动保存设置 - 从配置动态获取
const getAutoSaveDelay = () => configStore.config.editing.autoSaveDelay;
// 创建防抖的语法树缓存清理函数
const debouncedClearSyntaxCache = createDebounce((instance) => {
if (instance) {
instance.syntaxTreeCache = null;
}
}, { delay: 500 }); // 500ms 内的多次输入只清理一次
// 缓存化的语法树确保方法 // 验证缓存是否有效
const ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => { const isCacheValid = (cached: EditorInstance, doc: Document): boolean => {
const instance = editorCache.get(documentId); return cached.contentTimestamp === doc.updated_at
if (!instance) return; && cached.contentLength === (doc.content || '').length;
};
const docLength = view.state.doc.length; // 检查内容是否真的不同
const content = view.state.doc.toString(); const hasContentChanged = (cached: EditorInstance, doc: Document): boolean => {
const contentHash = generateContentHash(content); const currentContent = cached.view.state.doc.toString();
const now = new Date(); return currentContent !== (doc.content || '');
// 检查是否需要重新构建语法树
const cache = instance.syntaxTreeCache;
const shouldRebuild = !cache ||
cache.lastDocLength !== docLength ||
cache.lastContentHash !== contentHash ||
(now.getTime() - cache.lastParsed.getTime()) > EDITOR_CONFIG.SYNTAX_TREE_CACHE_TIMEOUT;
if (shouldRebuild) {
try {
ensureSyntaxTree(view.state, docLength, 5000);
// 更新缓存
instance.syntaxTreeCache = {
lastDocLength: docLength,
lastContentHash: contentHash,
lastParsed: now
};
} catch (error) {
console.warn('Failed to ensure syntax tree:', error);
}
}
}; };
// 创建编辑器实例 // 创建编辑器实例
const createEditorInstance = async ( const createEditorInstance = async (
content: string, docId: number,
operationId: number, doc: Document
documentId: number ): Promise<EditorInstance> => {
): Promise<EditorView> => {
if (!containerElement.value) { if (!containerElement.value) {
throw new Error('Editor container not set'); throw new Error('Editor container not set');
} }
// 检查操作是否仍然有效 const content = doc.content || '';
if (!operationManager.isOperationValid(operationId, documentId)) {
throw new Error('Operation cancelled');
}
// 获取基本扩展 // 基本扩展
const basicExtensions = createBasicSetup(); const basicExtensions = createBasicSetup();
// 获取主题扩展 // 主题扩展
const themeExtension = createThemeExtension(); const themeExtension = createThemeExtension();
// Tab相关扩展 // Tab 扩展
const tabExtensions = getTabExtensions( const tabExtensions = getTabExtensions(
configStore.config.editing.tabSize, configStore.config.editing.tabSize,
configStore.config.editing.enableTabIndent, configStore.config.editing.enableTabIndent,
@@ -163,6 +90,7 @@ export const useEditorStore = defineStore('editor', () => {
fontWeight: configStore.config.editing.fontWeight fontWeight: configStore.config.editing.fontWeight
}); });
// 滚轮缩放扩展
const wheelZoomExtension = createWheelZoomExtension({ const wheelZoomExtension = createWheelZoomExtension({
increaseFontSize: () => configStore.increaseFontSizeLocal(), increaseFontSize: () => configStore.increaseFontSizeLocal(),
decreaseFontSize: () => configStore.decreaseFontSizeLocal(), decreaseFontSize: () => configStore.decreaseFontSizeLocal(),
@@ -171,10 +99,16 @@ export const useEditorStore = defineStore('editor', () => {
}); });
// 统计扩展 // 统计扩展
const statsExtension = createStatsUpdateExtension(updateDocumentStats); const statsExtension = createStatsUpdateExtension((stats: DocumentStats) => {
if (currentEditorId.value) {
editorStateStore.saveDocumentStats(currentEditorId.value, stats);
}
});
// 内容变化扩展 // 内容变化扩展
const contentChangeExtension = createContentChangePlugin(); const contentChangeExtension = createContentChangePlugin(() => {
handleContentChange(docId);
});
// 代码块扩展 // 代码块扩展
const codeBlockExtension = createCodeBlockExtension({ const codeBlockExtension = createCodeBlockExtension({
@@ -183,29 +117,17 @@ export const useEditorStore = defineStore('editor', () => {
}); });
// 光标位置持久化扩展 // 光标位置持久化扩展
const cursorPositionExtension = createCursorPositionExtension(documentId); const cursorPositionExtension = createCursorPositionExtension(docId);
// 再次检查操作有效性 // 折叠状态持久化扩展
if (!operationManager.isOperationValid(operationId, documentId)) { const foldStateExtension = createFoldStateExtension(docId);
throw new Error('Operation cancelled');
}
// 快捷键扩展 // 快捷键扩展
const keymapExtension = await createDynamicKeymapExtension(); const keymapExtension = await createDynamicKeymapExtension();
// 检查操作有效性 // 动态扩展
if (!operationManager.isOperationValid(operationId, documentId)) {
throw new Error('Operation cancelled');
}
// 动态扩展传递文档ID以便扩展管理器可以预初始化
const dynamicExtensions = await createDynamicExtensions(); const dynamicExtensions = await createDynamicExtensions();
// 最终检查操作有效性
if (!operationManager.isOperationValid(operationId, documentId)) {
throw new Error('Operation cancelled');
}
// 组合所有扩展 // 组合所有扩展
const extensions: Extension[] = [ const extensions: Extension[] = [
keymapExtension, keymapExtension,
@@ -218,327 +140,303 @@ export const useEditorStore = defineStore('editor', () => {
contentChangeExtension, contentChangeExtension,
codeBlockExtension, codeBlockExtension,
cursorPositionExtension, cursorPositionExtension,
foldStateExtension,
...dynamicExtensions, ...dynamicExtensions,
]; ];
// 获取保存的光标位置 // 获取保存的光标位置
const savedState = documentStore.documentStates[documentId]; const savedCursorPos = editorStateStore.getCursorPosition(docId);
const docLength = content.length; const docLength = content.length;
const initialCursorPos = savedState?.cursorPos !== undefined const initialCursorPos = savedCursorPos !== undefined
? Math.min(savedState.cursorPos, docLength) ? Math.min(savedCursorPos, docLength)
: docLength; : docLength;
// 创建编辑器状态
// 创建编辑器状态,设置初始光标位置
const state = EditorState.create({ const state = EditorState.create({
doc: content, doc: content,
extensions, extensions,
selection: { anchor: initialCursorPos, head: initialCursorPos } selection: {anchor: initialCursorPos, head: initialCursorPos}
}); });
return new EditorView({ const view = new EditorView({state});
state
});
};
// 添加编辑器到缓存 return {
const addEditorToCache = (documentId: number, view: EditorView, content: string) => {
const instance: EditorInstance = {
view, view,
documentId, documentId: docId,
content, contentTimestamp: doc.updated_at || '',
contentLength: content.length,
isDirty: false, isDirty: false,
lastModified: new Date(), lastModified: Date.now()
autoSaveTimer: createTimerManager(),
syntaxTreeCache: null,
editorState: documentStore.documentStates[documentId]
}; };
// 使用LRU缓存的onEvict回调处理被驱逐的实例
editorCache.set(documentId, instance, (_evictedKey, evictedInstance) => {
// 清除自动保存定时器
evictedInstance.autoSaveTimer.clear();
// 移除DOM元素
if (evictedInstance.view.dom.parentElement) {
evictedInstance.view.dom.remove();
}
evictedInstance.view.destroy();
});
// 初始化语法树缓存
ensureSyntaxTreeCached(view, documentId);
}; };
// 获取或创建编辑器 // 更新编辑器内容
const getOrCreateEditor = async ( const updateEditorContent = (instance: EditorInstance, doc: Document) => {
documentId: number, const currentContent = instance.view.state.doc.toString();
content: string, const newContent = doc.content || '';
operationId: number
): Promise<EditorView> => { // 如果内容相同,只更新元数据
// 检查缓存 if (currentContent === newContent) {
const cached = editorCache.get(documentId); instance.contentTimestamp = doc.updated_at || '';
if (cached) { instance.contentLength = newContent.length;
return cached.view; return;
} }
// 检查操作是否仍然有效 // 保存当前光标位置
if (!operationManager.isOperationValid(operationId, documentId)) { const currentCursorPos = instance.view.state.selection.main.head;
throw new Error('Operation cancelled');
}
// 创建新的编辑器实例 // 更新内容
const view = await createEditorInstance(content, operationId, documentId); instance.view.dispatch({
changes: {
// 完善取消操作时的清理逻辑 from: 0,
if (!operationManager.isOperationValid(operationId, documentId)) { to: instance.view.state.doc.length,
// 如果操作已取消,彻底清理创建的实例 insert: newContent
try {
// 移除 DOM 元素(如果已添加到文档)
if (view.dom && view.dom.parentElement) {
view.dom.remove();
}
// 销毁编辑器视图
view.destroy();
} catch (error) {
console.error('Error cleaning up cancelled editor:', error);
} }
throw new Error('Operation cancelled'); });
// 智能恢复光标位置
const newContentLength = newContent.length;
const safeCursorPos = Math.min(currentCursorPos, newContentLength);
if (safeCursorPos > 0 && safeCursorPos < newContentLength) {
instance.view.dispatch({
selection: {anchor: safeCursorPos, head: safeCursorPos}
});
} }
addEditorToCache(documentId, view, content); // 同步元数据
instance.contentTimestamp = doc.updated_at || '';
return view; instance.contentLength = newContent.length;
instance.isDirty = false;
}; };
// 显示编辑器 // 显示编辑器
const showEditor = (documentId: number) => { const showEditor = (instance: EditorInstance) => {
const instance = editorCache.get(documentId); if (!containerElement.value) return;
if (!instance || !containerElement.value) return;
try { try {
// 移除当前编辑器DOM // 移除当前编辑器 DOM
if (currentEditor.value && currentEditor.value.dom && currentEditor.value.dom.parentElement) { const currentEditor = editorCache.get(currentEditorId.value || 0);
currentEditor.value.dom.remove(); if (currentEditor && currentEditor.view.dom && currentEditor.view.dom.parentElement) {
currentEditor.view.dom.remove();
} }
// 目标编辑器DOM添加到容器 // 添加目标编辑器 DOM
containerElement.value.appendChild(instance.view.dom); containerElement.value.appendChild(instance.view.dom);
currentEditor.value = instance.view; currentEditorId.value = instance.documentId;
// 设置扩展管理器视图 // 设置扩展管理器视图
setExtensionManagerView(instance.view, documentId); setExtensionManagerView(instance.view, instance.documentId);
//使用 nextTick + requestAnimationFrame 确保 DOM 完全渲染 // 使用 requestAnimationFrame 确保 DOM 渲染
nextTick(() => { requestAnimationFrame(() => {
requestAnimationFrame(() => { scrollToCursor(instance.view);
// 滚动到当前光标位置 instance.view.focus();
scrollToCursor(instance.view);
// 恢复折叠状态
// 聚焦编辑器 const savedFoldState = editorStateStore.getFoldState(instance.documentId);
instance.view.focus(); if (savedFoldState.length > 0) {
restoreFoldState(instance.view, savedFoldState);
// 使用缓存的语法树确保方法 }
ensureSyntaxTreeCached(instance.view, documentId);
});
}); });
} catch (error) { } catch (error) {
console.error('Error showing editor:', error); console.error('Error showing editor:', error);
} }
}; };
// 保存编辑器内容
const saveEditorContent = async (documentId: number): Promise<boolean> => {
const instance = editorCache.get(documentId);
if (!instance || !instance.isDirty) return true;
try {
const content = instance.view.state.doc.toString();
const lastModified = instance.lastModified;
await DocumentService.UpdateDocumentContent(documentId, content);
// 检查在保存期间内容是否又被修改了
if (instance.lastModified === lastModified) {
instance.content = content;
instance.isDirty = false;
instance.lastModified = new Date();
}
return true;
} catch (error) {
console.error('Failed to save editor content:', error);
return false;
}
};
// 内容变化处理 // 内容变化处理
const onContentChange = () => { const handleContentChange = (docId: number) => {
const documentId = documentStore.currentDocumentId; const instance = editorCache.get(docId);
if (!documentId) return;
const instance = editorCache.get(documentId);
if (!instance) return; if (!instance) return;
// 立即设置脏标记和修改时间(切换文档时需要判断) // 标记为脏数据
instance.isDirty = true; instance.isDirty = true;
instance.lastModified = new Date(); instance.lastModified = Date.now();
// 优使用防抖清理语法树缓存
debouncedClearSyntaxCache.debouncedFn(instance);
// 设置自动保存定时器(已经是防抖效果:每次重置定时器) // 调度自动保存
instance.autoSaveTimer.set(() => { const autoSaveDelay = configStore.config.editing.autoSaveDelay;
saveEditorContent(documentId); documentStore.scheduleAutoSave(
}, getAutoSaveDelay()); docId,
async () => {
const content = instance.view.state.doc.toString();
const savedDoc = await documentStore.saveDocument(docId, content);
// 同步版本信息
if (savedDoc) {
instance.contentTimestamp = savedDoc.updated_at || '';
instance.contentLength = (savedDoc.content || '').length;
instance.isDirty = false;
}
},
autoSaveDelay
);
}; };
// 设置编辑器容 // 切换到指定编辑
const setEditorContainer = (container: HTMLElement | null) => { const switchToEditor = async (docId: number) => {
containerElement.value = container;
// 如果设置容器时已有当前文档,立即加载编辑器
if (container && documentStore.currentDocument && documentStore.currentDocument.id !== undefined) {
loadEditor(documentStore.currentDocument.id, documentStore.currentDocument.content || '');
}
};
// 加载编辑器
const loadEditor = async (documentId: number, content: string) => {
// 修复:使用计数器精确管理加载状态
loadingOperations.value++;
isLoading.value = true; isLoading.value = true;
// 开始新的操作
const { operationId } = operationManager.startOperation(documentId);
try { try {
// 验证参数 // 直接从后端获取文档
if (!documentId) { const doc = await documentStore.getDocument(docId);
throw new Error('Invalid parameters for loadEditor'); if (!doc) {
throw new Error(`Failed to load document ${docId}`);
} }
// 保存当前编辑器内容 const cached = editorCache.get(docId);
if (currentEditor.value) {
const currentDocId = documentStore.currentDocumentId; if (cached) {
if (currentDocId && currentDocId !== documentId) { // 场景1缓存有效
await saveEditorContent(currentDocId); if (isCacheValid(cached, doc)) {
showEditor(cached);
// 检查操作是否仍然有效 return;
if (!operationManager.isOperationValid(operationId, documentId)) { }
return;
// 场景2有未保存修改
if (cached.isDirty) {
// 检查内容是否真的不同
if (!hasContentChanged(cached, doc)) {
// 内容实际相同,只是元数据变了,同步元数据
cached.contentTimestamp = doc.updated_at || '';
cached.contentLength = (doc.content || '').length;
cached.isDirty = false;
} }
// 内容不同,保留用户编辑
showEditor(cached);
return;
} }
}
// 获取或创建编辑器 // 场景3缓存失效且无脏数据更新内容
const view = await getOrCreateEditor(documentId, content, operationId); updateEditorContent(cached, doc);
showEditor(cached);
// 检查操作是否仍然有效
if (!operationManager.isOperationValid(operationId, documentId)) {
return;
}
// 更新内容(如果需要)
const instance = editorCache.get(documentId);
if (instance && instance.content !== content) {
// 确保编辑器视图有效
if (view && view.state && view.dispatch) {
view.dispatch({
changes: {
from: 0,
to: view.state.doc.length,
insert: content
}
});
instance.content = content;
instance.isDirty = false;
// 清理语法树缓存,因为内容已更新
instance.syntaxTreeCache = null;
// 修复:内容变了,清空光标位置,避免越界
instance.editorState = undefined;
delete documentStore.documentStates[documentId];
}
}
// 最终检查操作有效性
if (!operationManager.isOperationValid(operationId, documentId)) {
return;
}
// 显示编辑器
showEditor(documentId);
} catch (error) {
if (error instanceof Error && error.message === 'Operation cancelled') {
console.log(`Editor loading cancelled for document ${documentId}`);
} else { } else {
console.error('Failed to load editor:', error); // 场景4创建新编辑器
const editor = await createEditorInstance(docId, doc);
// 添加到缓存
editorCache.set(docId, editor, (_evictedKey, evictedInstance) => {
// 保存光标位置
const cursorPos = evictedInstance.view.state.selection.main.head;
editorStateStore.saveCursorPosition(evictedInstance.documentId, cursorPos);
// 从扩展管理器移除
removeExtensionManagerView(evictedInstance.documentId);
// 移除 DOM
if (evictedInstance.view.dom.parentElement) {
evictedInstance.view.dom.remove();
}
// 销毁编辑器
evictedInstance.view.destroy();
});
showEditor(editor);
} }
} catch (error) {
console.error('Failed to switch editor:', error);
} finally { } finally {
// 完成操作
operationManager.completeOperation(operationId);
// 修复:使用计数器精确管理加载状态,避免快速切换时状态不准确
loadingOperations.value--;
// 延迟一段时间后再取消加载状态,但要确保所有操作都完成了
setTimeout(() => { setTimeout(() => {
if (loadingOperations.value <= 0) { isLoading.value = false;
loadingOperations.value = 0;
isLoading.value = false;
}
}, EDITOR_CONFIG.LOADING_DELAY); }, EDITOR_CONFIG.LOADING_DELAY);
} }
}; };
// 移除编辑器 // 获取当前内容
const removeEditor = async (documentId: number) => { const getCurrentContent = (): string => {
const instance = editorCache.get(documentId); if (!currentEditorId.value) return '';
if (instance) { const instance = editorCache.get(currentEditorId.value);
try { return instance ? instance.view.state.doc.toString() : '';
// 如果正在加载这个文档,取消操作 };
if (operationManager.getCurrentContext() === documentId) {
operationManager.cancelAllOperations();
}
// 修复:移除前先保存内容(如果有未保存的修改) // 获取当前光标位置
if (instance.isDirty) { const getCurrentCursorPosition = (): number => {
await saveEditorContent(documentId); if (!currentEditorId.value) return 0;
} const instance = editorCache.get(currentEditorId.value);
return instance ? instance.view.state.selection.main.head : 0;
};
// 清除自动保存定时器 // 检查是否有未保存修改
instance.autoSaveTimer.clear(); const hasUnsavedChanges = (docId: number): boolean => {
const instance = editorCache.get(docId);
return instance?.isDirty || false;
};
// 从扩展管理器中移除视图 // 同步保存后的版本信息
removeExtensionManagerView(documentId); const syncAfterSave = async (docId: number) => {
const instance = editorCache.get(docId);
if (!instance) return;
// 移除DOM元素 const doc = await documentStore.getDocument(docId);
if (instance.view && instance.view.dom && instance.view.dom.parentElement) { if (doc) {
instance.view.dom.remove(); instance.contentTimestamp = doc.updated_at || '';
} instance.contentLength = (doc.content || '').length;
instance.isDirty = false;
// 销毁编辑器
if (instance.view && instance.view.destroy) {
instance.view.destroy();
}
// 清理引用
if (currentEditor.value === instance.view) {
currentEditor.value = null;
}
// 从缓存中删除
editorCache.delete(documentId);
} catch (error) {
console.error('Error removing editor:', error);
}
} }
}; };
// 更新文档统计 // 销毁编辑器
const updateDocumentStats = (stats: DocumentStats) => { const destroyEditor = async (docId: number) => {
documentStats.value = stats; const instance = editorCache.get(docId);
if (!instance) return;
try {
// 保存光标位置
const cursorPos = instance.view.state.selection.main.head;
editorStateStore.saveCursorPosition(docId, cursorPos);
// 从扩展管理器移除
removeExtensionManagerView(docId);
// 移除 DOM
if (instance.view.dom && instance.view.dom.parentElement) {
instance.view.dom.remove();
}
// 销毁编辑器
instance.view.destroy();
// 从缓存删除
editorCache.delete(docId);
// 清空当前编辑器引用
if (currentEditorId.value === docId) {
currentEditorId.value = null;
}
} catch (error) {
console.error('Error destroying editor:', error);
}
}; };
// 清空所有编辑器
const destroyAllEditors = () => {
editorCache.clear((_documentId, instance) => {
// 保存光标位置
const cursorPos = instance.view.state.selection.main.head;
editorStateStore.saveCursorPosition(instance.documentId, cursorPos);
// 从扩展管理器移除
removeExtensionManagerView(instance.documentId);
// 移除 DOM
if (instance.view.dom.parentElement) {
instance.view.dom.remove();
}
// 销毁编辑器
instance.view.destroy();
});
currentEditorId.value = null;
};
// 设置编辑器容器
const setEditorContainer = (container: HTMLElement | null) => {
containerElement.value = container;
};
// 应用字体设置 // 应用字体设置
const applyFontSettings = () => { const applyFontSettings = () => {
editorCache.values().forEach(instance => { editorCache.values().forEach(instance => {
@@ -558,8 +456,7 @@ export const useEditorStore = defineStore('editor', () => {
}); });
}; };
// 应用 Tab 设置
// 应用Tab设置
const applyTabSettings = () => { const applyTabSettings = () => {
editorCache.values().forEach(instance => { editorCache.values().forEach(instance => {
updateTabConfig( updateTabConfig(
@@ -573,7 +470,6 @@ export const useEditorStore = defineStore('editor', () => {
// 应用快捷键设置 // 应用快捷键设置
const applyKeymapSettings = async () => { const applyKeymapSettings = async () => {
// 确保所有编辑器实例的快捷键都更新
await Promise.all( await Promise.all(
editorCache.values().map(instance => editorCache.values().map(instance =>
updateKeymapExtension(instance.view) updateKeymapExtension(instance.view)
@@ -581,104 +477,36 @@ export const useEditorStore = defineStore('editor', () => {
); );
}; };
// 清空所有编辑器 const hasContainer = computed(() => containerElement.value !== null);
const clearAllEditors = () => { const currentEditor = computed(() => {
// 取消所有挂起的操作 if (!currentEditorId.value) return null;
operationManager.cancelAllOperations(); const instance = editorCache.get(currentEditorId.value);
return instance ? instance.view : null;
editorCache.clear((_documentId, instance) => {
// 清除自动保存定时器
instance.autoSaveTimer.clear();
// 从扩展管理器移除
removeExtensionManagerView(instance.documentId);
// 移除DOM元素
if (instance.view.dom.parentElement) {
instance.view.dom.remove();
}
// 销毁编辑器
instance.view.destroy();
});
currentEditor.value = null;
};
// 更新扩展
const updateExtension = async (id: number, enabled: boolean, config?: any) => {
// 更新启用状态
await ExtensionService.UpdateExtensionEnabled(id, enabled);
// 如果需要更新配置
if (config !== undefined) {
await ExtensionService.UpdateExtensionConfig(id, config);
}
// 重新加载扩展配置
await extensionStore.loadExtensions();
// 获取更新后的扩展名称
const extension = extensionStore.extensions.find(ext => ext.id === id);
if (!extension) return;
// 更新前端编辑器扩展 - 应用于所有实例
const manager = getExtensionManager();
if (manager) {
// 直接更新前端扩展至所有视图
manager.updateExtension(extension.name, enabled, config);
}
await useKeybindingStore().loadKeyBindings();
await applyKeymapSettings();
};
// 监听文档切换
watch(() => documentStore.currentDocument, async (newDoc, oldDoc) => {
if (newDoc && newDoc.id !== undefined && containerElement.value) {
// 等待 DOM 更新完成,再加载新文档的编辑器
await nextTick();
loadEditor(newDoc.id, newDoc.content || '');
}
}); });
// 创建字体配置的计算属性
const fontConfig = computed(() => ({
fontSize: configStore.config.editing.fontSize,
fontFamily: configStore.config.editing.fontFamily,
lineHeight: configStore.config.editing.lineHeight,
fontWeight: configStore.config.editing.fontWeight
}));
// 创建Tab配置的计算属性
const tabConfig = computed(() => ({
tabSize: configStore.config.editing.tabSize,
enableTabIndent: configStore.config.editing.enableTabIndent,
tabType: configStore.config.editing.tabType
}));
// 监听字体配置变化
watch(fontConfig, applyFontSettings, { deep: true });
// 监听Tab配置变化
watch(tabConfig, applyTabSettings, { deep: true });
return { return {
// 状态 // 状态
currentEditorId: readonly(currentEditorId),
currentEditor, currentEditor,
documentStats, isLoading: readonly(isLoading),
isLoading, hasContainer,
// 方法 // 编辑器管理
setEditorContainer, setEditorContainer,
loadEditor, switchToEditor,
removeEditor, destroyEditor,
clearAllEditors, destroyAllEditors,
onContentChange,
// 配置更新方法 // 查询方法
getCurrentContent,
getCurrentCursorPosition,
hasUnsavedChanges,
syncAfterSave,
// 配置应用
applyFontSettings, applyFontSettings,
applyThemeSettings, applyThemeSettings,
applyTabSettings, applyTabSettings,
applyKeymapSettings, applyKeymapSettings,
// 扩展管理方法
updateExtension,
editorView: currentEditor,
}; };
}); });

View File

@@ -18,12 +18,9 @@ export const useTabStore = defineStore('tab', () => {
const tabsMap = ref<Record<number, Tab>>({}); const tabsMap = ref<Record<number, Tab>>({});
const tabOrder = ref<number[]>([]); // 维护标签页顺序 const tabOrder = ref<number[]>([]); // 维护标签页顺序
const draggedTabId = ref<number | null>(null); const draggedTabId = ref<number | null>(null);
// === 计算属性 ===
const isTabsEnabled = computed(() => configStore.config.general.enableTabs); const isTabsEnabled = computed(() => configStore.config.general.enableTabs);
const canCloseTab = computed(() => tabOrder.value.length > 1); const canCloseTab = computed(() => tabOrder.value.length > 1);
const currentDocumentId = computed(() => documentStore.currentDocumentId);
// 按顺序返回标签页数组用于UI渲染 // 按顺序返回标签页数组用于UI渲染
const tabs = computed(() => { const tabs = computed(() => {
@@ -75,7 +72,7 @@ export const useTabStore = defineStore('tab', () => {
/** /**
* 关闭标签页 * 关闭标签页
*/ */
const closeTab = (documentId: number) => { const closeTab = async (documentId: number) => {
if (!hasTab(documentId)) return; if (!hasTab(documentId)) return;
const tabIndex = tabOrder.value.indexOf(documentId); const tabIndex = tabOrder.value.indexOf(documentId);
@@ -95,7 +92,7 @@ export const useTabStore = defineStore('tab', () => {
if (nextIndex >= 0 && tabOrder.value[nextIndex]) { if (nextIndex >= 0 && tabOrder.value[nextIndex]) {
const nextDocumentId = tabOrder.value[nextIndex]; const nextDocumentId = tabOrder.value[nextIndex];
switchToTabAndDocument(nextDocumentId); await switchToTabAndDocument(nextDocumentId);
} }
} }
}; };
@@ -120,15 +117,15 @@ export const useTabStore = defineStore('tab', () => {
/** /**
* 切换到指定标签页并打开对应文档 * 切换到指定标签页并打开对应文档
*/ */
const switchToTabAndDocument = (documentId: number) => { const switchToTabAndDocument = async (documentId: number) => {
if (!hasTab(documentId)) return; if (!hasTab(documentId)) return;
// 如果点击的是当前已激活的文档,不需要重复请求 // 如果点击的是当前已激活的文档,不需要重复请求
if (documentStore.currentDocumentId === documentId) { if (documentStore.currentDocumentId === documentId) {
return; return;
} }
documentStore.openDocument(documentId); await documentStore.openDocument(documentId);
}; };
/** /**
@@ -154,8 +151,9 @@ export const useTabStore = defineStore('tab', () => {
/** /**
* 验证并清理无效的标签页 * 验证并清理无效的标签页
*/ */
const validateTabs = () => { const validateTabs = async () => {
const validDocIds = Object.keys(documentStore.documents).map(Number); const docs = await documentStore.getDocumentList();
const validDocIds = docs.map(doc => doc.id).filter((id): id is number => id !== undefined);
// 找出无效的标签页(文档已被删除) // 找出无效的标签页(文档已被删除)
const invalidTabIds = tabOrder.value.filter(docId => !validDocIds.includes(docId)); const invalidTabIds = tabOrder.value.filter(docId => !validDocIds.includes(docId));
@@ -172,9 +170,9 @@ export const useTabStore = defineStore('tab', () => {
/** /**
* 初始化标签页(当前文档) * 初始化标签页(当前文档)
*/ */
const initializeTab = () => { const initTab = async () => {
// 先验证并清理无效的标签页(处理持久化的脏数据) // 先验证并清理无效的标签页
validateTabs(); await validateTabs();
if (isTabsEnabled.value) { if (isTabsEnabled.value) {
const currentDoc = documentStore.currentDocument; const currentDoc = documentStore.currentDocument;
@@ -189,7 +187,7 @@ export const useTabStore = defineStore('tab', () => {
/** /**
* 关闭其他标签页(除了指定的标签页) * 关闭其他标签页(除了指定的标签页)
*/ */
const closeOtherTabs = (keepDocumentId: number) => { const closeOtherTabs = async (keepDocumentId: number) => {
if (!hasTab(keepDocumentId)) return; if (!hasTab(keepDocumentId)) return;
// 获取所有其他标签页的ID // 获取所有其他标签页的ID
@@ -200,14 +198,14 @@ export const useTabStore = defineStore('tab', () => {
// 如果当前打开的文档在被关闭的标签中,需要切换到保留的文档 // 如果当前打开的文档在被关闭的标签中,需要切换到保留的文档
if (otherTabIds.includes(documentStore.currentDocumentId!)) { if (otherTabIds.includes(documentStore.currentDocumentId!)) {
switchToTabAndDocument(keepDocumentId); await switchToTabAndDocument(keepDocumentId);
} }
}; };
/** /**
* 关闭指定标签页右侧的所有标签页 * 关闭指定标签页右侧的所有标签页
*/ */
const closeTabsToRight = (documentId: number) => { const closeTabsToRight = async (documentId: number) => {
const index = getTabIndex(documentId); const index = getTabIndex(documentId);
if (index === -1) return; if (index === -1) return;
@@ -219,14 +217,14 @@ export const useTabStore = defineStore('tab', () => {
// 如果当前打开的文档在被关闭的右侧标签中,需要切换到指定的文档 // 如果当前打开的文档在被关闭的右侧标签中,需要切换到指定的文档
if (rightTabIds.includes(documentStore.currentDocumentId!)) { if (rightTabIds.includes(documentStore.currentDocumentId!)) {
switchToTabAndDocument(documentId); await switchToTabAndDocument(documentId);
} }
}; };
/** /**
* 关闭指定标签页左侧的所有标签页 * 关闭指定标签页左侧的所有标签页
*/ */
const closeTabsToLeft = (documentId: number) => { const closeTabsToLeft = async (documentId: number) => {
const index = getTabIndex(documentId); const index = getTabIndex(documentId);
if (index <= 0) return; if (index <= 0) return;
@@ -238,7 +236,7 @@ export const useTabStore = defineStore('tab', () => {
// 如果当前打开的文档在被关闭的左侧标签中,需要切换到指定的文档 // 如果当前打开的文档在被关闭的左侧标签中,需要切换到指定的文档
if (leftTabIds.includes(documentStore.currentDocumentId!)) { if (leftTabIds.includes(documentStore.currentDocumentId!)) {
switchToTabAndDocument(documentId); await switchToTabAndDocument(documentId);
} }
}; };
@@ -262,7 +260,6 @@ export const useTabStore = defineStore('tab', () => {
// 计算属性 // 计算属性
isTabsEnabled, isTabsEnabled,
canCloseTab, canCloseTab,
currentDocumentId,
// 方法 // 方法
addOrActivateTab, addOrActivateTab,
@@ -273,7 +270,7 @@ export const useTabStore = defineStore('tab', () => {
switchToTabAndDocument, switchToTabAndDocument,
moveTab, moveTab,
getTabIndex, getTabIndex,
initializeTab, initTab,
clearAllTabs, clearAllTabs,
updateTabTitle, updateTabTitle,
validateTabs, validateTabs,

View File

@@ -4,12 +4,12 @@ import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
import {Type as ThemeType} from '@/../bindings/voidraft/internal/models/ent/theme/models'; import {Type as ThemeType} from '@/../bindings/voidraft/internal/models/ent/theme/models';
import {ThemeService} from '@/../bindings/voidraft/internal/services'; import {ThemeService} from '@/../bindings/voidraft/internal/services';
import {useConfigStore} from './configStore'; import {useConfigStore} from './configStore';
import {useEditorStore} from './editorStore';
import type {ThemeColors} from '@/views/editor/theme/types'; import type {ThemeColors} from '@/views/editor/theme/types';
import {cloneThemeColors, FALLBACK_THEME_NAME, themePresetList, themePresetMap} from '@/views/editor/theme/presets'; import {cloneThemeColors, FALLBACK_THEME_NAME, themePresetList, themePresetMap} from '@/views/editor/theme/presets';
import {useEditorStore} from "@/stores/editorStore";
// 类型定义 // 类型定义
type ThemeOption = {name: string; type: ThemeType}; type ThemeOption = { name: string; type: ThemeType };
// 解析主题名称,确保返回有效的主题 // 解析主题名称,确保返回有效的主题
const resolveThemeName = (name?: string): string => const resolveThemeName = (name?: string): string =>
@@ -62,15 +62,11 @@ export const useThemeStore = defineStore('theme', () => {
// 从服务器获取主题颜色 // 从服务器获取主题颜色
const fetchThemeColors = async (themeName: string): Promise<ThemeColors> => { const fetchThemeColors = async (themeName: string): Promise<ThemeColors> => {
const safeName = resolveThemeName(themeName); const safeName = resolveThemeName(themeName);
try { const theme = await ThemeService.GetThemeByName(safeName);
const theme = await ThemeService.GetThemeByName(safeName); if (theme?.colors) {
if (theme?.colors) { const colors = cloneThemeColors(theme.colors as ThemeColors);
const colors = cloneThemeColors(theme.colors as ThemeColors); colors.themeName = safeName;
colors.themeName = safeName; return colors;
return colors;
}
} catch (error) {
console.error('Failed to load theme override:', error);
} }
return getPresetColors(safeName); return getPresetColors(safeName);
}; };
@@ -80,21 +76,36 @@ export const useThemeStore = defineStore('theme', () => {
const targetName = resolveThemeName( const targetName = resolveThemeName(
themeName || configStore.config?.appearance?.currentTheme themeName || configStore.config?.appearance?.currentTheme
); );
currentColors.value = getPresetColors(targetName);
currentColors.value = await fetchThemeColors(targetName); currentColors.value = await fetchThemeColors(targetName);
};
// 获取可用的主题颜色
const getEffectiveColors = (): ThemeColors => {
const targetName = resolveThemeName(
currentColors.value?.themeName || configStore.config?.appearance?.currentTheme
);
return currentColors.value ?? getPresetColors(targetName);
};
// 同步应用到 DOM 与编辑器
const applyAllThemes = () => {
applyThemeToDOM(currentTheme.value);
const editorStore = useEditorStore();
editorStore.applyThemeSettings();
}; };
// 初始化主题 // 初始化主题
const initTheme = async () => { const initTheme = async () => {
applyThemeToDOM(currentTheme.value);
await loadThemeColors(); await loadThemeColors();
refreshEditorTheme(); applyAllThemes();
}; };
// 设置系统主题 // 设置系统主题
const setTheme = async (theme: SystemThemeType) => { const setTheme = async (theme: SystemThemeType) => {
await configStore.setSystemTheme(theme); await configStore.setSystemTheme(theme);
applyThemeToDOM(theme); applyAllThemes();
refreshEditorTheme();
}; };
// 切换到指定主题 // 切换到指定主题
@@ -106,7 +117,7 @@ export const useThemeStore = defineStore('theme', () => {
await loadThemeColors(themeName); await loadThemeColors(themeName);
await configStore.setCurrentTheme(themeName); await configStore.setCurrentTheme(themeName);
refreshEditorTheme(); applyAllThemes();
return true; return true;
}; };
@@ -128,7 +139,7 @@ export const useThemeStore = defineStore('theme', () => {
await ThemeService.UpdateTheme(themeName, currentColors.value); await ThemeService.UpdateTheme(themeName, currentColors.value);
await loadThemeColors(themeName); await loadThemeColors(themeName);
refreshEditorTheme(); applyAllThemes();
return true; return true;
}; };
@@ -142,16 +153,10 @@ export const useThemeStore = defineStore('theme', () => {
await ThemeService.ResetTheme(themeName); await ThemeService.ResetTheme(themeName);
await loadThemeColors(themeName); await loadThemeColors(themeName);
refreshEditorTheme(); applyAllThemes();
return true; return true;
}; };
// 刷新编辑器主题
const refreshEditorTheme = () => {
applyThemeToDOM(currentTheme.value);
const editorStore = useEditorStore();
editorStore?.applyThemeSettings();
};
return { return {
availableThemes, availableThemes,
@@ -164,7 +169,8 @@ export const useThemeStore = defineStore('theme', () => {
updateCurrentColors, updateCurrentColors,
saveCurrentTheme, saveCurrentTheme,
resetCurrentTheme, resetCurrentTheme,
refreshEditorTheme,
applyThemeToDOM, applyThemeToDOM,
applyAllThemes,
getEffectiveColors,
}; };
}); });

View File

@@ -28,11 +28,16 @@ onMounted(async () => {
const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined; const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined;
await documentStore.initialize(urlDocumentId); await documentStore.initDocument(urlDocumentId);
editorStore.setEditorContainer(editorElement.value); editorStore.setEditorContainer(editorElement.value);
await tabStore.initializeTab(); const currentDocId = documentStore.currentDocumentId;
if (currentDocId) {
await editorStore.switchToEditor(currentDocId);
}
await tabStore.initTab();
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@@ -1,13 +1,13 @@
import {EditorView, ViewPlugin, ViewUpdate} from '@codemirror/view'; import {EditorView, ViewPlugin, ViewUpdate} from '@codemirror/view';
import type {Text} from '@codemirror/state'; import type {Text} from '@codemirror/state';
import {useEditorStore} from '@/stores/editorStore';
/** /**
* 内容变化监听扩展
* 通过回调函数解耦,不直接依赖 Store
*/ */
export function createContentChangePlugin() { export function createContentChangePlugin(onContentChange: () => void) {
return ViewPlugin.fromClass( return ViewPlugin.fromClass(
class ContentChangePlugin { class ContentChangePlugin {
private readonly editorStore = useEditorStore();
private lastDoc: Text; private lastDoc: Text;
private rafId: number | null = null; private rafId: number | null = null;
private pendingNotification = false; private pendingNotification = false;
@@ -40,7 +40,7 @@ export function createContentChangePlugin() {
this.rafId = requestAnimationFrame(() => { this.rafId = requestAnimationFrame(() => {
this.pendingNotification = false; this.pendingNotification = false;
this.rafId = null; this.rafId = null;
this.editorStore.onContentChange(); onContentChange(); // 调用注入的回调
}); });
} }
} }

View File

@@ -1,15 +1,15 @@
import {EditorView, ViewPlugin, ViewUpdate} from '@codemirror/view'; import {EditorView, ViewPlugin, ViewUpdate} from '@codemirror/view';
import {useDocumentStore} from '@/stores/documentStore'; import {useEditorStateStore} from '@/stores/editorStateStore';
import {createDebounce} from '@/common/utils/debounce'; import {createDebounce} from '@/common/utils/debounce';
/** /**
* 光标位置持久化扩展 * 光标位置持久化扩展
* 实时监听光标位置变化并持久化到 documentStore * 实时监听光标位置变化并持久化到 editorStateStore
*/ */
export function createCursorPositionExtension(documentId: number) { export function createCursorPositionExtension(documentId: number) {
return ViewPlugin.fromClass( return ViewPlugin.fromClass(
class CursorPositionPlugin { class CursorPositionPlugin {
private readonly documentStore = useDocumentStore(); private readonly editorStateStore = useEditorStateStore();
private readonly debouncedSave; private readonly debouncedSave;
constructor(private view: EditorView) { constructor(private view: EditorView) {
@@ -42,11 +42,7 @@ export function createCursorPositionExtension(documentId: number) {
private saveCursorPosition() { private saveCursorPosition() {
const cursorPos = this.view.state.selection.main.head; const cursorPos = this.view.state.selection.main.head;
if (!this.documentStore.documentStates[documentId]) { this.editorStateStore.saveCursorPosition(documentId, cursorPos);
this.documentStore.documentStates[documentId] = {cursorPos};
} else {
this.documentStore.documentStates[documentId].cursorPos = cursorPos;
}
} }
} }
); );

View File

@@ -0,0 +1,113 @@
import {EditorView, ViewPlugin, ViewUpdate} from '@codemirror/view';
import {foldedRanges, foldEffect, unfoldEffect} from '@codemirror/language';
import {StateEffect} from '@codemirror/state';
import {useEditorStateStore, type FoldRange} from '@/stores/editorStateStore';
import {createDebounce} from '@/common/utils/debounce';
/**
* 折叠状态持久化扩展
*/
export function createFoldStateExtension(documentId: number) {
return ViewPlugin.fromClass(
class FoldStatePlugin {
private readonly editorStateStore = useEditorStateStore();
private readonly debouncedSave;
constructor(private view: EditorView) {
const {debouncedFn, flush} = createDebounce(
() => this.saveFoldState(),
{delay: 500}
);
this.debouncedSave = {fn: debouncedFn, flush};
}
update(update: ViewUpdate) {
// 检查是否有折叠/展开操作
const hasFoldChange = update.transactions.some(tr =>
tr.effects.some(effect =>
effect.is(foldEffect) || effect.is(unfoldEffect)
)
);
if (hasFoldChange) {
this.debouncedSave.fn();
}
}
destroy() {
// 销毁时立即执行待保存的操作
this.debouncedSave.flush();
// 再保存一次确保最新状态
this.saveFoldState();
}
private saveFoldState() {
const foldRanges: FoldRange[] = [];
const foldCursor = foldedRanges(this.view.state).iter();
const doc = this.view.state.doc;
// 遍历所有折叠区间
while (foldCursor.value !== null) {
const from = foldCursor.from;
const to = foldCursor.to;
// 同时记录字符偏移和行号
const fromLine = doc.lineAt(from).number;
const toLine = doc.lineAt(to).number;
foldRanges.push({
from,
to,
fromLine,
toLine
});
foldCursor.next();
}
this.editorStateStore.saveFoldState(documentId, foldRanges);
}
}
);
}
/**
* 恢复折叠状态(基于行号,更稳定)
* @param view 编辑器视图
* @param foldRanges 要恢复的折叠区间
*/
export function restoreFoldState(view: EditorView, foldRanges: FoldRange[]) {
if (foldRanges.length === 0) return;
const doc = view.state.doc;
const effects: StateEffect<any>[] = [];
for (const range of foldRanges) {
try {
// 优先使用行号恢复
if (range.fromLine && range.toLine) {
// 确保行号在有效范围内
if (range.fromLine >= 1 && range.toLine <= doc.lines && range.fromLine <= range.toLine) {
const fromPos = doc.line(range.fromLine).from;
const toPos = doc.line(range.toLine).to;
effects.push(foldEffect.of({from: fromPos, to: toPos}));
continue;
}
}
// 使用字符偏移
if (range.from >= 0 && range.to <= doc.length && range.from < range.to) {
effects.push(foldEffect.of({from: range.from, to: range.to}));
}
} catch (error) {
// 忽略无效的折叠区间
console.warn('Failed to restore fold range:', range, error);
}
}
if (effects.length > 0) {
view.dispatch({effects});
}
}

View File

@@ -1,6 +1,6 @@
import {Extension} from '@codemirror/state'; import {Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view'; import {EditorView} from '@codemirror/view';
import {DocumentStats} from '@/stores/editorStore'; import {DocumentStats} from '@/stores/editorStateStore';
import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state'; import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state';
// 更新编辑器文档统计信息 // 更新编辑器文档统计信息

View File

@@ -9,15 +9,11 @@ export const themeCompartment = new Compartment();
/** /**
* 根据主题类型获取主题扩展 * 根据主题类型获取主题扩展
*/ */
const getThemeExtension = (): Extension | null => { const getThemeExtension = (): Extension => {
const themeStore = useThemeStore(); const themeStore = useThemeStore();
// 直接获取当前主题颜色配置 // 获取有效主题颜色
const colors = themeStore.currentColors; const colors = themeStore.getEffectiveColors();
if (!colors) {
return null;
}
// 使用颜色配置创建主题 // 使用颜色配置创建主题
return createThemeByColors(colors); return createThemeByColors(colors);
@@ -28,12 +24,6 @@ const getThemeExtension = (): Extension | null => {
*/ */
export const createThemeExtension = (): Extension => { export const createThemeExtension = (): Extension => {
const extension = getThemeExtension(); const extension = getThemeExtension();
// 如果主题未加载,返回空扩展
if (!extension) {
return themeCompartment.of([]);
}
return themeCompartment.of(extension); return themeCompartment.of(extension);
}; };
@@ -48,11 +38,6 @@ export const updateEditorTheme = (view: EditorView): void => {
try { try {
const extension = getThemeExtension(); const extension = getThemeExtension();
// 如果主题未加载,不更新
if (!extension) {
return;
}
view.dispatch({ view.dispatch({
effects: themeCompartment.reconfigure(extension) effects: themeCompartment.reconfigure(extension)
}); });
@@ -60,4 +45,3 @@ export const updateEditorTheme = (view: EditorView): void => {
console.error('Failed to update editor theme:', error); console.error('Failed to update editor theme:', error);
} }
}; };

View File

@@ -39,6 +39,9 @@ export interface CodeBlockOptions {
/** 新建块时的默认语言 */ /** 新建块时的默认语言 */
defaultLanguage?: SupportedLanguage; defaultLanguage?: SupportedLanguage;
/** 分隔符高度(像素) */
separatorHeight?: number;
} }
/** /**

View File

@@ -1,12 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { ref } from 'vue'; import { ref, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { useConfigStore } from '@/stores/configStore';
import MemoryMonitor from '@/components/monitor/MemoryMonitor.vue'; import MemoryMonitor from '@/components/monitor/MemoryMonitor.vue';
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const configStore = useConfigStore();
// 计算属性
const enableMemoryMonitor = computed(() => configStore.config.general.enableMemoryMonitor);
// 导航配置 // 导航配置
const navItems = [ const navItems = [
@@ -64,7 +69,7 @@ const goBackToEditor = async () => {
<span class="nav-text">{{ item.id === 'test' ? 'Test' : t(`settings.${item.id}`) }}</span> <span class="nav-text">{{ item.id === 'test' ? 'Test' : t(`settings.${item.id}`) }}</span>
</div> </div>
</div> </div>
<div class="settings-footer"> <div class="settings-footer" v-if="enableMemoryMonitor">
<div class="memory-info-section"> <div class="memory-info-section">
<div class="section-title">{{ t('settings.systemInfo') }}</div> <div class="section-title">{{ t('settings.systemInfo') }}</div>
<MemoryMonitor /> <MemoryMonitor />

View File

@@ -133,16 +133,13 @@ const updateLocalColor = (colorKey: string, value: string) => {
const applyChanges = async () => { const applyChanges = async () => {
try { try {
if (!tempColors.value) return; if (!tempColors.value) return;
// 更新 store 中的颜色 // 更新 store 中的颜色
themeStore.updateCurrentColors(tempColors.value); themeStore.updateCurrentColors(tempColors.value);
// 保存到数据库 // 保存到数据库
await themeStore.saveCurrentTheme(); await themeStore.saveCurrentTheme();
// 刷新编辑器主题
themeStore.refreshEditorTheme();
// 清除未保存标记 // 清除未保存标记
hasUnsavedChanges.value = false; hasUnsavedChanges.value = false;
} catch (error) { } catch (error) {

View File

@@ -2,48 +2,18 @@
import {useConfigStore} from '@/stores/configStore'; import {useConfigStore} from '@/stores/configStore';
import {useBackupStore} from '@/stores/backupStore'; import {useBackupStore} from '@/stores/backupStore';
import {useI18n} from 'vue-i18n'; import {useI18n} from 'vue-i18n';
import {computed, ref, watch, onUnmounted} from 'vue'; import {computed} from 'vue';
import SettingSection from '../components/SettingSection.vue'; import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue'; import SettingItem from '../components/SettingItem.vue';
import ToggleSwitch from '../components/ToggleSwitch.vue'; import ToggleSwitch from '../components/ToggleSwitch.vue';
import {AuthMethod} from '@/../bindings/voidraft/internal/models/models'; import {AuthMethod} from '@/../bindings/voidraft/internal/models/models';
import {DialogService} from '@/../bindings/voidraft/internal/services'; import {DialogService} from '@/../bindings/voidraft/internal/services';
import toast from '@/components/toast';
const {t} = useI18n(); const {t} = useI18n();
const configStore = useConfigStore(); const configStore = useConfigStore();
const backupStore = useBackupStore(); const backupStore = useBackupStore();
// 消息显示状态
const message = ref<string | null>(null);
const isError = ref(false);
let messageTimer: ReturnType<typeof setTimeout> | null = null;
const clearMessage = () => {
if (messageTimer) {
clearTimeout(messageTimer);
messageTimer = null;
}
message.value = null;
};
// 监听同步完成,显示消息并自动消失
watch(() => backupStore.isSyncing, (syncing, wasSyncing) => {
if (wasSyncing && !syncing) {
clearMessage();
if (backupStore.error) {
message.value = backupStore.error;
isError.value = true;
messageTimer = setTimeout(clearMessage, 5000);
} else {
message.value = 'Sync successful';
isError.value = false;
messageTimer = setTimeout(clearMessage, 3000);
}
}
});
onUnmounted(clearMessage);
const authMethodOptions = computed(() => [ const authMethodOptions = computed(() => [
{value: AuthMethod.Token, label: t('settings.backup.authMethods.token')}, {value: AuthMethod.Token, label: t('settings.backup.authMethods.token')},
{value: AuthMethod.SSHKey, label: t('settings.backup.authMethods.sshKey')}, {value: AuthMethod.SSHKey, label: t('settings.backup.authMethods.sshKey')},
@@ -64,6 +34,15 @@ const selectSshKeyFile = async () => {
configStore.setSshKeyPath(selectedPath.trim()); configStore.setSshKeyPath(selectedPath.trim());
} }
}; };
const handleSync = async () => {
try {
await backupStore.sync();
toast.success('Sync successful');
} catch (e) {
toast.error(e instanceof Error ? e.message : String(e));
}
};
</script> </script>
<template> <template>
@@ -202,14 +181,10 @@ const selectSshKeyFile = async () => {
<!-- 备份操作 --> <!-- 备份操作 -->
<SettingSection :title="t('settings.backup.backupOperations')"> <SettingSection :title="t('settings.backup.backupOperations')">
<SettingItem <SettingItem :title="t('settings.backup.syncToRemote')">
:title="t('settings.backup.syncToRemote')"
:description="message || undefined"
:descriptionType="message ? (isError ? 'error' : 'success') : 'default'"
>
<button <button
class="sync-button" class="sync-button"
@click="backupStore.sync" @click="handleSync"
:disabled="!configStore.config.backup.enabled || !configStore.config.backup.repo_url || backupStore.isSyncing" :disabled="!configStore.config.backup.enabled || !configStore.config.backup.repo_url || backupStore.isSyncing"
:class="{ 'syncing': backupStore.isSyncing }" :class="{ 'syncing': backupStore.isSyncing }"
> >
@@ -222,10 +197,6 @@ const selectSshKeyFile = async () => {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.settings-page {
//max-width: 800px;
}
// 统一的输入控件样式 // 统一的输入控件样式
.repo-url-input, .repo-url-input,
.branch-input, .branch-input,

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useConfigStore } from '@/stores/configStore'; import { useConfigStore } from '@/stores/configStore';
import { useEditorStore } from '@/stores/editorStore';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import {computed, onMounted } from 'vue'; import {computed, onMounted } from 'vue';
import SettingSection from '../components/SettingSection.vue'; import SettingSection from '../components/SettingSection.vue';
@@ -9,6 +10,7 @@ import { TabType } from '@/../bindings/voidraft/internal/models/';
const { t } = useI18n(); const { t } = useI18n();
const configStore = useConfigStore(); const configStore = useConfigStore();
const editorStore = useEditorStore();
// 确保配置已加载 // 确保配置已加载
onMounted(async () => { onMounted(async () => {
@@ -27,6 +29,7 @@ const fontFamilyModel = computed({
set: async (fontFamily: string) => { set: async (fontFamily: string) => {
if (fontFamily) { if (fontFamily) {
await configStore.setFontFamily(fontFamily); await configStore.setFontFamily(fontFamily);
editorStore.applyFontSettings();
} }
} }
}); });
@@ -50,6 +53,7 @@ const fontWeightModel = computed({
set: async (value: string) => { set: async (value: string) => {
if (value) { if (value) {
await configStore.setFontWeight(value); await configStore.setFontWeight(value);
editorStore.applyFontSettings();
} }
} }
}); });
@@ -58,20 +62,24 @@ const fontWeightModel = computed({
const increaseLineHeight = async () => { const increaseLineHeight = async () => {
const newLineHeight = Math.min(3.0, configStore.config.editing.lineHeight + 0.1); const newLineHeight = Math.min(3.0, configStore.config.editing.lineHeight + 0.1);
await configStore.setLineHeight(Math.round(newLineHeight * 10) / 10); await configStore.setLineHeight(Math.round(newLineHeight * 10) / 10);
editorStore.applyFontSettings();
}; };
const decreaseLineHeight = async () => { const decreaseLineHeight = async () => {
const newLineHeight = Math.max(1.0, configStore.config.editing.lineHeight - 0.1); const newLineHeight = Math.max(1.0, configStore.config.editing.lineHeight - 0.1);
await configStore.setLineHeight(Math.round(newLineHeight * 10) / 10); await configStore.setLineHeight(Math.round(newLineHeight * 10) / 10);
editorStore.applyFontSettings();
}; };
// 字体大小控制 // 字体大小控制
const increaseFontSize = async () => { const increaseFontSize = async () => {
await configStore.increaseFontSize(); await configStore.increaseFontSize();
editorStore.applyFontSettings();
}; };
const decreaseFontSize = async () => { const decreaseFontSize = async () => {
await configStore.decreaseFontSize(); await configStore.decreaseFontSize();
editorStore.applyFontSettings();
}; };
// Tab类型切换 // Tab类型切换
@@ -84,15 +92,18 @@ const tabTypeText = computed(() => {
// Tab大小增减 // Tab大小增减
const increaseTabSize = async () => { const increaseTabSize = async () => {
await configStore.increaseTabSize(); await configStore.increaseTabSize();
editorStore.applyTabSettings();
}; };
const decreaseTabSize = async () => { const decreaseTabSize = async () => {
await configStore.decreaseTabSize(); await configStore.decreaseTabSize();
editorStore.applyTabSettings();
}; };
// Tab相关操作 // Tab相关操作
const handleToggleTabType = async () => { const handleToggleTabType = async () => {
await configStore.toggleTabType(); await configStore.toggleTabType();
editorStore.applyTabSettings();
}; };
// 创建双向绑定的计算属性 // 创建双向绑定的计算属性
@@ -100,6 +111,7 @@ const enableTabIndent = computed({
get: () => configStore.config.editing.enableTabIndent, get: () => configStore.config.editing.enableTabIndent,
set: async (value: boolean) => { set: async (value: boolean) => {
await configStore.setEnableTabIndent(value); await configStore.setEnableTabIndent(value);
editorStore.applyTabSettings();
} }
}); });
@@ -187,13 +199,13 @@ const handleAutoSaveDelayChange = async (event: Event) => {
<button <button
@click="decreaseTabSize" @click="decreaseTabSize"
class="control-button" class="control-button"
:disabled="!enableTabIndent || configStore.config.editing.tabSize <= configStore.tabSize.min" :disabled="!enableTabIndent || configStore.config.editing.tabSize <= 2"
>-</button> >-</button>
<span>{{ configStore.config.editing.tabSize }}</span> <span>{{ configStore.config.editing.tabSize }}</span>
<button <button
@click="increaseTabSize" @click="increaseTabSize"
class="control-button" class="control-button"
:disabled="!enableTabIndent || configStore.config.editing.tabSize >= configStore.tabSize.max" :disabled="!enableTabIndent || configStore.config.editing.tabSize >= 8"
>+</button> >+</button>
</div> </div>
</SettingItem> </SettingItem>

View File

@@ -3,6 +3,7 @@ import {computed, onMounted, ref} from 'vue';
import {useI18n} from 'vue-i18n'; import {useI18n} from 'vue-i18n';
import {useEditorStore} from '@/stores/editorStore'; import {useEditorStore} from '@/stores/editorStore';
import {useExtensionStore} from '@/stores/extensionStore'; import {useExtensionStore} from '@/stores/extensionStore';
import {useKeybindingStore} from '@/stores/keybindingStore';
import {ExtensionService} from '@/../bindings/voidraft/internal/services'; import {ExtensionService} from '@/../bindings/voidraft/internal/services';
import { import {
getExtensionDefaultConfig, getExtensionDefaultConfig,
@@ -10,13 +11,16 @@ import {
getExtensionDisplayName, getExtensionsMap, getExtensionDisplayName, getExtensionsMap,
hasExtensionConfig hasExtensionConfig
} from '@/views/editor/manager/extensions'; } from '@/views/editor/manager/extensions';
import {getExtensionManager} from '@/views/editor/manager';
import SettingSection from '../components/SettingSection.vue'; import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue'; import AccordionContainer from '@/components/accordion/AccordionContainer.vue';
import AccordionItem from '@/components/accordion/AccordionItem.vue';
import ToggleSwitch from '../components/ToggleSwitch.vue'; import ToggleSwitch from '../components/ToggleSwitch.vue';
const {t} = useI18n(); const {t} = useI18n();
const editorStore = useEditorStore(); const editorStore = useEditorStore();
const extensionStore = useExtensionStore(); const extensionStore = useExtensionStore();
const keybindingStore = useKeybindingStore();
// 页面初始化时加载扩展数据 // 页面初始化时加载扩展数据
onMounted(async () => { onMounted(async () => {
@@ -24,11 +28,11 @@ onMounted(async () => {
}); });
// 展开状态管理 // 展开状态管理
const expandedExtensions = ref<Set<number>>(new Set()); const expandedExtensions = ref<number[]>([]);
// 获取所有可用的扩展 // 获取所有可用的扩展
const availableExtensions = computed(() => { const availableExtensions = computed(() => {
return getExtensionsMap().map(name => { const extensions = getExtensionsMap().map(name => {
const extension = extensionStore.extensions.find(ext => ext.name === name); const extension = extensionStore.extensions.find(ext => ext.name === name);
return { return {
id: extension?.id ?? 0, id: extension?.id ?? 0,
@@ -41,21 +45,37 @@ const availableExtensions = computed(() => {
defaultConfig: getExtensionDefaultConfig(name) defaultConfig: getExtensionDefaultConfig(name)
}; };
}); });
console.log('Available Extensions:', extensions);
return extensions;
}); });
// 切换展开状态 // 获取扩展图标路径(直接使用扩展名称作为文件名)
const toggleExpanded = (extensionId: number) => { const getExtensionIcon = (name: string): string => {
if (expandedExtensions.value.has(extensionId)) { return `/images/${name}.svg`;
expandedExtensions.value.delete(extensionId);
} else {
expandedExtensions.value.add(extensionId);
}
}; };
// 更新扩展状态 // 更新扩展状态
const updateExtension = async (extensionId: number, enabled: boolean) => { const updateExtension = async (extensionId: number, enabled: boolean) => {
try { try {
await editorStore.updateExtension(extensionId, enabled); // 更新后端
await ExtensionService.UpdateExtensionEnabled(extensionId, enabled);
// 重新加载各个 Store 的状态
await extensionStore.loadExtensions();
await keybindingStore.loadKeyBindings();
// 获取更新后的扩展
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
if (!extension) return;
// 应用到编辑器
const manager = getExtensionManager();
if (manager) {
manager.updateExtension(extension.name, enabled, extension.config);
}
// 更新快捷键
await editorStore.applyKeymapSettings();
} catch (error) { } catch (error) {
console.error('Failed to update extension:', error); console.error('Failed to update extension:', error);
} }
@@ -75,9 +95,18 @@ const updateExtensionConfig = async (extensionId: number, configKey: string, val
} else { } else {
updatedConfig[configKey] = value; updatedConfig[configKey] = value;
} }
// 使用editorStore的updateExtension方法更新确保应用到所有编辑器实例
await editorStore.updateExtension(extensionId, extension.enabled ?? false, updatedConfig); // 更新后端配置
await ExtensionService.UpdateExtensionConfig(extensionId, updatedConfig);
// 重新加载状态
await extensionStore.loadExtensions();
// 应用到编辑器
const manager = getExtensionManager();
if (manager) {
manager.updateExtension(extension.name, extension.enabled ?? false, updatedConfig);
}
} catch (error) { } catch (error) {
console.error('Failed to update extension config:', error); console.error('Failed to update extension config:', error);
} }
@@ -89,14 +118,16 @@ const resetExtension = async (extensionId: number) => {
// 重置到默认配置 // 重置到默认配置
await ExtensionService.ResetExtensionConfig(extensionId); await ExtensionService.ResetExtensionConfig(extensionId);
// 重新加载扩展状态以获取最新配置 // 重新加载扩展状态
await extensionStore.loadExtensions(); await extensionStore.loadExtensions();
// 获取重置后的状态,立即应用到所有编辑器视图 // 获取重置后的状态,应用到编辑器
const extension = extensionStore.extensions.find(ext => ext.id === extensionId); const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
if (extension) { if (extension) {
// 通过editorStore更新确保所有视图都能同步 const manager = getExtensionManager();
await editorStore.updateExtension(extensionId, extension.enabled ?? false, extension.config); if (manager) {
manager.updateExtension(extension.name, extension.enabled ?? false, extension.config);
}
} }
} catch (error) { } catch (error) {
console.error('Failed to reset extension:', error); console.error('Failed to reset extension:', error);
@@ -161,152 +192,227 @@ const handleConfigInput = async (
<template> <template>
<div class="settings-page"> <div class="settings-page">
<SettingSection :title="t('settings.extensions')"> <SettingSection :title="t('settings.extensions')">
<div <!-- 空状态提示 -->
v-for="extension in availableExtensions" <div v-if="availableExtensions.length === 0" class="empty-state">
:key="extension.name" <p>{{ t('settings.extensionsPage.loading') }}</p>
class="extension-item"
>
<!-- 扩展主项 -->
<SettingItem
:title="extension.displayName"
:description="extension.description"
>
<div class="extension-controls">
<button
v-if="extension.hasConfig"
class="config-button"
@click="toggleExpanded(extension.id)"
:class="{ expanded: expandedExtensions.has(extension.id) }"
:title="t('settings.extensionsPage.configuration')"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path
d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</button>
<div v-else class="config-placeholder"></div>
<ToggleSwitch
:model-value="extension.enabled"
@update:model-value="updateExtension(extension.id, $event)"
/>
</div>
</SettingItem>
<!-- 可展开的配置区域 -->
<div
v-if="extension.hasConfig && expandedExtensions.has(extension.id)"
class="extension-config"
>
<!-- 配置项标题和重置按钮 -->
<div class="config-header">
<h4 class="config-title">{{ t('settings.extensionsPage.configuration') }}</h4>
<button
class="reset-button"
@click="resetExtension(extension.id)"
:title="t('settings.extensionsPage.resetToDefault')"
>
{{ t('settings.reset') }}
</button>
</div>
<div class="config-table-wrapper">
<table class="config-table">
<tbody>
<tr
v-for="[configKey, configValue] in Object.entries(extension.defaultConfig)"
:key="configKey"
>
<th scope="row" class="config-table-key">
{{ configKey }}
</th>
<td class="config-table-value">
<input
class="config-value-input"
type="text"
:value="formatConfigValue(getConfigValue(extension.config, configKey, configValue))"
@change="handleConfigInput(extension.id, configKey, configValue, $event)"
@keyup.enter.prevent="handleConfigInput(extension.id, configKey, configValue, $event)"
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div> </div>
<!-- 扩展列表 -->
<AccordionContainer v-else v-model="expandedExtensions" :multiple="false">
<AccordionItem
v-for="extension in availableExtensions"
:key="extension.id"
:id="extension.id"
:class="{ 'extension-disabled': !extension.enabled }"
>
<!-- 标题插槽显示图标和扩展名称 -->
<template #title>
<div class="extension-header">
<div class="extension-icon-wrapper">
<div class="extension-icon-placeholder" :class="{ 'disabled': !extension.enabled }">
<!-- 直接使用扩展名称作为图标文件名 -->
<img
:src="getExtensionIcon(extension.name)"
:alt="extension.displayName"
class="extension-icon-img"
/>
</div>
</div>
<div class="extension-info">
<div class="extension-name">{{ extension.displayName }}</div>
<div class="extension-description">{{ extension.description }}</div>
</div>
</div>
</template>
<!-- 默认插槽显示开关和配置项 -->
<div class="extension-content">
<!-- 启用开关 -->
<div class="extension-toggle-section">
<label class="toggle-label">{{ t('settings.extensionsPage.enabled') }}</label>
<ToggleSwitch
:model-value="extension.enabled"
@update:model-value="updateExtension(extension.id, $event)"
/>
</div>
<!-- 配置项 -->
<div v-if="extension.hasConfig" class="extension-config-section">
<div class="config-header">
<h4 class="config-title">{{ t('settings.extensionsPage.configuration') }}</h4>
<button
class="reset-button"
@click="resetExtension(extension.id)"
:title="t('settings.extensionsPage.resetToDefault')"
>
{{ t('settings.reset') }}
</button>
</div>
<div class="config-table-wrapper">
<table class="config-table">
<tbody>
<tr
v-for="[configKey, configValue] in Object.entries(extension.defaultConfig)"
:key="configKey"
>
<th scope="row" class="config-table-key">
{{ configKey }}
</th>
<td class="config-table-value">
<input
class="config-value-input"
type="text"
:value="formatConfigValue(getConfigValue(extension.config, configKey, configValue))"
@change="handleConfigInput(extension.id, configKey, configValue, $event)"
@keyup.enter.prevent="handleConfigInput(extension.id, configKey, configValue, $event)"
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</AccordionItem>
</AccordionContainer>
</SettingSection> </SettingSection>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.extension-item { .empty-state {
border-bottom: 1px solid var(--settings-input-border); padding: 40px 20px;
text-align: center;
color: var(--settings-text-secondary);
font-size: 14px;
}
&:last-child { // 禁用状态的扩展项
border-bottom: none; :deep(.extension-disabled) {
background-color: rgba(0, 0, 0, 0.02);
.accordion-header {
opacity: 0.7;
&:hover {
background-color: rgba(0, 0, 0, 0.03);
opacity: 0.8;
}
}
&.is-expanded {
background-color: rgba(0, 0, 0, 0.03);
.accordion-header {
opacity: 0.8;
}
}
.extension-description {
opacity: 0.7;
} }
} }
.extension-controls { .extension-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
min-width: 140px; width: 100%;
justify-content: flex-end;
} }
.config-button { .extension-icon-wrapper {
padding: 4px;
border: none;
background: none;
color: var(--settings-text-secondary);
cursor: pointer;
transition: all 0.2s ease;
border-radius: 4px;
flex-shrink: 0; flex-shrink: 0;
width: 24px; }
height: 24px;
.extension-icon-placeholder {
width: 40px;
height: 40px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 8px;
&:hover { background: linear-gradient(135deg, rgba(var(--settings-accent-rgb, 74, 158, 255), 0.12), rgba(var(--settings-accent-rgb, 74, 158, 255), 0.06));
background-color: var(--settings-hover); border: 1px solid rgba(var(--settings-accent-rgb, 74, 158, 255), 0.15);
color: var(--settings-text); color: white;
} transition: all 0.2s ease;
position: relative;
&.expanded { overflow: hidden;
color: var(--settings-accent); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
background-color: var(--settings-hover);
} &.disabled {
background: linear-gradient(135deg, rgba(136, 136, 136, 0.08), rgba(136, 136, 136, 0.04));
svg { border-color: rgba(136, 136, 136, 0.1);
transition: all 0.2s ease; box-shadow: none;
.extension-icon-img {
opacity: 0.4;
filter: grayscale(1);
}
} }
} }
.config-placeholder { .extension-icon-img {
width: 24px; width: 28px;
height: 24px; height: 28px;
flex-shrink: 0; object-fit: contain;
transition: all 0.2s ease;
} }
.extension-config { .extension-info {
background-color: var(--settings-input-bg); flex: 1;
border-left: 2px solid var(--settings-accent); min-width: 0;
margin: 4px 0 12px 0; }
padding: 8px 10px;
border-radius: 2px; .extension-name {
font-size: 14px;
font-weight: 500;
color: var(--settings-text);
margin-bottom: 2px;
}
.extension-description {
font-size: 12px; font-size: 12px;
color: var(--settings-text-secondary);
line-height: 1.5;
word-wrap: break-word;
word-break: break-word;
}
.extension-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.extension-toggle-section {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background-color: var(--settings-input-bg);
border-radius: 4px;
border: 1px solid var(--settings-input-border);
}
.toggle-label {
font-size: 13px;
font-weight: 500;
color: var(--settings-text);
margin: 0;
}
.extension-config-section {
display: flex;
flex-direction: column;
gap: 8px;
} }
.config-header { .config-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 6px; margin-bottom: 4px;
} }
.config-title { .config-title {
@@ -319,10 +425,10 @@ const handleConfigInput = async (
} }
.reset-button { .reset-button {
padding: 3px 8px; padding: 4px 10px;
font-size: 12px; font-size: 12px;
border: 1px solid var(--settings-input-border); border: 1px solid var(--settings-input-border);
border-radius: 2px; border-radius: 3px;
background-color: transparent; background-color: transparent;
color: var(--settings-text-secondary); color: var(--settings-text-secondary);
cursor: pointer; cursor: pointer;
@@ -338,7 +444,7 @@ const handleConfigInput = async (
.config-table-wrapper { .config-table-wrapper {
border: 1px solid var(--settings-input-border); border: 1px solid var(--settings-input-border);
border-radius: 2px; border-radius: 4px;
overflow: hidden; overflow: hidden;
background-color: var(--settings-panel, var(--settings-input-bg)); background-color: var(--settings-panel, var(--settings-input-bg));
} }
@@ -346,7 +452,7 @@ const handleConfigInput = async (
.config-table { .config-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-size: 11px; font-size: 12px;
} }
.config-table tr + tr { .config-table tr + tr {
@@ -355,7 +461,7 @@ const handleConfigInput = async (
.config-table th, .config-table th,
.config-table td { .config-table td {
padding: 5px 8px; padding: 6px 10px;
vertical-align: middle; vertical-align: middle;
} }
@@ -367,36 +473,36 @@ const handleConfigInput = async (
border-right: 1px solid var(--settings-input-border); border-right: 1px solid var(--settings-input-border);
background-color: rgba(0, 0, 0, 0.05); background-color: rgba(0, 0, 0, 0.05);
font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, monospace; font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, monospace;
font-size: 10px; font-size: 12px;
} }
.config-table-value { .config-table-value {
padding: 3px 4px; padding: 4px 6px;
} }
.config-value-input { .config-value-input {
width: 100%; width: 100%;
padding: 4px 6px; padding: 5px 8px;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 2px; border-radius: 3px;
background: transparent; background: transparent;
color: var(--settings-text); color: var(--settings-text);
font-size: 11px; font-size: 12px;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, monospace; font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, monospace;
line-height: 1.3; line-height: 1.4;
box-sizing: border-box; box-sizing: border-box;
transition: border-color 0.15s ease, background-color 0.15s ease; transition: border-color 0.15s ease, background-color 0.15s ease;
}
.config-value-input:hover { &:hover {
border-color: var(--settings-input-border); border-color: var(--settings-input-border);
background-color: var(--settings-hover); background-color: var(--settings-hover);
} }
.config-value-input:focus { &:focus {
outline: none; outline: none;
border-color: var(--settings-accent); border-color: var(--settings-accent);
background-color: var(--settings-input-bg); background-color: var(--settings-input-bg);
}
} }
</style> </style>

View File

@@ -9,6 +9,7 @@ import ToggleSwitch from '../components/ToggleSwitch.vue';
import {DialogService, HotkeyService, MigrationService} from '@/../bindings/voidraft/internal/services'; import {DialogService, HotkeyService, MigrationService} from '@/../bindings/voidraft/internal/services';
import {useSystemStore} from "@/stores/systemStore"; import {useSystemStore} from "@/stores/systemStore";
import {useConfirm, usePolling} from '@/composables'; import {useConfirm, usePolling} from '@/composables';
import toast from '@/components/toast';
const {t} = useI18n(); const {t} = useI18n();
const { const {
@@ -18,6 +19,7 @@ const {
setDataPath, setDataPath,
setEnableGlobalHotkey, setEnableGlobalHotkey,
setEnableLoadingAnimation, setEnableLoadingAnimation,
setEnableMemoryMonitor,
setEnableSystemTray, setEnableSystemTray,
setEnableTabs, setEnableTabs,
setEnableWindowSnap, setEnableWindowSnap,
@@ -29,7 +31,6 @@ const tabStore = useTabStore();
// 进度条显示控制 // 进度条显示控制
const showBar = ref(false); const showBar = ref(false);
const manualError = ref(''); // 用于捕获 MigrateDirectory 抛出的错误
let hideTimer = 0; let hideTimer = 0;
// 轮询迁移进度 // 轮询迁移进度
@@ -39,15 +40,20 @@ const {data: progress, error: pollError, isActive: migrating, start, stop, reset
interval: 300, interval: 300,
shouldStop: ({progress, error}) => !!error || progress >= 100, shouldStop: ({progress, error}) => !!error || progress >= 100,
onStop: () => { onStop: () => {
const hasError = pollError.value || progress.value?.error; const error = pollError.value || progress.value?.error;
hideTimer = window.setTimeout(hideAll, hasError ? 5000 : 3000); if (error) {
toast.error(error);
} else if ((progress.value?.progress ?? 0) >= 100) {
toast.success('Migration successful');
}
hideTimer = window.setTimeout(hideAll, 3000);
} }
} }
); );
// 派生状态 // 派生状态
const migrationError = computed(() => manualError.value || pollError.value || progress.value?.error || '');
const currentProgress = computed(() => progress.value?.progress ?? 0); const currentProgress = computed(() => progress.value?.progress ?? 0);
const migrationError = computed(() => pollError.value || progress.value?.error || '');
const barClass = computed(() => { const barClass = computed(() => {
if (!showBar.value) return ''; if (!showBar.value) return '';
@@ -64,8 +70,7 @@ const hideAll = () => {
clearTimeout(hideTimer); clearTimeout(hideTimer);
hideTimer = 0; hideTimer = 0;
showBar.value = false; showBar.value = false;
manualError.value = ''; reset();
reset(); // 清除轮询状态
}; };
// 重置设置确认 // 重置设置确认
@@ -125,7 +130,7 @@ const enableTabs = computed({
await setEnableTabs(value); await setEnableTabs(value);
if (value) { if (value) {
// 开启tabs功能时初始化当前文档到标签页 // 开启tabs功能时初始化当前文档到标签页
tabStore.initializeTab(); tabStore.initTab();
} else { } else {
// 关闭tabs功能时清空所有标签页 // 关闭tabs功能时清空所有标签页
tabStore.clearAllTabs(); tabStore.clearAllTabs();
@@ -133,6 +138,12 @@ const enableTabs = computed({
} }
}); });
// 计算属性 - 启用内存监视器
const enableMemoryMonitor = computed({
get: () => general.enableMemoryMonitor,
set: (value: boolean) => setEnableMemoryMonitor(value)
});
// 计算属性 - 开机启动 // 计算属性 - 开机启动
const startAtLogin = computed({ const startAtLogin = computed({
get: () => general.startAtLogin, get: () => general.startAtLogin,
@@ -193,10 +204,8 @@ const selectDataDirectory = async () => {
const [oldPath, newPath] = [currentDataPath.value, selectedPath.trim()]; const [oldPath, newPath] = [currentDataPath.value, selectedPath.trim()];
// 清除之前的状态并开始轮询
hideAll(); hideAll();
showBar.value = true; showBar.value = true;
manualError.value = '';
start(); start();
try { try {
@@ -204,10 +213,9 @@ const selectDataDirectory = async () => {
await setDataPath(newPath); await setDataPath(newPath);
} catch (e) { } catch (e) {
stop(); stop();
// 设置手动捕获的错误(当轮询还没获取到错误时) toast.error(String(e).replace(/^Error:\s*/i, '') || 'Migration failed');
manualError.value = String(e).replace(/^Error:\s*/i, '') || 'Migration failed';
showBar.value = true; showBar.value = true;
hideTimer = window.setTimeout(hideAll, 5000); hideTimer = window.setTimeout(hideAll, 3000);
} }
}; };
</script> </script>
@@ -272,6 +280,9 @@ const selectDataDirectory = async () => {
<SettingItem :title="t('settings.enableTabs')"> <SettingItem :title="t('settings.enableTabs')">
<ToggleSwitch v-model="enableTabs"/> <ToggleSwitch v-model="enableTabs"/>
</SettingItem> </SettingItem>
<SettingItem :title="t('settings.enableMemoryMonitor')">
<ToggleSwitch v-model="enableMemoryMonitor"/>
</SettingItem>
</SettingSection> </SettingSection>
<SettingSection :title="t('settings.startup')"> <SettingSection :title="t('settings.startup')">
@@ -300,11 +311,6 @@ const selectDataDirectory = async () => {
<!-- 进度条 --> <!-- 进度条 -->
<div class="progress-bar" :class="[{'active': showBar}, barClass]" :style="{width: barWidth}"/> <div class="progress-bar" :class="[{'active': showBar}, barClass]" :style="{width: barWidth}"/>
</div> </div>
<!-- 错误提示 -->
<Transition name="error-fade">
<div v-if="migrationError" class="progress-error">{{ migrationError }}</div>
</Transition>
</div> </div>
</div> </div>
</SettingSection> </SettingSection>
@@ -537,13 +543,6 @@ const selectDataDirectory = async () => {
} }
} }
} }
.progress-error {
font-size: 12px;
color: #ef4444;
opacity: 1;
transition: all 0.3s ease;
}
} }
.reset-button { .reset-button {
@@ -602,35 +601,4 @@ const selectDataDirectory = async () => {
box-shadow: 0 0 0 0 rgba(255, 71, 87, 0); box-shadow: 0 0 0 0 rgba(255, 71, 87, 0);
} }
} }
// 消息点脉冲动画
@keyframes pulse-dot {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(1.2);
}
}
// 错误提示动画
.error-fade-enter-active {
transition: all 0.3s ease;
}
.error-fade-leave-active {
transition: all 0.3s ease;
}
.error-fade-enter-from {
opacity: 0;
transform: translateY(-4px);
}
.error-fade-leave-to {
opacity: 0;
transform: translateY(-4px);
}
</style> </style>

View File

@@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { onMounted, computed, ref, onUnmounted, watch } from 'vue'; import { onMounted, computed, ref, nextTick } from 'vue';
import SettingSection from '../components/SettingSection.vue'; import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue'; import SettingItem from '../components/SettingItem.vue';
import { AccordionContainer, AccordionItem } from '@/components/accordion';
import { useKeybindingStore } from '@/stores/keybindingStore'; import { useKeybindingStore } from '@/stores/keybindingStore';
import { useSystemStore } from '@/stores/systemStore'; import { useSystemStore } from '@/stores/systemStore';
import { useConfigStore } from '@/stores/configStore'; import { useConfigStore } from '@/stores/configStore';
@@ -11,6 +12,7 @@ import { getCommandDescription } from '@/views/editor/keymap/commands';
import { KeyBindingType } from '@/../bindings/voidraft/internal/models/models'; import { KeyBindingType } from '@/../bindings/voidraft/internal/models/models';
import { KeyBindingService } from '@/../bindings/voidraft/internal/services'; import { KeyBindingService } from '@/../bindings/voidraft/internal/services';
import { useConfirm } from '@/composables/useConfirm'; import { useConfirm } from '@/composables/useConfirm';
import toast from '@/components/toast';
const { t } = useI18n(); const { t } = useI18n();
const keybindingStore = useKeybindingStore(); const keybindingStore = useKeybindingStore();
@@ -20,16 +22,26 @@ const editorStore = useEditorStore();
interface EditingState { interface EditingState {
id: number; id: number;
name: string;
originalKey: string;
} }
const editingBinding = ref<EditingState | null>(null); const editingBinding = ref<EditingState | null>(null);
const capturedKey = ref(''); const inputKey = ref('');
const capturedKeyDisplay = ref<string[]>([]);
const isConflict = ref(false);
const isEditing = computed(() => !!editingBinding.value); // 将快捷键字符串拆分为独立的键
const splitKeys = (keyStr: string): string[] => {
if (!keyStr) return [];
return keyStr.split(/[-+]/).filter(Boolean);
};
// 动态设置 ref 并自动聚焦
const setInputRef = (el: any) => {
if (el && el instanceof HTMLInputElement) {
// 使用 nextTick 确保 DOM 完全渲染后再聚焦
nextTick(() => {
el.focus();
});
}
};
onMounted(async () => { onMounted(async () => {
await keybindingStore.loadKeyBindings(); await keybindingStore.loadKeyBindings();
@@ -63,7 +75,10 @@ const keyBindings = computed(() =>
command: getDisplayKeybinding(kb), command: getDisplayKeybinding(kb),
rawKey: getRawKey(kb), rawKey: getRawKey(kb),
extension: kb.extension || '', extension: kb.extension || '',
description: getCommandDescription(kb.name) || kb.name || '' description: getCommandDescription(kb.name) || kb.name || '',
enabled: kb.enabled,
preventDefault: kb.preventDefault,
originalData: kb
})) }))
); );
@@ -82,6 +97,8 @@ const getDisplayKeybinding = (kb: any): string[] => {
}; };
const parseKeyString = (keyStr: string): string[] => { const parseKeyString = (keyStr: string): string[] => {
if (!keyStr) return [];
const symbolMap: Record<string, string> = { const symbolMap: Record<string, string> = {
'Mod': systemStore.isMacOS ? '⌘' : 'Ctrl', 'Mod': systemStore.isMacOS ? '⌘' : 'Ctrl',
'Cmd': '⌘', 'Cmd': '⌘',
@@ -102,128 +119,141 @@ const parseKeyString = (keyStr: string): string[] => {
.filter(Boolean); .filter(Boolean);
}; };
// 切换启用状态
// 键盘事件捕获 const toggleEnabled = async (binding: any) => {
const SPECIAL_KEYS: Record<string, string> = { try {
' ': 'Space', await KeyBindingService.UpdateKeyBindingEnabled(binding.id, !binding.enabled);
'ArrowUp': 'ArrowUp', await keybindingStore.loadKeyBindings();
'ArrowDown': 'ArrowDown', await editorStore.applyKeymapSettings();
'ArrowLeft': 'ArrowLeft', } catch (error) {
'ArrowRight': 'ArrowRight', console.error('Failed to update enabled status:', error);
'Enter': 'Enter', }
'Tab': 'Tab',
'Backspace': 'Backspace',
'Delete': 'Delete',
'Home': 'Home',
'End': 'End',
'PageUp': 'PageUp',
'PageDown': 'PageDown',
}; };
const MODIFIER_KEYS = ['Control', 'Alt', 'Shift', 'Meta']; // 切换 PreventDefault
const MAX_KEY_PARTS = 3; // 最多3个键 const togglePreventDefault = async (binding: any) => {
try {
const captureKeyBinding = (event: KeyboardEvent): string | null => { await KeyBindingService.UpdateKeyBindingPreventDefault(binding.id, !binding.preventDefault);
// 忽略单独的修饰键 await keybindingStore.loadKeyBindings();
if (MODIFIER_KEYS.includes(event.key)) return null; await editorStore.applyKeymapSettings();
} catch (error) {
const parts: string[] = []; console.error('Failed to update preventDefault:', error);
}
// 添加修饰键
if (event.ctrlKey || event.metaKey) parts.push('Mod');
if (event.altKey) parts.push('Alt');
if (event.shiftKey) parts.push('Shift');
// 获取主键
const mainKey = SPECIAL_KEYS[event.key] ??
(event.key.length === 1 ? event.key.toLowerCase() : event.key);
if (mainKey) parts.push(mainKey);
// 限制最多3个键
if (parts.length > MAX_KEY_PARTS) return null;
return parts.join('-');
}; };
// 开始添加快捷键
const startAddKey = (bindingId: number) => {
editingBinding.value = {
id: bindingId
};
inputKey.value = '';
};
// 取消编辑
const cancelEdit = () => { const cancelEdit = () => {
window.removeEventListener('keydown', handleKeyCapture, true);
editingBinding.value = null; editingBinding.value = null;
capturedKey.value = ''; inputKey.value = '';
capturedKeyDisplay.value = [];
isConflict.value = false;
}; };
const handleKeyCapture = (event: KeyboardEvent) => { // 验证快捷键格式
if (!isEditing.value) return; const validateKeyFormat = (key: string): boolean => {
if (!key || key.trim() === '') return false;
event.preventDefault(); // 基本格式验证:允许 Mod/Ctrl/Alt/Shift + 其他键
event.stopPropagation(); const validPattern = /^(Mod|Ctrl|Alt|Shift|Cmd)(-[A-Za-z0-9\[\]\\/;',.\-=`]|-(ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Enter|Tab|Backspace|Delete|Home|End|PageUp|PageDown|Space|Escape))+$/;
const simpleKeyPattern = /^[A-Za-z0-9]$/;
const specialKeyPattern = /^(ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Enter|Tab|Backspace|Delete|Home|End|PageUp|PageDown|Space|Escape)$/;
// ESC 取消编辑 return validPattern.test(key) || simpleKeyPattern.test(key) || specialKeyPattern.test(key);
if (event.key === 'Escape') { };
cancelEdit();
// 检查快捷键冲突
const checkConflict = (newKey: string, currentBindingId: number): { conflict: boolean; conflictWith?: string } => {
const conflictBinding = keyBindings.value.find(kb =>
kb.rawKey === newKey && kb.id !== currentBindingId
);
if (conflictBinding) {
return {
conflict: true,
conflictWith: conflictBinding.description
};
}
return { conflict: false };
};
// 添加新键到快捷键
const addKeyPart = async () => {
if (!editingBinding.value || !inputKey.value.trim()) {
return; return;
} }
const key = captureKeyBinding(event); const newPart = inputKey.value.trim();
if (key) { const binding = keyBindings.value.find(kb => kb.id === editingBinding.value!.id);
capturedKey.value = key; if (!binding) return;
capturedKeyDisplay.value = parseKeyString(key);
isConflict.value = false;
}
};
const startEditBinding = (binding: any) => {
editingBinding.value = {
id: binding.id,
name: binding.name,
originalKey: binding.rawKey
};
capturedKey.value = '';
capturedKeyDisplay.value = [];
isConflict.value = false;
// 手动添加键盘监听 // 检查键数量限制最多4个
window.addEventListener('keydown', handleKeyCapture, true); const currentParts = splitKeys(binding.rawKey);
}; if (currentParts.length >= 4) {
toast.error(t('keybindings.maxKeysReached'));
const checkConflict = (newKey: string): boolean => inputKey.value = '';
keyBindings.value.some(kb => return;
kb.rawKey === newKey && kb.name !== editingBinding.value?.name }
);
// 获取现有的键
const confirmKeybinding = async () => { const currentKey = binding.rawKey;
if (!editingBinding.value || !capturedKey.value) return; const newKey = currentKey ? `${currentKey}-${newPart}` : newPart;
// 验证格式
if (!validateKeyFormat(newKey)) {
toast.error(t('keybindings.invalidFormat'));
inputKey.value = '';
return;
}
// 检查冲突 // 检查冲突
if (checkConflict(capturedKey.value)) { const conflictCheck = checkConflict(newKey, editingBinding.value.id);
isConflict.value = true; if (conflictCheck.conflict) {
setTimeout(cancelEdit, 600); toast.error(t('keybindings.conflict', { command: conflictCheck.conflictWith }));
inputKey.value = '';
return; return;
} }
try { try {
await keybindingStore.updateKeyBinding( await keybindingStore.updateKeyBinding(editingBinding.value.id, newKey);
editingBinding.value.id,
capturedKey.value
);
await editorStore.applyKeymapSettings(); await editorStore.applyKeymapSettings();
inputKey.value = '';
} catch (error) { } catch (error) {
console.error(error); console.error('Failed to add key part:', error);
} finally {
cancelEdit();
} }
}; };
// 删除快捷键的某个部分
const removeKeyPart = async (bindingId: number, index: number) => {
const binding = keyBindings.value.find(kb => kb.id === bindingId);
if (!binding) return;
const parts = splitKeys(binding.rawKey);
parts.splice(index, 1);
const newKey = parts.join('-');
try {
await keybindingStore.updateKeyBinding(bindingId, newKey);
await editorStore.applyKeymapSettings();
} catch (error) {
console.error('Failed to remove key part:', error);
}
};
</script> </script>
<template> <template>
<div class="settings-page"> <div class="settings-page">
<!-- 快捷键模式设置 --> <!-- 快捷键模式设置 -->
<SettingSection :title="t('keybindings.keymapMode')"> <SettingSection :title="t('keybindings.keymapMode')">
<SettingItem <SettingItem :title="t('keybindings.keymapMode')">
:title="t('keybindings.keymapMode')">
<select <select
:value="configStore.config.editing.keymapMode" :value="configStore.config.editing.keymapMode"
@change="updateKeymapMode(($event.target as HTMLSelectElement).value as KeyBindingType)" @change="updateKeymapMode(($event.target as HTMLSelectElement).value as KeyBindingType)"
@@ -251,68 +281,111 @@ const confirmKeybinding = async () => {
</button> </button>
</template> </template>
<div class="key-bindings-container"> <AccordionContainer :multiple="false">
<div class="key-bindings-header"> <AccordionItem
<div class="keybinding-col">{{ t('keybindings.headers.shortcut') }}</div>
<div class="extension-col">{{ t('keybindings.headers.extension') }}</div>
<div class="description-col">{{ t('keybindings.headers.description') }}</div>
</div>
<div
v-for="binding in keyBindings" v-for="binding in keyBindings"
:key="binding.name" :key="binding.id"
class="key-binding-row" :id="binding.id!"
> >
<!-- 快捷键列 --> <!-- 标题插槽 -->
<div <template #title>
class="keybinding-col" <div class="binding-title" :class="{ 'disabled': !binding.enabled }">
:class="{ 'editing': editingBinding?.name === binding.name }" <div class="binding-name">
@click.stop="editingBinding?.name !== binding.name && startEditBinding(binding)" <span class="binding-description">{{ binding.description }}</span>
> <span class="binding-extension">{{ binding.extension }}</span>
<!-- 编辑模式 --> </div>
<template v-if="editingBinding?.name === binding.name"> <div class="binding-keys">
<template v-if="!capturedKey">
<span class="key-badge waiting">waiting...</span>
</template>
<template v-else>
<span <span
v-for="(key, index) in capturedKeyDisplay" v-for="(key, index) in binding.command"
:key="index" :key="index"
class="key-badge captured" class="key-badge"
:class="{ 'conflict': isConflict }"
> >
{{ key }} {{ key }}
</span> </span>
</template> <span v-if="!binding.command.length" class="key-badge-empty">-</span>
<button </div>
@click.stop="confirmKeybinding" </div>
class="btn-mini btn-confirm" </template>
:disabled="!capturedKey"
title="Ok" <!-- 展开内容 -->
></button> <div class="binding-config">
<button <!-- Enabled 配置 -->
@click.stop="cancelEdit" <div class="config-row">
class="btn-mini btn-cancel" <span class="config-label">{{ t('keybindings.config.enabled') }}</span>
title="Cancel" <label class="switch">
></button> <input
</template> type="checkbox"
:checked="binding.enabled"
<!-- 显示模式 --> @change="toggleEnabled(binding)"
<template v-else> >
<span <span class="slider"></span>
v-for="(key, index) in binding.command" </label>
:key="index" </div>
class="key-badge"
> <!-- PreventDefault 配置 -->
{{ key }} <div class="config-row">
</span> <span class="config-label">{{ t('keybindings.config.preventDefault') }}</span>
</template> <label class="switch">
<input
type="checkbox"
:checked="binding.preventDefault"
@change="togglePreventDefault(binding)"
>
<span class="slider"></span>
</label>
</div>
<!-- Key 配置 -->
<div class="config-row">
<span class="config-label">{{ t('keybindings.config.keybinding') }}</span>
<div class="key-input-wrapper">
<div class="key-tags">
<!-- 显示现有快捷键的每个部分 -->
<template v-if="binding.rawKey">
<span
v-for="(keyPart, index) in splitKeys(binding.rawKey)"
:key="index"
class="key-tag"
>
<span class="key-tag-text">{{ keyPart }}</span>
<button
class="key-tag-remove"
@click="removeKeyPart(binding.id!, index)"
>×</button>
</span>
</template>
<!-- 添加输入框 -->
<template v-if="editingBinding?.id === binding.id">
<input
:ref="setInputRef"
v-model="inputKey"
type="text"
class="key-input"
:placeholder="t('keybindings.keyPlaceholder')"
@keydown.enter="addKeyPart"
@keydown.escape="cancelEdit"
@blur="cancelEdit"
/>
</template>
<!-- 添加按钮 -->
<template v-else>
<button
class="key-tag-add"
@click="startAddKey(binding.id!)"
>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 1V11M1 6H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
</template>
</div>
</div>
</div>
</div> </div>
</AccordionItem>
<div class="extension-col">{{ binding.extension }}</div> </AccordionContainer>
<div class="description-col">{{ binding.description }}</div>
</div>
</div>
</SettingSection> </SettingSection>
</div> </div>
</template> </template>
@@ -375,167 +448,275 @@ const confirmKeybinding = async () => {
} }
} }
.key-bindings-container { .binding-title {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
gap: 16px;
.key-bindings-header { &.disabled {
display: flex; opacity: 0.5;
padding: 0 0 8px 0;
border-bottom: 1px solid var(--settings-border);
color: var(--text-muted);
font-size: 12px;
font-weight: 500;
}
.key-binding-row {
display: flex;
padding: 10px 0;
border-bottom: 1px solid var(--settings-border);
align-items: center;
transition: background-color 0.2s ease;
&:hover {
background-color: var(--settings-hover);
}
}
.keybinding-col {
width: 150px;
display: flex;
gap: 4px;
padding: 0 10px 0 0;
color: var(--settings-text);
align-items: center;
cursor: pointer;
transition: all 0.2s ease;
&:hover:not(.editing) .key-badge {
border-color: #4a9eff;
}
&.editing {
cursor: default;
}
.key-badge {
background-color: var(--settings-input-bg);
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
border: 1px solid var(--settings-input-border);
color: var(--settings-text);
transition: border-color 0.2s ease;
white-space: nowrap;
&.waiting {
border: none;
background-color: transparent;
padding: 0;
color: #4a9eff;
font-style: italic;
animation: colorPulse 1.5s ease-in-out infinite;
}
&.captured {
background-color: #4a9eff;
color: white;
border-color: #4a9eff;
&.conflict {
background-color: #dc3545;
border-color: #dc3545;
animation: shake 0.6s ease-in-out;
}
}
}
}
.btn-mini {
width: 16px;
height: 16px;
min-width: 16px;
border: none;
border-radius: 2px;
cursor: pointer;
font-size: 10px;
transition: opacity 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
line-height: 1;
margin-left: auto;
&.btn-confirm {
background-color: #28a745;
color: white;
&:hover:not(:disabled) {
opacity: 0.85;
}
&:disabled {
background-color: var(--settings-input-border);
cursor: not-allowed;
opacity: 0.5;
}
}
&.btn-cancel {
background-color: #dc3545;
color: white;
margin-left: 2px;
&:hover {
opacity: 0.85;
}
}
}
.extension-col {
width: 80px;
padding: 0 10px 0 0;
font-size: 13px;
color: var(--settings-text);
text-transform: capitalize;
}
.description-col {
flex: 1;
font-size: 13px;
color: var(--settings-text);
} }
} }
@keyframes colorPulse { .binding-name {
0%, 100% { flex: 1;
color: #4a9eff; display: flex;
flex-direction: column;
gap: 4px;
}
.binding-description {
font-size: 13px;
font-weight: 500;
color: var(--settings-text);
}
.binding-extension {
font-size: 11px;
color: var(--text-muted);
text-transform: capitalize;
}
.binding-keys {
display: flex;
gap: 4px;
align-items: center;
}
.key-badge {
background-color: var(--settings-input-bg);
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
border: 1px solid var(--settings-input-border);
color: var(--settings-text);
white-space: nowrap;
}
.key-badge-empty {
font-size: 11px;
color: var(--text-muted);
font-style: italic;
}
.binding-config {
display: flex;
flex-direction: column;
gap: 16px;
}
.config-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.config-label {
font-size: 13px;
color: var(--settings-text);
font-weight: 500;
}
// Switch 开关样式
.switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
flex-shrink: 0;
input {
opacity: 0;
width: 0;
height: 0;
&:checked + .slider {
background-color: #4a9eff;
&:before {
transform: translateX(16px);
}
}
&:focus + .slider {
box-shadow: 0 0 1px #4a9eff;
}
}
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--settings-input-border);
transition: 0.3s;
border-radius: 20px;
&:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
}
.key-input-wrapper {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
}
.key-tags {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
flex: 1;
min-height: 28px;
}
.key-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
height: 28px;
background-color: var(--settings-input-bg);
border: 1px solid var(--settings-input-border);
border-radius: 4px;
font-size: 12px;
font-weight: 500;
color: var(--settings-text);
transition: all 0.2s ease;
box-sizing: border-box;
&:hover {
border-color: #4a9eff;
.key-tag-remove {
opacity: 1;
}
}
}
.key-tag-text {
user-select: none;
}
.key-tag-remove {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 16px;
height: 16px;
border: none;
background: none;
color: var(--text-muted);
cursor: pointer;
font-size: 18px;
line-height: 1;
padding: 0;
margin: 0;
opacity: 0.6;
transition: all 0.2s ease;
&:hover {
color: #e74c3c;
opacity: 1; opacity: 1;
} }
50% {
color: #2080ff;
opacity: 0.6;
}
} }
@keyframes shake { .key-tag-add {
0%, 100% { display: flex;
transform: translateX(0); align-items: center;
} justify-content: center;
10%, 30%, 50%, 70%, 90% { width: 28px;
transform: translateX(-4px); height: 28px;
} flex-shrink: 0;
20%, 40%, 60%, 80% { border: 1px dashed var(--settings-input-border);
transform: translateX(4px); border-radius: 4px;
} background-color: transparent;
}
.coming-soon-placeholder {
padding: 20px;
background-color: var(--settings-card-bg);
border-radius: 6px;
color: var(--text-muted); color: var(--text-muted);
text-align: center; cursor: pointer;
font-style: italic; transition: all 0.2s ease;
font-size: 13px; box-sizing: border-box;
&:hover {
border-color: #4a9eff;
background-color: var(--settings-input-bg);
color: #4a9eff;
}
} }
</style>
.key-input {
padding: 4px 8px;
height: 28px;
border: 1px solid #4a9eff;
border-radius: 4px;
background-color: var(--settings-input-bg);
color: var(--settings-text);
font-size: 12px;
width: 60px;
outline: none;
box-sizing: border-box;
&::placeholder {
color: var(--text-muted);
font-size: 11px;
}
}
.btn-mini {
width: 24px;
height: 24px;
min-width: 24px;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
transition: opacity 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
line-height: 1;
flex-shrink: 0;
&.btn-confirm {
background-color: #28a745;
color: white;
&:hover:not(:disabled) {
opacity: 0.85;
}
&:disabled {
background-color: var(--settings-input-border);
cursor: not-allowed;
opacity: 0.5;
}
}
&.btn-cancel {
background-color: #dc3545;
color: white;
&:hover {
opacity: 0.85;
}
}
}
</style>

View File

@@ -72,6 +72,72 @@
</div> </div>
</SettingSection> </SettingSection>
<!-- Toast 通知测试区域 -->
<SettingSection title="Toast Notification Test">
<SettingItem title="Toast Message">
<input
v-model="toastMessage"
type="text"
placeholder="Enter toast message"
class="select-input"
/>
</SettingItem>
<SettingItem title="Toast Title (Optional)">
<input
v-model="toastTitle"
type="text"
placeholder="Enter toast title"
class="select-input"
/>
</SettingItem>
<SettingItem title="Position">
<select v-model="toastPosition" class="select-input">
<option value="top-right">Top Right</option>
<option value="top-left">Top Left</option>
<option value="top-center">Top Center</option>
<option value="bottom-right">Bottom Right</option>
<option value="bottom-left">Bottom Left</option>
<option value="bottom-center">Bottom Center</option>
</select>
</SettingItem>
<SettingItem title="Duration (ms)">
<input
v-model.number="toastDuration"
type="number"
min="0"
step="500"
placeholder="4000"
class="select-input"
/>
</SettingItem>
<SettingItem title="Toast Types">
<div class="button-group">
<button @click="showToast('success')" class="test-button toast-success-btn">
Success
</button>
<button @click="showToast('error')" class="test-button toast-error-btn">
Error
</button>
<button @click="showToast('warning')" class="test-button toast-warning-btn">
Warning
</button>
<button @click="showToast('info')" class="test-button toast-info-btn">
Info
</button>
</div>
</SettingItem>
<SettingItem title="Quick Tests">
<div class="button-group">
<button @click="showMultipleToasts" class="test-button">
Show Multiple Toasts
</button>
<button @click="clearAllToasts" class="test-button">
Clear All Toasts
</button>
</div>
</SettingItem>
</SettingSection>
<!-- 清除所有测试状态 --> <!-- 清除所有测试状态 -->
<SettingSection title="Cleanup"> <SettingSection title="Cleanup">
<SettingItem title="Clear All"> <SettingItem title="Clear All">
@@ -91,6 +157,8 @@ import { ref } from 'vue';
import * as TestService from '@/../bindings/voidraft/internal/services/testservice'; import * as TestService from '@/../bindings/voidraft/internal/services/testservice';
import SettingSection from '../components/SettingSection.vue'; import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue'; import SettingItem from '../components/SettingItem.vue';
import toast from '@/components/toast';
import type { ToastPosition, ToastType } from '@/components/toast/types';
// Badge测试状态 // Badge测试状态
const badgeText = ref(''); const badgeText = ref('');
@@ -102,6 +170,12 @@ const notificationSubtitle = ref('');
const notificationBody = ref(''); const notificationBody = ref('');
const notificationStatus = ref<{ type: string; message: string } | null>(null); const notificationStatus = ref<{ type: string; message: string } | null>(null);
// Toast 测试状态
const toastMessage = ref('This is a test toast notification!');
const toastTitle = ref('');
const toastPosition = ref<ToastPosition>('top-right');
const toastDuration = ref(4000);
// 清除状态 // 清除状态
const clearStatus = ref<{ type: string; message: string } | null>(null); const clearStatus = ref<{ type: string; message: string } | null>(null);
@@ -172,13 +246,57 @@ const clearAll = async () => {
showStatus(clearStatus, 'error', `Failed to clear test states: ${error.message || error}`); showStatus(clearStatus, 'error', `Failed to clear test states: ${error.message || error}`);
} }
}; };
// Toast 相关函数
const showToast = (type: ToastType) => {
const message = toastMessage.value || `This is a ${type} toast notification!`;
const title = toastTitle.value || undefined;
const options = {
position: toastPosition.value,
duration: toastDuration.value,
};
switch (type) {
case 'success':
toast.success(message, title, options);
break;
case 'error':
toast.error(message, title, options);
break;
case 'warning':
toast.warning(message, title, options);
break;
case 'info':
toast.info(message, title, options);
break;
}
};
const showMultipleToasts = () => {
const positions: ToastPosition[] = ['top-right', 'top-left', 'bottom-right', 'bottom-left'];
const types: ToastType[] = ['success', 'error', 'warning', 'info'];
positions.forEach((position, index) => {
setTimeout(() => {
const type = types[index % types.length];
toast.show({
type,
message: `Toast from ${position}`,
title: `${type.charAt(0).toUpperCase() + type.slice(1)} Toast`,
position,
duration: 5000,
});
}, index * 200);
});
};
const clearAllToasts = () => {
toast.clear();
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.settings-page {
//padding: 20px 0 20px 0;
}
.dev-description { .dev-description {
color: var(--settings-text-secondary); color: var(--settings-text-secondary);
font-size: 12px; font-size: 12px;
@@ -249,6 +367,50 @@ const clearAll = async () => {
opacity: 0.9; opacity: 0.9;
} }
} }
&.toast-success-btn {
background-color: #16a34a;
color: white;
border-color: #16a34a;
&:hover {
background-color: #15803d;
border-color: #15803d;
}
}
&.toast-error-btn {
background-color: #dc2626;
color: white;
border-color: #dc2626;
&:hover {
background-color: #b91c1c;
border-color: #b91c1c;
}
}
&.toast-warning-btn {
background-color: #f59e0b;
color: white;
border-color: #f59e0b;
&:hover {
background-color: #d97706;
border-color: #d97706;
}
}
&.toast-info-btn {
background-color: #3b82f6;
color: white;
border-color: #3b82f6;
&:hover {
background-color: #2563eb;
border-color: #2563eb;
}
}
} }
.test-status { .test-status {

15
go.mod
View File

@@ -11,17 +11,17 @@ require (
github.com/knadh/koanf/providers/file v1.2.1 github.com/knadh/koanf/providers/file v1.2.1
github.com/knadh/koanf/providers/structs v1.0.0 github.com/knadh/koanf/providers/structs v1.0.0
github.com/knadh/koanf/v2 v2.3.0 github.com/knadh/koanf/v2 v2.3.0
github.com/mattn/go-sqlite3 v1.14.32 github.com/mattn/go-sqlite3 v1.14.33
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/wailsapp/wails/v3 v3.0.0-alpha.51 github.com/wailsapp/wails/v3 v3.0.0-alpha.55
golang.org/x/net v0.48.0 golang.org/x/net v0.48.0
golang.org/x/sys v0.39.0 golang.org/x/sys v0.39.0
golang.org/x/text v0.32.0 golang.org/x/text v0.32.0
resty.dev/v3 v3.0.0-beta.5 resty.dev/v3 v3.0.0-beta.6
) )
require ( require (
ariga.io/atlas v0.38.0 // indirect ariga.io/atlas v1.0.0 // indirect
code.gitea.io/sdk/gitea v0.22.1 // indirect code.gitea.io/sdk/gitea v0.22.1 // indirect
dario.cat/mergo v1.0.2 // indirect dario.cat/mergo v1.0.2 // indirect
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
@@ -48,11 +48,12 @@ require (
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/inflect v0.21.5 // indirect github.com/go-openapi/inflect v0.21.5 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/godbus/dbus/v5 v5.2.1 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-github/v74 v74.0.0 // indirect github.com/google/go-github/v74 v74.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.2.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect github.com/hashicorp/go-version v1.8.0 // indirect
@@ -79,7 +80,6 @@ require (
github.com/skeema/knownhosts v1.3.2 // indirect github.com/skeema/knownhosts v1.3.2 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect github.com/ulikunitz/xz v0.5.15 // indirect
github.com/wailsapp/go-webview2 v1.0.23 // indirect github.com/wailsapp/go-webview2 v1.0.23 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/zclconf/go-cty v1.17.0 // indirect github.com/zclconf/go-cty v1.17.0 // indirect
github.com/zclconf/go-cty-yaml v1.2.0 // indirect github.com/zclconf/go-cty-yaml v1.2.0 // indirect
@@ -92,7 +92,6 @@ require (
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.40.0 // indirect golang.org/x/tools v0.40.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

54
go.sum
View File

@@ -1,5 +1,5 @@
ariga.io/atlas v0.38.0 h1:MwbtwVtDWJFq+ECyeTAz2ArvewDnpeiw/t/sgNdDsdo= ariga.io/atlas v1.0.0 h1:v9DQH49xK+SM2kKwk4OQBjfz/KNRMUR+pvDiEIxSJto=
ariga.io/atlas v0.38.0/go.mod h1:D7XMK6ei3GvfDqvzk+2VId78j77LdqHrqPOWamn51/s= ariga.io/atlas v1.0.0/go.mod h1:esBbk3F+pi/mM2PvbCymDm+kWhaOk4PaaiegQdNELk8=
code.gitea.io/sdk/gitea v0.22.1 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA= code.gitea.io/sdk/gitea v0.22.1 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA=
code.gitea.io/sdk/gitea v0.22.1/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= code.gitea.io/sdk/gitea v0.22.1/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
@@ -33,8 +33,16 @@ github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ= github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creativeprojects/go-selfupdate v1.5.2 h1:3KR3JLrq70oplb9yZzbmJ89qRP78D1AN/9u+l3k0LJ4= github.com/creativeprojects/go-selfupdate v1.5.2 h1:3KR3JLrq70oplb9yZzbmJ89qRP78D1AN/9u+l3k0LJ4=
github.com/creativeprojects/go-selfupdate v1.5.2/go.mod h1:BCOuwIl1dRRCmPNRPH0amULeZqayhKyY2mH/h4va7Dk= github.com/creativeprojects/go-selfupdate v1.5.2/go.mod h1:BCOuwIl1dRRCmPNRPH0amULeZqayhKyY2mH/h4va7Dk=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
@@ -68,6 +76,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 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-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-openapi/inflect v0.21.5 h1:M2RCq6PPS3YbIaL7CXosGL3BbzAcmfBAT0nC3YfesZA= github.com/go-openapi/inflect v0.21.5 h1:M2RCq6PPS3YbIaL7CXosGL3BbzAcmfBAT0nC3YfesZA=
@@ -76,17 +86,19 @@ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= 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/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.2.1 h1:I4wwMdWSkmI57ewd+elNGwLRf2/dtSaFz1DujfWYvOk= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/godbus/dbus/v5 v5.2.1/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM= github.com/google/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM=
github.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak= github.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
@@ -103,6 +115,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
@@ -137,14 +151,18 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
@@ -172,14 +190,14 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0= github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0=
github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/wails/v3 v3.0.0-alpha.55 h1:Wxwxc4EN6axDAvH/O5n3uoZQ+XRY/HQZ5rMdn0npq78=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/wails/v3 v3.0.0-alpha.55/go.mod h1:AyH9vRcseorpL3p5XvxKgK0Lv/agJ7pTmcPdy25xZPo=
github.com/wailsapp/wails/v3 v3.0.0-alpha.51 h1:n8KT0H4lvtWld9tMIiHVX4nrR0wEMT2zy5hM/R6luMU=
github.com/wailsapp/wails/v3 v3.0.0-alpha.51/go.mod h1:yaz8baG0+YzoiN8J6osn0wKiEi0iUux0ZU5NsZFu6OQ=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=
@@ -190,6 +208,8 @@ github.com/zclconf/go-cty-yaml v1.2.0 h1:GDyL4+e/Qe/S0B7YaecMLbVvAR/Mp21CXMOSiCT
github.com/zclconf/go-cty-yaml v1.2.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= github.com/zclconf/go-cty-yaml v1.2.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
gitlab.com/gitlab-org/api/client-go v1.10.0 h1:VlB9gXQdG6w643lH53VduUHVnCWQG5Ty86VbXnyi70A= gitlab.com/gitlab-org/api/client-go v1.10.0 h1:VlB9gXQdG6w643lH53VduUHVnCWQG5Ty86VbXnyi70A=
gitlab.com/gitlab-org/api/client-go v1.10.0/go.mod h1:U3QKvjbT1J1FrgLsA7w/XlhoBIendUqB4o3/Ht3UhEQ= gitlab.com/gitlab-org/api/client-go v1.10.0/go.mod h1:U3QKvjbT1J1FrgLsA7w/XlhoBIendUqB4o3/Ht3UhEQ=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
@@ -204,7 +224,6 @@ golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
@@ -238,18 +257,15 @@ golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
resty.dev/v3 v3.0.0-beta.5 h1:NV1xbqOLzSq7XMTs1t/HLPvu7xrxoXzF90SR4OO6faQ= resty.dev/v3 v3.0.0-beta.6 h1:ghRdNpoE8/wBCv+kTKIOauW1aCrSIeTq7GxtfYgtevU=
resty.dev/v3 v3.0.0-beta.5/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM= resty.dev/v3 v3.0.0-beta.6/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM=

View File

@@ -39,29 +39,12 @@ const (
SystemThemeAuto SystemThemeType = "auto" SystemThemeAuto SystemThemeType = "auto"
) )
// UpdateSourceType 更新源类型
type UpdateSourceType string
const (
// UpdateSourceGithub GitHub更新源
UpdateSourceGithub UpdateSourceType = "github"
// UpdateSourceGitea Gitea更新源
UpdateSourceGitea UpdateSourceType = "gitea"
)
// GithubConfig GitHub配置 // GithubConfig GitHub配置
type GithubConfig struct { type GithubConfig struct {
Owner string `json:"owner"` // 仓库所有者 Owner string `json:"owner"` // 仓库所有者
Repo string `json:"repo"` // 仓库名称 Repo string `json:"repo"` // 仓库名称
} }
// GiteaConfig Gitea配置
type GiteaConfig struct {
BaseURL string `json:"baseURL"` // Gitea服务器URL
Owner string `json:"owner"` // 仓库所有者
Repo string `json:"repo"` // 仓库名称
}
// GeneralConfig 通用设置配置 // GeneralConfig 通用设置配置
type GeneralConfig struct { type GeneralConfig struct {
AlwaysOnTop bool `json:"alwaysOnTop"` // 窗口是否置顶 AlwaysOnTop bool `json:"alwaysOnTop"` // 窗口是否置顶
@@ -79,6 +62,7 @@ type GeneralConfig struct {
// 界面设置 // 界面设置
EnableLoadingAnimation bool `json:"enableLoadingAnimation"` // 是否启用加载动画 EnableLoadingAnimation bool `json:"enableLoadingAnimation"` // 是否启用加载动画
EnableTabs bool `json:"enableTabs"` // 是否启用标签页模式 EnableTabs bool `json:"enableTabs"` // 是否启用标签页模式
EnableMemoryMonitor bool `json:"enableMemoryMonitor"` // 是否启用内存监视器
} }
// HotkeyCombo 热键组合定义 // HotkeyCombo 热键组合定义
@@ -119,14 +103,11 @@ type AppearanceConfig struct {
// UpdatesConfig 更新设置配置 // UpdatesConfig 更新设置配置
type UpdatesConfig struct { type UpdatesConfig struct {
Version string `json:"version"` // 当前版本号 Version string `json:"version"` // 当前版本号
AutoUpdate bool `json:"autoUpdate"` // 是否自动更新 AutoUpdate bool `json:"autoUpdate"` // 是否自动更新
PrimarySource UpdateSourceType `json:"primarySource"` // 主要更新源 BackupBeforeUpdate bool `json:"backupBeforeUpdate"` // 更新前是否备份
BackupSource UpdateSourceType `json:"backupSource"` // 备用更新源 UpdateTimeout int `json:"updateTimeout"` // 更新超时时间(秒)
BackupBeforeUpdate bool `json:"backupBeforeUpdate"` // 更新前是否备份 Github GithubConfig `json:"github"` // GitHub配置
UpdateTimeout int `json:"updateTimeout"` // 更新超时时间(秒)
Github GithubConfig `json:"github"` // GitHub配置
Gitea GiteaConfig `json:"gitea"` // Gitea配置
} }
// Git备份相关类型定义 // Git备份相关类型定义
@@ -188,6 +169,7 @@ func NewDefaultAppConfig() *AppConfig {
EnableGlobalHotkey: false, EnableGlobalHotkey: false,
EnableLoadingAnimation: true, // 默认启用加载动画 EnableLoadingAnimation: true, // 默认启用加载动画
EnableTabs: false, // 默认不启用标签页模式 EnableTabs: false, // 默认不启用标签页模式
EnableMemoryMonitor: true, // 默认启用内存监视器
GlobalHotkey: HotkeyCombo{ GlobalHotkey: HotkeyCombo{
Ctrl: false, Ctrl: false,
Shift: false, Shift: false,
@@ -219,19 +201,12 @@ func NewDefaultAppConfig() *AppConfig {
Updates: UpdatesConfig{ Updates: UpdatesConfig{
Version: version.Version, Version: version.Version,
AutoUpdate: true, AutoUpdate: true,
PrimarySource: UpdateSourceGitea,
BackupSource: UpdateSourceGithub,
BackupBeforeUpdate: true, BackupBeforeUpdate: true,
UpdateTimeout: 30, UpdateTimeout: 120,
Github: GithubConfig{ Github: GithubConfig{
Owner: "landaiqing", Owner: "landaiqing",
Repo: "voidraft", Repo: "voidraft",
}, },
Gitea: GiteaConfig{
BaseURL: "https://git.landaiqing.cn",
Owner: "landaiqing",
Repo: "voidraft",
},
}, },
Backup: GitBackupConfig{ Backup: GitBackupConfig{
Enabled: false, Enabled: false,

View File

@@ -106,7 +106,7 @@ const (
CopyBlockImage KeyBindingName = "copyBlockImage" // 复制块为图片 CopyBlockImage KeyBindingName = "copyBlockImage" // 复制块为图片
) )
const defaultExtension = "editor" const DefaultExtension = "editor"
// NewDefaultKeyBindings 创建默认快捷键配置 // NewDefaultKeyBindings 创建默认快捷键配置
func NewDefaultKeyBindings() []KeyBinding { func NewDefaultKeyBindings() []KeyBinding {
@@ -135,7 +135,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockSelectAll, Name: BlockSelectAll,
Type: Standard, Type: Standard,
Key: "Mod-a", Key: "Mod-a",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -143,7 +143,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockAddAfterCurrent, Name: BlockAddAfterCurrent,
Type: Standard, Type: Standard,
Key: "Mod-Enter", Key: "Mod-Enter",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -151,7 +151,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockAddAfterLast, Name: BlockAddAfterLast,
Type: Standard, Type: Standard,
Key: "Mod-Shift-Enter", Key: "Mod-Shift-Enter",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -159,7 +159,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockAddBeforeCurrent, Name: BlockAddBeforeCurrent,
Type: Standard, Type: Standard,
Key: "Alt-Enter", Key: "Alt-Enter",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -167,7 +167,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockGotoPrevious, Name: BlockGotoPrevious,
Type: Standard, Type: Standard,
Key: "Mod-ArrowUp", Key: "Mod-ArrowUp",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -175,7 +175,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockGotoNext, Name: BlockGotoNext,
Type: Standard, Type: Standard,
Key: "Mod-ArrowDown", Key: "Mod-ArrowDown",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -183,7 +183,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockSelectPrevious, Name: BlockSelectPrevious,
Type: Standard, Type: Standard,
Key: "Mod-Shift-ArrowUp", Key: "Mod-Shift-ArrowUp",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -191,7 +191,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockSelectNext, Name: BlockSelectNext,
Type: Standard, Type: Standard,
Key: "Mod-Shift-ArrowDown", Key: "Mod-Shift-ArrowDown",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -199,7 +199,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockDelete, Name: BlockDelete,
Type: Standard, Type: Standard,
Key: "Mod-Shift-d", Key: "Mod-Shift-d",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -207,7 +207,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockMoveUp, Name: BlockMoveUp,
Type: Standard, Type: Standard,
Key: "Shift-Mod-ArrowUp", Key: "Shift-Mod-ArrowUp",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -215,7 +215,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockMoveDown, Name: BlockMoveDown,
Type: Standard, Type: Standard,
Key: "Shift-Mod-ArrowDown", Key: "Shift-Mod-ArrowDown",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -223,7 +223,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockDeleteLine, Name: BlockDeleteLine,
Type: Standard, Type: Standard,
Key: "Mod-Shift-k", Key: "Mod-Shift-k",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -231,7 +231,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockMoveLineUp, Name: BlockMoveLineUp,
Type: Standard, Type: Standard,
Key: "Alt-ArrowUp", Key: "Alt-ArrowUp",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -239,7 +239,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockMoveLineDown, Name: BlockMoveLineDown,
Type: Standard, Type: Standard,
Key: "Alt-ArrowDown", Key: "Alt-ArrowDown",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -247,7 +247,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockTransposeChars, Name: BlockTransposeChars,
Type: Standard, Type: Standard,
Key: "Mod-t", Key: "Mod-t",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -255,7 +255,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockFormat, Name: BlockFormat,
Type: Standard, Type: Standard,
Key: "Mod-Shift-f", Key: "Mod-Shift-f",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -263,7 +263,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockCopy, Name: BlockCopy,
Type: Standard, Type: Standard,
Key: "Mod-c", Key: "Mod-c",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -271,7 +271,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockCut, Name: BlockCut,
Type: Standard, Type: Standard,
Key: "Mod-x", Key: "Mod-x",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -279,7 +279,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockPaste, Name: BlockPaste,
Type: Standard, Type: Standard,
Key: "Mod-v", Key: "Mod-v",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -335,7 +335,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: HistoryUndo, Name: HistoryUndo,
Type: Standard, Type: Standard,
Key: "Mod-z", Key: "Mod-z",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -344,7 +344,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Type: Standard, Type: Standard,
Key: "Mod-Shift-z", Key: "Mod-Shift-z",
Windows: "Ctrl-y", Windows: "Ctrl-y",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -352,7 +352,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: HistoryUndoSelection, Name: HistoryUndoSelection,
Type: Standard, Type: Standard,
Key: "Mod-u", Key: "Mod-u",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -361,7 +361,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Type: Standard, Type: Standard,
Key: "Mod-Shift-u", Key: "Mod-Shift-u",
Windows: "Alt-u", Windows: "Alt-u",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -373,7 +373,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Macos: "Ctrl-ArrowLeft", Macos: "Ctrl-ArrowLeft",
Windows: "Alt-ArrowLeft", Windows: "Alt-ArrowLeft",
Linux: "Alt-ArrowLeft", Linux: "Alt-ArrowLeft",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -383,7 +383,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Macos: "Ctrl-ArrowRight", Macos: "Ctrl-ArrowRight",
Windows: "Alt-ArrowRight", Windows: "Alt-ArrowRight",
Linux: "Alt-ArrowRight", Linux: "Alt-ArrowRight",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -391,7 +391,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: SelectSyntaxLeft, Name: SelectSyntaxLeft,
Type: Standard, Type: Standard,
Key: "Shift-Alt-ArrowLeft", Key: "Shift-Alt-ArrowLeft",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -399,7 +399,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: SelectSyntaxRight, Name: SelectSyntaxRight,
Type: Standard, Type: Standard,
Key: "Shift-Alt-ArrowRight", Key: "Shift-Alt-ArrowRight",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -407,7 +407,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: CopyLineUp, Name: CopyLineUp,
Type: Standard, Type: Standard,
Key: "Shift-Alt-ArrowUp", Key: "Shift-Alt-ArrowUp",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -415,7 +415,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: CopyLineDown, Name: CopyLineDown,
Type: Standard, Type: Standard,
Key: "Shift-Alt-ArrowDown", Key: "Shift-Alt-ArrowDown",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -423,9 +423,9 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: InsertBlankLine, Name: InsertBlankLine,
Type: Standard, Type: Standard,
Key: "Mod-Enter", Key: "Mod-Enter",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: false,
PreventDefault: true, PreventDefault: false,
}, },
{ {
Name: SelectLine, Name: SelectLine,
@@ -433,7 +433,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Macos: "Ctrl-l", Macos: "Ctrl-l",
Windows: "Alt-l", Windows: "Alt-l",
Linux: "Alt-l", Linux: "Alt-l",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -441,7 +441,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: SelectParentSyntax, Name: SelectParentSyntax,
Type: Standard, Type: Standard,
Key: "Mod-i", Key: "Mod-i",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -449,7 +449,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: SimplifySelection, Name: SimplifySelection,
Type: Standard, Type: Standard,
Key: "Escape", Key: "Escape",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: false, PreventDefault: false,
}, },
@@ -459,7 +459,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Macos: "Cmd-Alt-ArrowUp", Macos: "Cmd-Alt-ArrowUp",
Windows: "Ctrl-Alt-ArrowUp", Windows: "Ctrl-Alt-ArrowUp",
Linux: "Ctrl-Alt-ArrowUp", Linux: "Ctrl-Alt-ArrowUp",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -469,7 +469,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Macos: "Cmd-Alt-ArrowDown", Macos: "Cmd-Alt-ArrowDown",
Windows: "Ctrl-Alt-ArrowDown", Windows: "Ctrl-Alt-ArrowDown",
Linux: "Ctrl-Alt-ArrowDown", Linux: "Ctrl-Alt-ArrowDown",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -479,7 +479,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Windows: "Ctrl-ArrowLeft", Windows: "Ctrl-ArrowLeft",
Linux: "Ctrl-ArrowLeft", Linux: "Ctrl-ArrowLeft",
Macos: "Alt-ArrowLeft", Macos: "Alt-ArrowLeft",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -489,7 +489,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Windows: "Ctrl-ArrowRight", Windows: "Ctrl-ArrowRight",
Linux: "Ctrl-ArrowRight", Linux: "Ctrl-ArrowRight",
Macos: "Alt-ArrowRight", Macos: "Alt-ArrowRight",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -499,7 +499,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Windows: "Ctrl-Shift-ArrowLeft", Windows: "Ctrl-Shift-ArrowLeft",
Linux: "Ctrl-Shift-ArrowLeft", Linux: "Ctrl-Shift-ArrowLeft",
Macos: "Alt-Shift-ArrowLeft", Macos: "Alt-Shift-ArrowLeft",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -509,7 +509,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Windows: "Ctrl-Shift-ArrowRight", Windows: "Ctrl-Shift-ArrowRight",
Linux: "Ctrl-Shift-ArrowRight", Linux: "Ctrl-Shift-ArrowRight",
Macos: "Alt-Shift-ArrowRight", Macos: "Alt-Shift-ArrowRight",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -519,7 +519,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Macos: "Ctrl-k", Macos: "Ctrl-k",
Windows: "Ctrl-k", Windows: "Ctrl-k",
Linux: "Ctrl-k", Linux: "Ctrl-k",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -527,7 +527,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: DeleteToLineStart, Name: DeleteToLineStart,
Type: Standard, Type: Standard,
Key: "Mod-Shift-Backspace", Key: "Mod-Shift-Backspace",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -535,7 +535,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: CursorLineStart, Name: CursorLineStart,
Type: Standard, Type: Standard,
Key: "Home", Key: "Home",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: false, PreventDefault: false,
}, },
@@ -543,7 +543,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: CursorLineEnd, Name: CursorLineEnd,
Type: Standard, Type: Standard,
Key: "End", Key: "End",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: false, PreventDefault: false,
}, },
@@ -551,7 +551,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: SelectLineStart, Name: SelectLineStart,
Type: Standard, Type: Standard,
Key: "Shift-Home", Key: "Shift-Home",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: false, PreventDefault: false,
}, },
@@ -559,7 +559,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: SelectLineEnd, Name: SelectLineEnd,
Type: Standard, Type: Standard,
Key: "Shift-End", Key: "Shift-End",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: false, PreventDefault: false,
}, },
@@ -567,7 +567,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: CursorDocStart, Name: CursorDocStart,
Type: Standard, Type: Standard,
Key: "Mod-Home", Key: "Mod-Home",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -575,7 +575,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: CursorDocEnd, Name: CursorDocEnd,
Type: Standard, Type: Standard,
Key: "Mod-End", Key: "Mod-End",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -583,7 +583,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: SelectDocStart, Name: SelectDocStart,
Type: Standard, Type: Standard,
Key: "Mod-Shift-Home", Key: "Mod-Shift-Home",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -591,7 +591,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: SelectDocEnd, Name: SelectDocEnd,
Type: Standard, Type: Standard,
Key: "Mod-Shift-End", Key: "Mod-Shift-End",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -599,7 +599,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: SelectMatchingBracket, Name: SelectMatchingBracket,
Type: Standard, Type: Standard,
Key: "Mod-Shift-p", Key: "Mod-Shift-p",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -609,7 +609,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Macos: "Ctrl-o", Macos: "Ctrl-o",
Windows: "Ctrl-o", Windows: "Ctrl-o",
Linux: "Ctrl-o", Linux: "Ctrl-o",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -618,7 +618,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: IndentLess, Name: IndentLess,
Type: Standard, Type: Standard,
Key: "Mod-[", Key: "Mod-[",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -626,7 +626,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: IndentMore, Name: IndentMore,
Type: Standard, Type: Standard,
Key: "Mod-]", Key: "Mod-]",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -634,7 +634,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: IndentSelection, Name: IndentSelection,
Type: Standard, Type: Standard,
Key: "Mod-Alt-\\", Key: "Mod-Alt-\\",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -642,7 +642,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: CursorMatchingBracket, Name: CursorMatchingBracket,
Type: Standard, Type: Standard,
Key: "Shift-Mod-\\", Key: "Shift-Mod-\\",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -650,7 +650,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: ToggleComment, Name: ToggleComment,
Type: Standard, Type: Standard,
Key: "Mod-/", Key: "Mod-/",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -658,7 +658,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: ToggleBlockComment, Name: ToggleBlockComment,
Type: Standard, Type: Standard,
Key: "Shift-Alt-a", Key: "Shift-Alt-a",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -667,7 +667,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: InsertNewlineAndIndent, Name: InsertNewlineAndIndent,
Type: Standard, Type: Standard,
Key: "Enter", Key: "Enter",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: false, PreventDefault: false,
}, },
@@ -675,7 +675,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: DeleteCharBackward, Name: DeleteCharBackward,
Type: Standard, Type: Standard,
Key: "Backspace", Key: "Backspace",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: false, PreventDefault: false,
}, },
@@ -683,7 +683,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: DeleteCharForward, Name: DeleteCharForward,
Type: Standard, Type: Standard,
Key: "Delete", Key: "Delete",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: false, PreventDefault: false,
}, },
@@ -691,7 +691,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: DeleteGroupBackward, Name: DeleteGroupBackward,
Type: Standard, Type: Standard,
Key: "Mod-Backspace", Key: "Mod-Backspace",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -699,7 +699,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: DeleteGroupForward, Name: DeleteGroupForward,
Type: Standard, Type: Standard,
Key: "Mod-Delete", Key: "Mod-Delete",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -712,7 +712,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: CursorCharLeft, Name: CursorCharLeft,
Type: Emacs, Type: Emacs,
Key: "Ctrl-b", Key: "Ctrl-b",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -720,7 +720,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: SelectCharLeft, Name: SelectCharLeft,
Type: Emacs, Type: Emacs,
Key: "Shift-Ctrl-b", Key: "Shift-Ctrl-b",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -728,7 +728,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: CursorCharRight, Name: CursorCharRight,
Type: Emacs, Type: Emacs,
Key: "Ctrl-f", Key: "Ctrl-f",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -736,7 +736,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: SelectCharRight, Name: SelectCharRight,
Type: Emacs, Type: Emacs,
Key: "Shift-Ctrl-f", Key: "Shift-Ctrl-f",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -746,7 +746,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: CursorLineUp, Name: CursorLineUp,
Type: Emacs, Type: Emacs,
Key: "Ctrl-p", Key: "Ctrl-p",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -754,7 +754,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: SelectLineUp, Name: SelectLineUp,
Type: Emacs, Type: Emacs,
Key: "Shift-Ctrl-p", Key: "Shift-Ctrl-p",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -762,7 +762,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: CursorLineDown, Name: CursorLineDown,
Type: Emacs, Type: Emacs,
Key: "Ctrl-n", Key: "Ctrl-n",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -770,7 +770,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: SelectLineDown, Name: SelectLineDown,
Type: Emacs, Type: Emacs,
Key: "Shift-Ctrl-n", Key: "Shift-Ctrl-n",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -780,7 +780,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: CursorLineStart, Name: CursorLineStart,
Type: Emacs, Type: Emacs,
Key: "Ctrl-a", Key: "Ctrl-a",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -788,7 +788,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: SelectLineStart, Name: SelectLineStart,
Type: Emacs, Type: Emacs,
Key: "Shift-Ctrl-a", Key: "Shift-Ctrl-a",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -796,7 +796,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: CursorLineEnd, Name: CursorLineEnd,
Type: Emacs, Type: Emacs,
Key: "Ctrl-e", Key: "Ctrl-e",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -804,7 +804,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: SelectLineEnd, Name: SelectLineEnd,
Type: Emacs, Type: Emacs,
Key: "Shift-Ctrl-e", Key: "Shift-Ctrl-e",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -814,7 +814,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: CursorPageDown, Name: CursorPageDown,
Type: Emacs, Type: Emacs,
Key: "Ctrl-v", Key: "Ctrl-v",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -822,7 +822,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: CursorPageUp, Name: CursorPageUp,
Type: Emacs, Type: Emacs,
Key: "Alt-v", Key: "Alt-v",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -832,7 +832,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: DeleteCharForward, Name: DeleteCharForward,
Type: Emacs, Type: Emacs,
Key: "Ctrl-d", Key: "Ctrl-d",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -840,7 +840,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: DeleteCharBackward, Name: DeleteCharBackward,
Type: Emacs, Type: Emacs,
Key: "Ctrl-h", Key: "Ctrl-h",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -848,7 +848,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: DeleteToLineEnd, Name: DeleteToLineEnd,
Type: Emacs, Type: Emacs,
Key: "Ctrl-k", Key: "Ctrl-k",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -856,7 +856,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: DeleteGroupBackward, Name: DeleteGroupBackward,
Type: Emacs, Type: Emacs,
Key: "Ctrl-Alt-h", Key: "Ctrl-Alt-h",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -864,7 +864,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: SplitLine, Name: SplitLine,
Type: Emacs, Type: Emacs,
Key: "Ctrl-o", Key: "Ctrl-o",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -872,7 +872,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockTransposeChars, Name: BlockTransposeChars,
Type: Emacs, Type: Emacs,
Key: "Ctrl-t", Key: "Ctrl-t",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -882,7 +882,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockSelectAll, Name: BlockSelectAll,
Type: Emacs, Type: Emacs,
Key: "Mod-Shift-a", Key: "Mod-Shift-a",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },
@@ -890,7 +890,7 @@ func NewDefaultKeyBindings() []KeyBinding {
Name: BlockPaste, Name: BlockPaste,
Type: Emacs, Type: Emacs,
Key: "Mod-Shift-v", Key: "Mod-Shift-v",
Extension: defaultExtension, Extension: DefaultExtension,
Enabled: true, Enabled: true,
PreventDefault: true, PreventDefault: true,
}, },

View File

@@ -6,6 +6,7 @@ import (
"voidraft/internal/models" "voidraft/internal/models"
"voidraft/internal/models/ent" "voidraft/internal/models/ent"
"voidraft/internal/models/ent/extension"
"voidraft/internal/models/ent/keybinding" "voidraft/internal/models/ent/keybinding"
"voidraft/internal/models/schema/mixin" "voidraft/internal/models/schema/mixin"
@@ -112,45 +113,74 @@ func (s *KeyBindingService) SyncKeyBindings(ctx context.Context) error {
// GetKeyBindings 根据类型获取快捷键 // GetKeyBindings 根据类型获取快捷键
func (s *KeyBindingService) GetKeyBindings(ctx context.Context, kbType models.KeyBindingType) ([]*ent.KeyBinding, error) { func (s *KeyBindingService) GetKeyBindings(ctx context.Context, kbType models.KeyBindingType) ([]*ent.KeyBinding, error) {
if kbType == models.Standard { // 获取启用的扩展名称集合
// Standard 模式:只返回 type=standard 且 enabled=true enabledExts, err := s.db.Client.Extension.Query().
return s.db.Client.KeyBinding.Query(). Where(extension.Enabled(true)).
Where(
keybinding.Type(string(kbType)),
keybinding.Enabled(true),
).
All(ctx)
}
// Emacs 模式:获取所有 enabled=true 的快捷键
allEnabled, err := s.db.Client.KeyBinding.Query().
Where(keybinding.Enabled(true)).
All(ctx) All(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("query enabled key bindings error: %w", err) return nil, fmt.Errorf("query enabled extensions error: %w", err)
}
enabledExtMap := make(map[string]bool, len(enabledExts))
for _, ext := range enabledExts {
enabledExtMap[ext.Name] = true
}
if kbType == models.Standard {
// Standard 模式:返回扩展已启用的快捷键
bindings, err := s.db.Client.KeyBinding.Query().
Where(keybinding.Type(string(kbType))).
Order(ent.Asc(keybinding.FieldID)).
All(ctx)
if err != nil {
return nil, err
}
return filterByExtension(bindings, enabledExtMap), nil
}
// Emacs 模式:获取所有快捷键
allBindings, err := s.db.Client.KeyBinding.Query().
Order(ent.Asc(keybinding.FieldID)).
All(ctx)
if err != nil {
return nil, fmt.Errorf("query key bindings error: %w", err)
} }
// 构建 emacs 快捷键的 name 集合 // 构建 emacs 快捷键的 name 集合
emacsNames := make(map[string]bool) emacsNames := make(map[string]bool)
for _, kb := range allEnabled { for _, kb := range allBindings {
if kb.Type == string(models.Emacs) { if kb.Type == string(models.Emacs) {
emacsNames[kb.Name] = true emacsNames[kb.Name] = true
} }
} }
// 过滤:去掉与 emacs 冲突的 standard 快捷键 // 过滤:去掉与 emacs 冲突的 standard 快捷键,并过滤扩展未启用的
var result []*ent.KeyBinding var result []*ent.KeyBinding
for _, kb := range allEnabled { for _, kb := range allBindings {
// 如果是 standard 类型,且与 emacs 有 name 冲突,则跳过
if kb.Type == string(models.Standard) && emacsNames[kb.Name] { if kb.Type == string(models.Standard) && emacsNames[kb.Name] {
continue continue
} }
// editor 扩展始终包含,不检查启用状态
if kb.Extension != models.DefaultExtension && !enabledExtMap[kb.Extension] {
continue
}
result = append(result, kb) result = append(result, kb)
} }
return result, nil return result, nil
} }
// filterByExtension 过滤出扩展已启用的快捷键
func filterByExtension(bindings []*ent.KeyBinding, enabledExtMap map[string]bool) []*ent.KeyBinding {
result := make([]*ent.KeyBinding, 0, len(bindings))
for _, kb := range bindings {
// editor 扩展始终包含,不检查启用状态
if kb.Extension == models.DefaultExtension || enabledExtMap[kb.Extension] {
result = append(result, kb)
}
}
return result
}
// GetKeyBindingByID 根据ID获取快捷键 // GetKeyBindingByID 根据ID获取快捷键
func (s *KeyBindingService) GetKeyBindingByID(ctx context.Context, id int) (*ent.KeyBinding, error) { func (s *KeyBindingService) GetKeyBindingByID(ctx context.Context, id int) (*ent.KeyBinding, error) {
kb, err := s.db.Client.KeyBinding.Get(ctx, id) kb, err := s.db.Client.KeyBinding.Get(ctx, id)
@@ -163,7 +193,7 @@ func (s *KeyBindingService) GetKeyBindingByID(ctx context.Context, id int) (*ent
return kb, nil return kb, nil
} }
// UpdateKeyBindingKeys 更新快捷键绑定(根据操作系统自动判断更新哪个字段) // UpdateKeyBindingKeys 更新快捷键绑定
func (s *KeyBindingService) UpdateKeyBindingKeys(ctx context.Context, id int, key string) error { func (s *KeyBindingService) UpdateKeyBindingKeys(ctx context.Context, id int, key string) error {
kb, err := s.GetKeyBindingByID(ctx, id) kb, err := s.GetKeyBindingByID(ctx, id)
if err != nil { if err != nil {
@@ -204,6 +234,20 @@ func (s *KeyBindingService) UpdateKeyBindingEnabled(ctx context.Context, id int,
Exec(ctx) Exec(ctx)
} }
// UpdateKeyBindingPreventDefault 更新快捷键 PreventDefault 状态
func (s *KeyBindingService) UpdateKeyBindingPreventDefault(ctx context.Context, id int, preventDefault bool) error {
kb, err := s.GetKeyBindingByID(ctx, id)
if err != nil {
return err
}
if kb == nil {
return fmt.Errorf("key binding not found: id=%d", id)
}
return s.db.Client.KeyBinding.UpdateOneID(kb.ID).
SetPreventDefault(preventDefault).
Exec(ctx)
}
// GetDefaultKeyBindings 获取默认快捷键配置 // GetDefaultKeyBindings 获取默认快捷键配置
func (s *KeyBindingService) GetDefaultKeyBindings() []models.KeyBinding { func (s *KeyBindingService) GetDefaultKeyBindings() []models.KeyBinding {
return models.NewDefaultKeyBindings() return models.NewDefaultKeyBindings()

View File

@@ -25,7 +25,7 @@ type SelfUpdateResult struct {
AssetURL string `json:"assetURL"` // 下载链接 AssetURL string `json:"assetURL"` // 下载链接
ReleaseNotes string `json:"releaseNotes"` // 发布说明 ReleaseNotes string `json:"releaseNotes"` // 发布说明
Error string `json:"error"` // 错误信息 Error string `json:"error"` // 错误信息
Source string `json:"source"` // 更新源github/gitea Source string `json:"source"` // 更新源github
} }
// SelfUpdateService 自我更新服务 // SelfUpdateService 自我更新服务
@@ -66,33 +66,6 @@ func (s *SelfUpdateService) CheckForUpdates(ctx context.Context) (*SelfUpdateRes
return nil, err return nil, err
} }
result := &SelfUpdateResult{
CurrentVersion: config.Updates.Version,
HasUpdate: false,
UpdateApplied: false,
}
// 尝试主要更新源
primaryResult, err := s.checkSourceForUpdates(ctx, config.Updates.PrimarySource, config)
if err == nil && primaryResult != nil {
s.handleUpdateBadge(primaryResult)
return primaryResult, nil
}
// 尝试备用更新源
backupResult, backupErr := s.checkSourceForUpdates(ctx, config.Updates.BackupSource, config)
if backupErr != nil {
result.Error = fmt.Sprintf("both sources failed: %v; %v", err, backupErr)
s.handleUpdateBadge(result)
return result, errors.New(result.Error)
}
s.handleUpdateBadge(backupResult)
return backupResult, nil
}
// checkSourceForUpdates 根据更新源类型检查更新
func (s *SelfUpdateService) checkSourceForUpdates(ctx context.Context, sourceType models.UpdateSourceType, config *models.AppConfig) (*SelfUpdateResult, error) {
timeout := config.Updates.UpdateTimeout timeout := config.Updates.UpdateTimeout
if timeout <= 0 { if timeout <= 0 {
timeout = 30 timeout = 30
@@ -104,28 +77,21 @@ func (s *SelfUpdateService) checkSourceForUpdates(ctx context.Context, sourceTyp
CurrentVersion: config.Updates.Version, CurrentVersion: config.Updates.Version,
HasUpdate: false, HasUpdate: false,
UpdateApplied: false, UpdateApplied: false,
Source: string(sourceType), Source: "github",
}
var release *selfupdate.Release
var found bool
var err error
switch sourceType {
case models.UpdateSourceGithub:
release, found, err = s.checkGithubUpdates(timeoutCtx, config)
case models.UpdateSourceGitea:
release, found, err = s.checkGiteaUpdates(timeoutCtx, config)
default:
return nil, fmt.Errorf("unsupported source: %s", sourceType)
} }
// 检查 GitHub 更新
release, found, err := s.checkGithubUpdates(timeoutCtx, config)
if err != nil { if err != nil {
return result, fmt.Errorf("check failed: %w", err) result.Error = fmt.Sprintf("check github updates failed: %v", err)
s.handleUpdateBadge(result)
return result, err
} }
if !found { if !found {
return result, fmt.Errorf("no release for %s/%s", runtime.GOOS, runtime.GOARCH) result.Error = fmt.Sprintf("no release for %s/%s", runtime.GOOS, runtime.GOARCH)
s.handleUpdateBadge(result)
return result, errors.New(result.Error)
} }
result.LatestVersion = release.Version() result.LatestVersion = release.Version()
@@ -133,6 +99,7 @@ func (s *SelfUpdateService) checkSourceForUpdates(ctx context.Context, sourceTyp
result.ReleaseNotes = release.ReleaseNotes result.ReleaseNotes = release.ReleaseNotes
result.HasUpdate = release.GreaterThan(config.Updates.Version) result.HasUpdate = release.GreaterThan(config.Updates.Version)
s.handleUpdateBadge(result)
return result, nil return result, nil
} }
@@ -141,18 +108,6 @@ func (s *SelfUpdateService) createGithubUpdater() (*selfupdate.Updater, error) {
return selfupdate.NewUpdater(selfupdate.Config{}) return selfupdate.NewUpdater(selfupdate.Config{})
} }
// createGiteaUpdater 创建Gitea更新器
func (s *SelfUpdateService) createGiteaUpdater(config *models.AppConfig) (*selfupdate.Updater, error) {
source, err := selfupdate.NewGiteaSource(selfupdate.GiteaConfig{
BaseURL: config.Updates.Gitea.BaseURL,
})
if err != nil {
return nil, fmt.Errorf("create gitea source failed: %w", err)
}
return selfupdate.NewUpdater(selfupdate.Config{Source: source})
}
// checkGithubUpdates 检查GitHub更新 // checkGithubUpdates 检查GitHub更新
func (s *SelfUpdateService) checkGithubUpdates(ctx context.Context, config *models.AppConfig) (*selfupdate.Release, bool, error) { func (s *SelfUpdateService) checkGithubUpdates(ctx context.Context, config *models.AppConfig) (*selfupdate.Release, bool, error) {
updater, err := s.createGithubUpdater() updater, err := s.createGithubUpdater()
@@ -164,17 +119,6 @@ func (s *SelfUpdateService) checkGithubUpdates(ctx context.Context, config *mode
return updater.DetectLatest(ctx, repo) return updater.DetectLatest(ctx, repo)
} }
// checkGiteaUpdates 检查Gitea更新
func (s *SelfUpdateService) checkGiteaUpdates(ctx context.Context, config *models.AppConfig) (*selfupdate.Release, bool, error) {
updater, err := s.createGiteaUpdater(config)
if err != nil {
return nil, false, err
}
repo := selfupdate.NewRepositorySlug(config.Updates.Gitea.Owner, config.Updates.Gitea.Repo)
return updater.DetectLatest(ctx, repo)
}
// ApplyUpdate 应用更新 // ApplyUpdate 应用更新
func (s *SelfUpdateService) ApplyUpdate(ctx context.Context) (*SelfUpdateResult, error) { func (s *SelfUpdateService) ApplyUpdate(ctx context.Context) (*SelfUpdateResult, error) {
s.mu.Lock() s.mu.Lock()
@@ -201,23 +145,17 @@ func (s *SelfUpdateService) ApplyUpdate(ctx context.Context) (*SelfUpdateResult,
return nil, fmt.Errorf("locate executable failed: %w", err) return nil, fmt.Errorf("locate executable failed: %w", err)
} }
// 尝试主要源 // 执行 GitHub 更新
result, err := s.performUpdate(ctx, config.Updates.PrimarySource, exe, config) result, err := s.performUpdate(ctx, exe, config)
if err == nil {
return result, nil
}
// 尝试备用源
result, err = s.performUpdate(ctx, config.Updates.BackupSource, exe, config)
if err != nil { if err != nil {
return nil, fmt.Errorf("update failed from both sources: %w", err) return nil, fmt.Errorf("update failed: %w", err)
} }
return result, nil return result, nil
} }
// performUpdate 执行更新操作(包括检测、备份、下载、应用) // performUpdate 执行更新操作(包括检测、备份、下载、应用)
func (s *SelfUpdateService) performUpdate(ctx context.Context, sourceType models.UpdateSourceType, exe string, config *models.AppConfig) (*SelfUpdateResult, error) { func (s *SelfUpdateService) performUpdate(ctx context.Context, exe string, config *models.AppConfig) (*SelfUpdateResult, error) {
timeout := config.Updates.UpdateTimeout timeout := config.Updates.UpdateTimeout
if timeout <= 0 { if timeout <= 0 {
timeout = 30 timeout = 30
@@ -225,8 +163,13 @@ func (s *SelfUpdateService) performUpdate(ctx context.Context, sourceType models
checkCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second) checkCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
defer cancel() defer cancel()
// 获取更新器和版本信息 // 获取 GitHub 更新信息
updater, release, found, err := s.getUpdateFromSource(checkCtx, sourceType, config) updater, err := s.createGithubUpdater()
if err != nil {
return nil, fmt.Errorf("create github updater failed: %w", err)
}
release, found, err := s.checkGithubUpdates(checkCtx, config)
if err != nil || !found { if err != nil || !found {
return nil, fmt.Errorf("detect release failed: %w", err) return nil, fmt.Errorf("detect release failed: %w", err)
} }
@@ -236,7 +179,7 @@ func (s *SelfUpdateService) performUpdate(ctx context.Context, sourceType models
LatestVersion: release.Version(), LatestVersion: release.Version(),
AssetURL: release.AssetURL, AssetURL: release.AssetURL,
ReleaseNotes: release.ReleaseNotes, ReleaseNotes: release.ReleaseNotes,
Source: string(sourceType), Source: "github",
HasUpdate: release.GreaterThan(config.Updates.Version), HasUpdate: release.GreaterThan(config.Updates.Version),
} }
@@ -269,33 +212,6 @@ func (s *SelfUpdateService) performUpdate(ctx context.Context, sourceType models
return result, nil return result, nil
} }
// getUpdateFromSource 从指定源获取更新信息
func (s *SelfUpdateService) getUpdateFromSource(ctx context.Context, sourceType models.UpdateSourceType, config *models.AppConfig) (*selfupdate.Updater, *selfupdate.Release, bool, error) {
var updater *selfupdate.Updater
var release *selfupdate.Release
var found bool
var err error
switch sourceType {
case models.UpdateSourceGithub:
updater, err = s.createGithubUpdater()
if err != nil {
return nil, nil, false, err
}
release, found, err = s.checkGithubUpdates(ctx, config)
case models.UpdateSourceGitea:
updater, err = s.createGiteaUpdater(config)
if err != nil {
return nil, nil, false, err
}
release, found, err = s.checkGiteaUpdates(ctx, config)
default:
return nil, nil, false, fmt.Errorf("unsupported source: %s", sourceType)
}
return updater, release, found, err
}
// handleUpdateSuccess 处理更新成功后的操作 // handleUpdateSuccess 处理更新成功后的操作
func (s *SelfUpdateService) handleUpdateSuccess(result *SelfUpdateResult) { func (s *SelfUpdateService) handleUpdateSuccess(result *SelfUpdateResult) {
// 更新配置版本 // 更新配置版本

View File

@@ -1 +1 @@
VERSION=1.5.5 VERSION=1.5.6