Compare commits
16 Commits
67d35626cb
...
v1.5.5
| Author | SHA1 | Date | |
|---|---|---|---|
| 541e4e96cf | |||
| 401eb3ab39 | |||
| d3eba96a29 | |||
| 81c02db00d | |||
|
|
9cb2ccbb4e | ||
| 8a10b8fe0f | |||
| 8fce8bdca4 | |||
| 1ab934cee9 | |||
| 6659ac6fad | |||
| 3a5ab1c614 | |||
| 1e07e1f833 | |||
| e1e91a3683 | |||
| c30d95a3e0 | |||
| 97f6fa843c | |||
| f43fc47539 | |||
|
|
c330de52fa |
3
.github/workflows/build-release.yml
vendored
3
.github/workflows/build-release.yml
vendored
@@ -28,6 +28,7 @@ env:
|
||||
jobs:
|
||||
# 准备构建配置
|
||||
prepare:
|
||||
permissions: {}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
@@ -84,6 +85,8 @@ jobs:
|
||||
fi
|
||||
|
||||
build:
|
||||
permissions:
|
||||
contents: read
|
||||
needs: prepare
|
||||
if: ${{ fromJson(needs.prepare.outputs.matrix).include[0] != null }}
|
||||
strategy:
|
||||
|
||||
109
.github/workflows/codeql.yml
vendored
Normal file
109
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: '29 8 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
build-mode: none
|
||||
- language: c-cpp
|
||||
build-mode: none
|
||||
- language: go
|
||||
build-mode: autobuild
|
||||
- language: javascript-typescript
|
||||
build-mode: none
|
||||
- language: python
|
||||
build-mode: none
|
||||
- language: rust
|
||||
build-mode: none
|
||||
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||
# or others). This is typically only required for manual builds.
|
||||
# - name: Setup runtime (example)
|
||||
# uses: actions/setup-example@v1
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- name: Run manual build steps
|
||||
if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
@@ -159,5 +159,8 @@ Welcome to Fork, Star, and contribute code.
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/yourusername/voidraft)
|
||||
[](https://github.com/yourusername/voidraft)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Flandaiqing%2Fvoidraft?ref=badge_shield)
|
||||
|
||||
*Made with ❤️ by landaiqing*
|
||||
*Made with ❤️ by landaiqing*
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Flandaiqing%2Fvoidraft?ref=badge_large)
|
||||
@@ -61,5 +61,3 @@ export class ServiceOptions {
|
||||
return new ServiceOptions($$parsedSource as Partial<ServiceOptions>);
|
||||
}
|
||||
}
|
||||
|
||||
export type Window = any;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export * from "./models.js";
|
||||
17
frontend/bindings/voidraft/internal/common/helper/models.ts
Normal file
17
frontend/bindings/voidraft/internal/common/helper/models.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* CancelFunc 取消订阅函数
|
||||
* 调用此函数可以取消对配置的监听
|
||||
*/
|
||||
export type CancelFunc = any;
|
||||
|
||||
/**
|
||||
* ObserverCallback 观察者回调函数
|
||||
*/
|
||||
export type ObserverCallback = any;
|
||||
@@ -21,7 +21,7 @@ export class Document {
|
||||
/**
|
||||
* UUID for cross-device sync (UUIDv7)
|
||||
*/
|
||||
"uuid": string;
|
||||
"uuid": string | null;
|
||||
|
||||
/**
|
||||
* creation time
|
||||
@@ -56,7 +56,7 @@ export class Document {
|
||||
/** Creates a new Document instance. */
|
||||
constructor($$source: Partial<Document> = {}) {
|
||||
if (!("uuid" in $$source)) {
|
||||
this["uuid"] = "";
|
||||
this["uuid"] = null;
|
||||
}
|
||||
if (!("created_at" in $$source)) {
|
||||
this["created_at"] = "";
|
||||
@@ -98,7 +98,7 @@ export class Extension {
|
||||
/**
|
||||
* UUID for cross-device sync (UUIDv7)
|
||||
*/
|
||||
"uuid": string;
|
||||
"uuid": string | null;
|
||||
|
||||
/**
|
||||
* creation time
|
||||
@@ -133,7 +133,7 @@ export class Extension {
|
||||
/** Creates a new Extension instance. */
|
||||
constructor($$source: Partial<Extension> = {}) {
|
||||
if (!("uuid" in $$source)) {
|
||||
this["uuid"] = "";
|
||||
this["uuid"] = null;
|
||||
}
|
||||
if (!("created_at" in $$source)) {
|
||||
this["created_at"] = "";
|
||||
@@ -179,7 +179,7 @@ export class KeyBinding {
|
||||
/**
|
||||
* UUID for cross-device sync (UUIDv7)
|
||||
*/
|
||||
"uuid": string;
|
||||
"uuid": string | null;
|
||||
|
||||
/**
|
||||
* creation time
|
||||
@@ -219,7 +219,7 @@ export class KeyBinding {
|
||||
/** Creates a new KeyBinding instance. */
|
||||
constructor($$source: Partial<KeyBinding> = {}) {
|
||||
if (!("uuid" in $$source)) {
|
||||
this["uuid"] = "";
|
||||
this["uuid"] = null;
|
||||
}
|
||||
if (!("created_at" in $$source)) {
|
||||
this["created_at"] = "";
|
||||
@@ -261,7 +261,7 @@ export class Theme {
|
||||
/**
|
||||
* UUID for cross-device sync (UUIDv7)
|
||||
*/
|
||||
"uuid": string;
|
||||
"uuid": string | null;
|
||||
|
||||
/**
|
||||
* creation time
|
||||
@@ -296,7 +296,7 @@ export class Theme {
|
||||
/** Creates a new Theme instance. */
|
||||
constructor($$source: Partial<Theme> = {}) {
|
||||
if (!("uuid" in $$source)) {
|
||||
this["uuid"] = "";
|
||||
this["uuid"] = null;
|
||||
}
|
||||
if (!("created_at" in $$source)) {
|
||||
this["created_at"] = "";
|
||||
|
||||
@@ -15,11 +15,10 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
import * as helper$0 from "../common/helper/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
/**
|
||||
* Get 获取配置项
|
||||
@@ -50,7 +49,7 @@ export function MigrateConfig(): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetConfig 强制重置所有配置为默认值
|
||||
* ResetConfig 重置所有配置为默认值
|
||||
*/
|
||||
export function ResetConfig(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3593047389) as any;
|
||||
@@ -66,7 +65,7 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup initializes the service when the application starts
|
||||
* ServiceStartup 服务启动时初始化
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3311949428, options) as any;
|
||||
@@ -84,7 +83,7 @@ export function Set(key: string, value: any): Promise<void> & { cancel(): void }
|
||||
/**
|
||||
* Watch 注册配置变更监听器
|
||||
*/
|
||||
export function Watch(path: string, callback: $models.ObserverCallback): Promise<$models.CancelFunc> & { cancel(): void } {
|
||||
export function Watch(path: string, callback: helper$0.ObserverCallback): Promise<helper$0.CancelFunc> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1143583035, path, callback) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
@@ -92,7 +91,7 @@ export function Watch(path: string, callback: $models.ObserverCallback): Promise
|
||||
/**
|
||||
* WatchWithContext 使用 Context 注册监听器
|
||||
*/
|
||||
export function WatchWithContext(path: string, callback: $models.ObserverCallback): Promise<void> & { cancel(): void } {
|
||||
export function WatchWithContext(path: string, callback: helper$0.ObserverCallback): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1454973098, path, callback) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
/**
|
||||
* GetCurrentHotkey 获取当前热键
|
||||
* GetSupportedKeys 返回系统支持的快捷键列表
|
||||
*/
|
||||
export function GetCurrentHotkey(): Promise<models$0.HotkeyCombo | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2572811187) as any;
|
||||
export function GetSupportedKeys(): Promise<string[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1511528650) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
return $$createType0($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
@@ -86,5 +86,4 @@ export function UpdateHotkey(enable: boolean, combo: models$0.HotkeyCombo | null
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.HotkeyCombo.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType0 = $Create.Array($Create.Any);
|
||||
|
||||
@@ -12,12 +12,6 @@ import * as http$0 from "../../../net/http/models.js";
|
||||
// @ts-ignore: Unused imports
|
||||
import * as time$0 from "../../../time/models.js";
|
||||
|
||||
/**
|
||||
* CancelFunc 取消订阅函数
|
||||
* 调用此函数可以取消对配置的监听
|
||||
*/
|
||||
export type CancelFunc = any;
|
||||
|
||||
/**
|
||||
* HttpRequest HTTP请求结构
|
||||
*/
|
||||
@@ -251,11 +245,6 @@ export class OSInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ObserverCallback 观察者回调函数
|
||||
*/
|
||||
export type ObserverCallback = any;
|
||||
|
||||
/**
|
||||
* SelfUpdateResult 自我更新结果
|
||||
*/
|
||||
|
||||
@@ -14,18 +14,6 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
// @ts-ignore: Unused imports
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
|
||||
/**
|
||||
* GetOpenWindows 获取所有打开的文档窗口
|
||||
*/
|
||||
export function GetOpenWindows(): Promise<application$0.Window[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1464997251) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* IsDocumentWindowOpen 检查指定文档的窗口是否已打开
|
||||
*/
|
||||
@@ -57,6 +45,3 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
|
||||
let $resultPromise = $Call.ByID(2432987694, options) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $Create.Array($Create.Any);
|
||||
|
||||
850
frontend/package-lock.json
generated
850
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.20.0",
|
||||
"@codemirror/commands": "^6.10.0",
|
||||
"@codemirror/commands": "^6.10.1",
|
||||
"@codemirror/lang-angular": "^0.1.4",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
@@ -34,7 +34,7 @@
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-less": "^6.0.2",
|
||||
"@codemirror/lang-lezer": "^6.0.2",
|
||||
"@codemirror/lang-liquid": "^6.3.0",
|
||||
"@codemirror/lang-liquid": "^6.3.1",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-php": "^6.0.2",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
@@ -50,10 +50,10 @@
|
||||
"@codemirror/lint": "^6.9.2",
|
||||
"@codemirror/search": "^6.5.11",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.38.8",
|
||||
"@codemirror/view": "^6.39.4",
|
||||
"@cospaia/prettier-plugin-clojure": "^0.0.2",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@lezer/lr": "^1.4.4",
|
||||
"@lezer/lr": "^1.4.5",
|
||||
"@prettier/plugin-xml": "^3.4.2",
|
||||
"@replit/codemirror-lang-svelte": "^6.0.0",
|
||||
"@toml-tools/lexer": "^1.0.0",
|
||||
@@ -61,45 +61,45 @@
|
||||
"@types/katex": "^0.16.7",
|
||||
"codemirror": "^6.0.2",
|
||||
"codemirror-lang-elixir": "^4.0.0",
|
||||
"colors-named": "^1.0.2",
|
||||
"colors-named-hex": "^1.0.2",
|
||||
"colors-named": "^1.0.4",
|
||||
"colors-named-hex": "^1.0.3",
|
||||
"groovy-beautify": "^0.0.17",
|
||||
"hsl-matcher": "^1.2.4",
|
||||
"java-parser": "^3.0.1",
|
||||
"katex": "^0.16.25",
|
||||
"linguist-languages": "^9.1.0",
|
||||
"katex": "^0.16.27",
|
||||
"linguist-languages": "^9.1.11",
|
||||
"marked": "^17.0.1",
|
||||
"mermaid": "^11.12.1",
|
||||
"mermaid": "^11.12.2",
|
||||
"php-parser": "^3.2.5",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"prettier": "^3.7.2",
|
||||
"sass": "^1.94.2",
|
||||
"prettier": "^3.7.4",
|
||||
"sass": "^1.97.0",
|
||||
"vue": "^3.5.25",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue-pick-colors": "^1.8.0",
|
||||
"vue-router": "^4.6.3"
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@lezer/generator": "^1.8.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@wailsio/runtime": "latest",
|
||||
"@types/node": "^25.0.3",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"@wailsio/runtime": "^3.0.0-alpha.76",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-vue": "^10.6.2",
|
||||
"globals": "^16.5.0",
|
||||
"happy-dom": "^20.0.11",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.48.0",
|
||||
"typescript-eslint": "^8.50.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"vitepress": "^2.0.0-alpha.12",
|
||||
"vitest": "^4.0.14",
|
||||
"vitest": "^4.0.16",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.1.5"
|
||||
"vue-tsc": "^3.1.8"
|
||||
},
|
||||
"overrides": {
|
||||
"vite": "npm:rolldown-vite@latest"
|
||||
|
||||
@@ -248,7 +248,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||
setFontWeight: (value: string) => updateConfig('fontWeight', value),
|
||||
|
||||
// 路径操作
|
||||
setDataPath: (value: string) => updateConfig('dataPath', value),
|
||||
setDataPath: (value: string) => updateConfigLocal('dataPath', value),
|
||||
|
||||
// 保存配置相关方法
|
||||
setAutoSaveDelay: (value: number) => updateConfig('autoSaveDelay', value),
|
||||
|
||||
@@ -70,12 +70,11 @@ export const useDocumentStore = defineStore('document', () => {
|
||||
// 在新窗口中打开文档
|
||||
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
|
||||
try {
|
||||
await OpenDocumentWindow(docId);
|
||||
const tabStore = useTabStore();
|
||||
if (tabStore.isTabsEnabled && tabStore.hasTab(docId)) {
|
||||
tabStore.closeTab(docId);
|
||||
}
|
||||
|
||||
await OpenDocumentWindow(docId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to open document in new window:', error);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/b
|
||||
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
|
||||
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
|
||||
import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension';
|
||||
import {createCursorPositionExtension, scrollToCursor} from '@/views/editor/basic/cursorPositionExtension';
|
||||
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
|
||||
import {
|
||||
createDynamicExtensions,
|
||||
@@ -21,7 +22,7 @@ import {
|
||||
setExtensionManagerView
|
||||
} from '@/views/editor/manager';
|
||||
import {useExtensionStore} from './extensionStore';
|
||||
import createCodeBlockExtension, {blockState} from "@/views/editor/extensions/codeblock";
|
||||
import createCodeBlockExtension from "@/views/editor/extensions/codeblock";
|
||||
import {LruCache} from '@/common/utils/lruCache';
|
||||
import {AsyncManager} from '@/common/utils/asyncManager';
|
||||
import {generateContentHash} from "@/common/utils/hashUtils";
|
||||
@@ -35,7 +36,7 @@ export interface DocumentStats {
|
||||
selectedCharacters: number;
|
||||
}
|
||||
|
||||
// 修复:只保存光标位置,恢复时自动滚动到光标处(更简单可靠)
|
||||
// 修复:只保存光标位置,恢复时自动滚动到光标处
|
||||
export interface EditorViewState {
|
||||
cursorPos: number;
|
||||
}
|
||||
@@ -180,6 +181,9 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
enableAutoDetection: true
|
||||
});
|
||||
|
||||
// 光标位置持久化扩展
|
||||
const cursorPositionExtension = createCursorPositionExtension(documentId);
|
||||
|
||||
// 再次检查操作有效性
|
||||
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||
throw new Error('Operation cancelled');
|
||||
@@ -212,13 +216,23 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
statsExtension,
|
||||
contentChangeExtension,
|
||||
codeBlockExtension,
|
||||
cursorPositionExtension,
|
||||
...dynamicExtensions,
|
||||
];
|
||||
|
||||
// 创建编辑器状态
|
||||
// 获取保存的光标位置
|
||||
const savedState = documentStore.documentStates[documentId];
|
||||
const docLength = content.length;
|
||||
const initialCursorPos = savedState?.cursorPos !== undefined
|
||||
? Math.min(savedState.cursorPos, docLength)
|
||||
: docLength;
|
||||
|
||||
|
||||
// 创建编辑器状态,设置初始光标位置
|
||||
const state = EditorState.create({
|
||||
doc: content,
|
||||
extensions
|
||||
extensions,
|
||||
selection: { anchor: initialCursorPos, head: initialCursorPos }
|
||||
});
|
||||
|
||||
return new EditorView({
|
||||
@@ -316,6 +330,9 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
//使用 nextTick + requestAnimationFrame 确保 DOM 完全渲染
|
||||
nextTick(() => {
|
||||
requestAnimationFrame(() => {
|
||||
// 滚动到当前光标位置
|
||||
scrollToCursor(instance.view);
|
||||
|
||||
// 聚焦编辑器
|
||||
instance.view.focus();
|
||||
|
||||
@@ -487,15 +504,6 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
await saveEditorContent(documentId);
|
||||
}
|
||||
|
||||
// 保存光标位置
|
||||
if (instance.view && instance.view.state) {
|
||||
const currentState: EditorViewState = {
|
||||
cursorPos: instance.view.state.selection.main.head
|
||||
};
|
||||
// 保存到 documentStore 用于持久化
|
||||
documentStore.documentStates[documentId] = currentState;
|
||||
}
|
||||
|
||||
// 清除自动保存定时器
|
||||
instance.autoSaveTimer.clear();
|
||||
|
||||
@@ -578,22 +586,10 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
operationManager.cancelAllOperations();
|
||||
|
||||
editorCache.clear((_documentId, instance) => {
|
||||
// 修复:清空前只保存光标位置
|
||||
if (instance.view) {
|
||||
const currentState: EditorViewState = {
|
||||
cursorPos: instance.view.state.selection.main.head
|
||||
};
|
||||
// 同时保存到实例和 documentStore
|
||||
instance.editorState = currentState;
|
||||
documentStore.documentStates[instance.documentId] = currentState;
|
||||
}
|
||||
|
||||
// 清除自动保存定时器
|
||||
instance.autoSaveTimer.clear();
|
||||
|
||||
// 从扩展管理器移除
|
||||
removeExtensionManagerView(instance.documentId);
|
||||
|
||||
// 移除DOM元素
|
||||
if (instance.view.dom.parentElement) {
|
||||
instance.view.dom.remove();
|
||||
|
||||
@@ -156,10 +156,10 @@ export const useTabStore = defineStore('tab', () => {
|
||||
*/
|
||||
const validateTabs = () => {
|
||||
const validDocIds = Object.keys(documentStore.documents).map(Number);
|
||||
|
||||
|
||||
// 找出无效的标签页(文档已被删除)
|
||||
const invalidTabIds = tabOrder.value.filter(docId => !validDocIds.includes(docId));
|
||||
|
||||
|
||||
if (invalidTabIds.length > 0) {
|
||||
// 批量清理无效标签页
|
||||
invalidTabIds.forEach(docId => {
|
||||
@@ -175,7 +175,7 @@ export const useTabStore = defineStore('tab', () => {
|
||||
const initializeTab = () => {
|
||||
// 先验证并清理无效的标签页(处理持久化的脏数据)
|
||||
validateTabs();
|
||||
|
||||
|
||||
if (isTabsEnabled.value) {
|
||||
const currentDoc = documentStore.currentDocument;
|
||||
if (currentDoc) {
|
||||
@@ -254,7 +254,7 @@ export const useTabStore = defineStore('tab', () => {
|
||||
return {
|
||||
tabsMap,
|
||||
tabOrder,
|
||||
|
||||
|
||||
// 状态
|
||||
tabs: readonly(tabs),
|
||||
draggedTabId,
|
||||
@@ -283,9 +283,5 @@ export const useTabStore = defineStore('tab', () => {
|
||||
getTab
|
||||
};
|
||||
}, {
|
||||
persist: {
|
||||
key: 'voidraft-tabs',
|
||||
storage: localStorage,
|
||||
pick: ['tabsMap', 'tabOrder'],
|
||||
},
|
||||
persist: false,
|
||||
});
|
||||
|
||||
68
frontend/src/views/editor/basic/cursorPositionExtension.ts
Normal file
68
frontend/src/views/editor/basic/cursorPositionExtension.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import {EditorView, ViewPlugin, ViewUpdate} from '@codemirror/view';
|
||||
import {useDocumentStore} from '@/stores/documentStore';
|
||||
import {createDebounce} from '@/common/utils/debounce';
|
||||
|
||||
/**
|
||||
* 光标位置持久化扩展
|
||||
* 实时监听光标位置变化并持久化到 documentStore
|
||||
*/
|
||||
export function createCursorPositionExtension(documentId: number) {
|
||||
return ViewPlugin.fromClass(
|
||||
class CursorPositionPlugin {
|
||||
private readonly documentStore = useDocumentStore();
|
||||
private readonly debouncedSave;
|
||||
|
||||
constructor(private view: EditorView) {
|
||||
const {debouncedFn, flush} = createDebounce(
|
||||
() => this.saveCursorPosition(),
|
||||
{delay: 400}
|
||||
);
|
||||
this.debouncedSave = {fn: debouncedFn, flush};
|
||||
|
||||
// 初始化时保存一次光标位置
|
||||
this.saveCursorPosition();
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
// 只在选择变化时触发
|
||||
if (!update.selectionSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 防抖保存光标位置
|
||||
this.debouncedSave.fn();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// 销毁时立即执行待保存的操作
|
||||
this.debouncedSave.flush();
|
||||
// 再保存一次确保最新状态
|
||||
this.saveCursorPosition();
|
||||
}
|
||||
|
||||
private saveCursorPosition() {
|
||||
const cursorPos = this.view.state.selection.main.head;
|
||||
if (!this.documentStore.documentStates[documentId]) {
|
||||
this.documentStore.documentStates[documentId] = {cursorPos};
|
||||
} else {
|
||||
this.documentStore.documentStates[documentId].cursorPos = cursorPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到当前光标位置(视口中心)
|
||||
* @param view 编辑器视图
|
||||
*/
|
||||
export function scrollToCursor(view: EditorView) {
|
||||
const cursorPos = view.state.selection.main.head;
|
||||
view.dispatch({
|
||||
effects: EditorView.scrollIntoView(cursorPos, {
|
||||
y: 'center',
|
||||
x: 'center'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,9 +26,7 @@ function formatKeyBinding(keyBinding: string): string {
|
||||
|
||||
return keyBinding
|
||||
.replace("Mod", isMac ? "Cmd" : "Ctrl")
|
||||
.replace("Shift", "Shift")
|
||||
.replace("Alt", isMac ? "Option" : "Alt")
|
||||
.replace("Ctrl", isMac ? "Ctrl" : "Ctrl")
|
||||
.replace(/-/g, " + ");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,28 @@
|
||||
import {useConfigStore} from '@/stores/configStore';
|
||||
import {useTabStore} from '@/stores/tabStore';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {computed, ref} from 'vue';
|
||||
import {computed, ref, onMounted} from 'vue';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
import SettingItem from '../components/SettingItem.vue';
|
||||
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||
import {DialogService, MigrationService} from '@/../bindings/voidraft/internal/services';
|
||||
import {DialogService, HotkeyService, MigrationService} from '@/../bindings/voidraft/internal/services';
|
||||
import {useSystemStore} from "@/stores/systemStore";
|
||||
import {useConfirm, usePolling} from '@/composables';
|
||||
|
||||
const {t} = useI18n();
|
||||
const configStore = useConfigStore();
|
||||
const {
|
||||
config: {general},
|
||||
resetConfig,
|
||||
setAlwaysOnTop,
|
||||
setDataPath,
|
||||
setEnableGlobalHotkey,
|
||||
setEnableLoadingAnimation,
|
||||
setEnableSystemTray,
|
||||
setEnableTabs,
|
||||
setEnableWindowSnap,
|
||||
setGlobalHotkey,
|
||||
setStartAtLogin
|
||||
} = useConfigStore();
|
||||
const systemStore = useSystemStore();
|
||||
const tabStore = useTabStore();
|
||||
|
||||
@@ -60,57 +72,57 @@ const hideAll = () => {
|
||||
const {isConfirming: isResetConfirming, requestConfirm: requestResetConfirm} = useConfirm({
|
||||
timeout: 3000,
|
||||
onConfirm: async () => {
|
||||
await configStore.resetConfig();
|
||||
await resetConfig();
|
||||
}
|
||||
});
|
||||
|
||||
// 可选键列表
|
||||
const keyOptions = [
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
|
||||
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12'
|
||||
];
|
||||
// 可选键列表 - 从后端获取系统支持的快捷键
|
||||
const keyOptions = ref<string[]>([]);
|
||||
|
||||
// 初始化时从后端获取支持的键列表
|
||||
onMounted(async () => {
|
||||
keyOptions.value = await HotkeyService.GetSupportedKeys();
|
||||
});
|
||||
|
||||
// 计算属性 - 启用全局热键
|
||||
const enableGlobalHotkey = computed({
|
||||
get: () => configStore.config.general.enableGlobalHotkey,
|
||||
set: (value: boolean) => configStore.setEnableGlobalHotkey(value)
|
||||
get: () => general.enableGlobalHotkey,
|
||||
set: (value: boolean) => setEnableGlobalHotkey(value)
|
||||
});
|
||||
|
||||
// 计算属性 - 窗口始终置顶
|
||||
const alwaysOnTop = computed({
|
||||
get: () => configStore.config.general.alwaysOnTop,
|
||||
get: () => general.alwaysOnTop,
|
||||
set: async (value: boolean) => {
|
||||
// 先更新配置
|
||||
await configStore.setAlwaysOnTop(value);
|
||||
await setAlwaysOnTop(value);
|
||||
await systemStore.setWindowOnTop(value);
|
||||
}
|
||||
});
|
||||
|
||||
// 计算属性 - 启用系统托盘
|
||||
const enableSystemTray = computed({
|
||||
get: () => configStore.config.general.enableSystemTray,
|
||||
set: (value: boolean) => configStore.setEnableSystemTray(value)
|
||||
get: () => general.enableSystemTray,
|
||||
set: (value: boolean) => setEnableSystemTray(value)
|
||||
});
|
||||
|
||||
// 计算属性 - 启用窗口吸附
|
||||
const enableWindowSnap = computed({
|
||||
get: () => configStore.config.general.enableWindowSnap,
|
||||
set: (value: boolean) => configStore.setEnableWindowSnap(value)
|
||||
get: () => general.enableWindowSnap,
|
||||
set: (value: boolean) => setEnableWindowSnap(value)
|
||||
});
|
||||
|
||||
// 计算属性 - 启用加载动画
|
||||
const enableLoadingAnimation = computed({
|
||||
get: () => configStore.config.general.enableLoadingAnimation,
|
||||
set: (value: boolean) => configStore.setEnableLoadingAnimation(value)
|
||||
get: () => general.enableLoadingAnimation,
|
||||
set: (value: boolean) => setEnableLoadingAnimation(value)
|
||||
});
|
||||
|
||||
// 计算属性 - 启用标签页
|
||||
const enableTabs = computed({
|
||||
get: () => configStore.config.general.enableTabs,
|
||||
get: () => general.enableTabs,
|
||||
set: async (value: boolean) => {
|
||||
await configStore.setEnableTabs(value);
|
||||
await setEnableTabs(value);
|
||||
if (value) {
|
||||
// 开启tabs功能时,初始化当前文档到标签页
|
||||
tabStore.initializeTab();
|
||||
@@ -123,33 +135,33 @@ const enableTabs = computed({
|
||||
|
||||
// 计算属性 - 开机启动
|
||||
const startAtLogin = computed({
|
||||
get: () => configStore.config.general.startAtLogin,
|
||||
set: (value: boolean) => configStore.setStartAtLogin(value)
|
||||
get: () => general.startAtLogin,
|
||||
set: (value: boolean) => setStartAtLogin(value)
|
||||
});
|
||||
|
||||
// 修饰键配置 - 只读计算属性
|
||||
const modifierKeys = computed(() => ({
|
||||
ctrl: configStore.config.general.globalHotkey.ctrl,
|
||||
shift: configStore.config.general.globalHotkey.shift,
|
||||
alt: configStore.config.general.globalHotkey.alt,
|
||||
win: configStore.config.general.globalHotkey.win
|
||||
ctrl: general.globalHotkey.ctrl,
|
||||
shift: general.globalHotkey.shift,
|
||||
alt: general.globalHotkey.alt,
|
||||
win: general.globalHotkey.win
|
||||
}));
|
||||
|
||||
// 主键配置
|
||||
const selectedKey = computed(() => configStore.config.general.globalHotkey.key);
|
||||
const selectedKey = computed(() => general.globalHotkey.key);
|
||||
|
||||
// 切换修饰键
|
||||
const toggleModifier = (key: 'ctrl' | 'shift' | 'alt' | 'win') => {
|
||||
const currentHotkey = configStore.config.general.globalHotkey;
|
||||
const currentHotkey = general.globalHotkey;
|
||||
const newHotkey = {...currentHotkey, [key]: !currentHotkey[key]};
|
||||
configStore.setGlobalHotkey(newHotkey);
|
||||
setGlobalHotkey(newHotkey);
|
||||
};
|
||||
|
||||
// 更新选择的键
|
||||
const updateSelectedKey = (event: Event) => {
|
||||
const select = event.target as HTMLSelectElement;
|
||||
const newHotkey = {...configStore.config.general.globalHotkey, key: select.value};
|
||||
configStore.setGlobalHotkey(newHotkey);
|
||||
const newHotkey = {...general.globalHotkey, key: select.value};
|
||||
setGlobalHotkey(newHotkey);
|
||||
};
|
||||
|
||||
|
||||
@@ -157,7 +169,7 @@ const updateSelectedKey = (event: Event) => {
|
||||
const hotkeyPreview = computed(() => {
|
||||
if (!enableGlobalHotkey.value) return '';
|
||||
|
||||
const {ctrl, shift, alt, win, key} = configStore.config.general.globalHotkey;
|
||||
const {ctrl, shift, alt, win, key} = general.globalHotkey;
|
||||
const modifiers = [
|
||||
ctrl && 'Ctrl',
|
||||
shift && 'Shift',
|
||||
@@ -170,7 +182,7 @@ const hotkeyPreview = computed(() => {
|
||||
});
|
||||
|
||||
// 数据路径配置
|
||||
const currentDataPath = computed(() => configStore.config.general.dataPath);
|
||||
const currentDataPath = computed(() => general.dataPath);
|
||||
|
||||
// 选择数据存储目录
|
||||
const selectDataDirectory = async () => {
|
||||
@@ -189,7 +201,7 @@ const selectDataDirectory = async () => {
|
||||
|
||||
try {
|
||||
await MigrationService.MigrateDirectory(oldPath, newPath);
|
||||
await configStore.setDataPath(newPath);
|
||||
await setDataPath(newPath);
|
||||
} catch (e) {
|
||||
stop();
|
||||
// 设置手动捕获的错误(当轮询还没获取到错误时)
|
||||
@@ -314,10 +326,6 @@ const selectDataDirectory = async () => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
//max-width: 800px;
|
||||
}
|
||||
|
||||
.hotkey-selector {
|
||||
padding: 15px 0 5px 20px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
35
go.mod
35
go.mod
@@ -5,7 +5,7 @@ go 1.25
|
||||
require (
|
||||
entgo.io/ent v0.14.5
|
||||
github.com/creativeprojects/go-selfupdate v1.5.1
|
||||
github.com/go-git/go-git/v5 v5.16.3
|
||||
github.com/go-git/go-git/v5 v5.16.4
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/knadh/koanf/parsers/json v1.0.0
|
||||
github.com/knadh/koanf/providers/file v1.2.0
|
||||
@@ -13,15 +13,15 @@ require (
|
||||
github.com/knadh/koanf/v2 v2.3.0
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.41
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/sys v0.38.0
|
||||
golang.org/x/text v0.31.0
|
||||
resty.dev/v3 v3.0.0-beta.3
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.48
|
||||
golang.org/x/net v0.48.0
|
||||
golang.org/x/sys v0.39.0
|
||||
golang.org/x/text v0.32.0
|
||||
resty.dev/v3 v3.0.0-beta.5
|
||||
)
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect
|
||||
ariga.io/atlas v0.38.0 // indirect
|
||||
code.gitea.io/sdk/gitea v0.22.1 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||
@@ -44,9 +44,9 @@ require (
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.7.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||
github.com/go-openapi/inflect v0.21.5 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
@@ -55,8 +55,8 @@ require (
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
|
||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.24.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
|
||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||
@@ -72,7 +72,6 @@ require (
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
@@ -83,14 +82,16 @@ require (
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
github.com/xanzy/go-gitlab v0.115.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/zclconf/go-cty v1.14.4 // indirect
|
||||
github.com/zclconf/go-cty v1.17.0 // indirect
|
||||
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
|
||||
golang.org/x/image v0.33.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/oauth2 v0.33.0 // indirect
|
||||
golang.org/x/image v0.34.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/time v0.14.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/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
76
go.sum
76
go.sum
@@ -1,5 +1,5 @@
|
||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 h1:E0wvcUXTkgyN4wy4LGtNzMNGMytJN8afmIWXJVMi4cc=
|
||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w=
|
||||
ariga.io/atlas v0.38.0 h1:MwbtwVtDWJFq+ECyeTAz2ArvewDnpeiw/t/sgNdDsdo=
|
||||
ariga.io/atlas v0.38.0/go.mod h1:D7XMK6ei3GvfDqvzk+2VId78j77LdqHrqPOWamn51/s=
|
||||
code.gitea.io/sdk/gitea v0.22.1 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA=
|
||||
code.gitea.io/sdk/gitea v0.22.1/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
@@ -62,16 +62,16 @@ github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
|
||||
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
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.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8=
|
||||
github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
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-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-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-openapi/inflect v0.21.5 h1:M2RCq6PPS3YbIaL7CXosGL3BbzAcmfBAT0nC3YfesZA=
|
||||
github.com/go-openapi/inflect v0.21.5/go.mod h1:GypUyi6bU880NYurWaEH2CmH84zFDNd+EhhmzroHmB4=
|
||||
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-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
@@ -97,10 +97,10 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo=
|
||||
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
||||
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
|
||||
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
|
||||
@@ -126,8 +126,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
@@ -182,38 +180,42 @@ github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+
|
||||
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/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.41 h1:DYcC1/vtO862sxnoyCOMfLLypbzpFWI257fR6zDYY+Y=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.41/go.mod h1:7i8tSuA74q97zZ5qEJlcVZdnO+IR7LT2KU8UpzYMPsw=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.48 h1:m22PcankYJI/lKbv7NnNekxsEJYPbvIUnvHvH4WD1xQ=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.48/go.mod h1:yaz8baG0+YzoiN8J6osn0wKiEi0iUux0ZU5NsZFu6OQ=
|
||||
github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
|
||||
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=
|
||||
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||
github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=
|
||||
github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
|
||||
github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=
|
||||
github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
|
||||
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-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
||||
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
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/net v0.0.0-20190311183353-d8887717615a/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-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -225,19 +227,21 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
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.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
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=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -252,5 +256,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E=
|
||||
resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=
|
||||
resty.dev/v3 v3.0.0-beta.5 h1:NV1xbqOLzSq7XMTs1t/HLPvu7xrxoXzF90SR4OO6faQ=
|
||||
resty.dev/v3 v3.0.0-beta.5/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package services
|
||||
package helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
)
|
||||
|
||||
const pathSeparator = '.'
|
||||
|
||||
// ObserverCallback 观察者回调函数
|
||||
type ObserverCallback func(oldValue, newValue interface{})
|
||||
|
||||
@@ -49,6 +51,8 @@ func NewConfigObserver(logger *log.LogService) *ConfigObserver {
|
||||
}
|
||||
|
||||
// Watch 注册配置变更监听器
|
||||
// 支持前缀监听:注册 "generate" 可以监听 "generate.xxx"、"generate.yyy" 等所有子路径的变化
|
||||
// 返回取消函数,调用后停止监听
|
||||
func (co *ConfigObserver) Watch(path string, callback ObserverCallback) CancelFunc {
|
||||
// 生成唯一ID
|
||||
id := fmt.Sprintf("obs_%d", co.nextObserverID.Add(1))
|
||||
@@ -103,36 +107,91 @@ func (co *ConfigObserver) removeObserver(path, id string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Notify 通知指定路径的所有观察者
|
||||
// Notify 通知指定路径及其所有父路径的观察者
|
||||
// 支持前缀监听:当 "generate.xxx" 变化时,同时通知监听 "generate" 的观察者
|
||||
// 通知顺序:精确匹配 -> 父路径(从近到远)
|
||||
func (co *ConfigObserver) Notify(path string, oldValue, newValue interface{}) {
|
||||
// 获取该路径的所有观察者(拷贝以避免并发问题)
|
||||
co.observerMu.RLock()
|
||||
observers := co.observers[path]
|
||||
if len(observers) == 0 {
|
||||
co.observerMu.RUnlock()
|
||||
callbacks := co.collectCallbacks(path)
|
||||
co.observerMu.RUnlock()
|
||||
|
||||
if len(callbacks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 拷贝观察者列表
|
||||
callbacks := make([]ObserverCallback, len(observers))
|
||||
for i, obs := range observers {
|
||||
callbacks[i] = obs.callback
|
||||
}
|
||||
co.observerMu.RUnlock()
|
||||
|
||||
// 在独立 goroutine 中执行回调
|
||||
// 执行所有回调
|
||||
for _, callback := range callbacks {
|
||||
co.executeCallback(callback, oldValue, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyAll 通知所有匹配前缀的观察者
|
||||
// collectCallbacks 收集指定路径及其所有父路径的观察者回调
|
||||
// 调用者必须持有读锁
|
||||
func (co *ConfigObserver) collectCallbacks(path string) []ObserverCallback {
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var callbacks []ObserverCallback
|
||||
|
||||
// 1. 收集精确匹配的观察者
|
||||
if observers := co.observers[path]; len(observers) > 0 {
|
||||
callbacks = make([]ObserverCallback, 0, len(observers)*2)
|
||||
for _, obs := range observers {
|
||||
callbacks = append(callbacks, obs.callback)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 收集父路径的观察者(从后向前遍历,避免 strings.Split 的内存分配)
|
||||
for i := len(path) - 1; i >= 0; i-- {
|
||||
if path[i] == pathSeparator {
|
||||
parentPath := path[:i]
|
||||
if observers := co.observers[parentPath]; len(observers) > 0 {
|
||||
if callbacks == nil {
|
||||
callbacks = make([]ObserverCallback, 0, len(observers))
|
||||
}
|
||||
for _, obs := range observers {
|
||||
callbacks = append(callbacks, obs.callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return callbacks
|
||||
}
|
||||
|
||||
// NotifyAll 批量通知所有匹配路径的观察者
|
||||
func (co *ConfigObserver) NotifyAll(changes map[string]struct {
|
||||
OldValue interface{}
|
||||
NewValue interface{}
|
||||
}) {
|
||||
if len(changes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
type callbackTask struct {
|
||||
callback ObserverCallback
|
||||
oldValue interface{}
|
||||
newValue interface{}
|
||||
}
|
||||
|
||||
// 只获取一次读锁,收集所有回调
|
||||
co.observerMu.RLock()
|
||||
var tasks []callbackTask
|
||||
for path, change := range changes {
|
||||
co.Notify(path, change.OldValue, change.NewValue)
|
||||
for _, cb := range co.collectCallbacks(path) {
|
||||
tasks = append(tasks, callbackTask{
|
||||
callback: cb,
|
||||
oldValue: change.OldValue,
|
||||
newValue: change.NewValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
co.observerMu.RUnlock()
|
||||
|
||||
// 执行所有回调
|
||||
for _, task := range tasks {
|
||||
co.executeCallback(task.callback, task.oldValue, task.newValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ type Document struct {
|
||||
// ID of the ent.
|
||||
ID int `json:"id,omitempty"`
|
||||
// UUID for cross-device sync (UUIDv7)
|
||||
UUID string `json:"uuid"`
|
||||
UUID *string `json:"uuid"`
|
||||
// creation time
|
||||
CreatedAt string `json:"created_at"`
|
||||
// update time
|
||||
@@ -69,7 +69,8 @@ func (_m *Document) assignValues(columns []string, values []any) error {
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field uuid", values[i])
|
||||
} else if value.Valid {
|
||||
_m.UUID = value.String
|
||||
_m.UUID = new(string)
|
||||
*_m.UUID = value.String
|
||||
}
|
||||
case document.FieldCreatedAt:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
@@ -144,8 +145,10 @@ func (_m *Document) String() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("Document(")
|
||||
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
||||
builder.WriteString("uuid=")
|
||||
builder.WriteString(_m.UUID)
|
||||
if v := _m.UUID; v != nil {
|
||||
builder.WriteString("uuid=")
|
||||
builder.WriteString(*v)
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("created_at=")
|
||||
builder.WriteString(_m.CreatedAt)
|
||||
|
||||
@@ -225,7 +225,7 @@ func (_c *DocumentCreate) createSpec() (*Document, *sqlgraph.CreateSpec) {
|
||||
)
|
||||
if value, ok := _c.mutation.UUID(); ok {
|
||||
_spec.SetField(document.FieldUUID, field.TypeString, value)
|
||||
_node.UUID = value
|
||||
_node.UUID = &value
|
||||
}
|
||||
if value, ok := _c.mutation.CreatedAt(); ok {
|
||||
_spec.SetField(document.FieldCreatedAt, field.TypeString, value)
|
||||
|
||||
@@ -18,7 +18,7 @@ type Extension struct {
|
||||
// ID of the ent.
|
||||
ID int `json:"id,omitempty"`
|
||||
// UUID for cross-device sync (UUIDv7)
|
||||
UUID string `json:"uuid"`
|
||||
UUID *string `json:"uuid"`
|
||||
// creation time
|
||||
CreatedAt string `json:"created_at"`
|
||||
// update time
|
||||
@@ -72,7 +72,8 @@ func (_m *Extension) assignValues(columns []string, values []any) error {
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field uuid", values[i])
|
||||
} else if value.Valid {
|
||||
_m.UUID = value.String
|
||||
_m.UUID = new(string)
|
||||
*_m.UUID = value.String
|
||||
}
|
||||
case extension.FieldCreatedAt:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
@@ -149,8 +150,10 @@ func (_m *Extension) String() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("Extension(")
|
||||
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
||||
builder.WriteString("uuid=")
|
||||
builder.WriteString(_m.UUID)
|
||||
if v := _m.UUID; v != nil {
|
||||
builder.WriteString("uuid=")
|
||||
builder.WriteString(*v)
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("created_at=")
|
||||
builder.WriteString(_m.CreatedAt)
|
||||
|
||||
@@ -213,7 +213,7 @@ func (_c *ExtensionCreate) createSpec() (*Extension, *sqlgraph.CreateSpec) {
|
||||
)
|
||||
if value, ok := _c.mutation.UUID(); ok {
|
||||
_spec.SetField(extension.FieldUUID, field.TypeString, value)
|
||||
_node.UUID = value
|
||||
_node.UUID = &value
|
||||
}
|
||||
if value, ok := _c.mutation.CreatedAt(); ok {
|
||||
_spec.SetField(extension.FieldCreatedAt, field.TypeString, value)
|
||||
|
||||
@@ -17,7 +17,7 @@ type KeyBinding struct {
|
||||
// ID of the ent.
|
||||
ID int `json:"id,omitempty"`
|
||||
// UUID for cross-device sync (UUIDv7)
|
||||
UUID string `json:"uuid"`
|
||||
UUID *string `json:"uuid"`
|
||||
// creation time
|
||||
CreatedAt string `json:"created_at"`
|
||||
// update time
|
||||
@@ -71,7 +71,8 @@ func (_m *KeyBinding) assignValues(columns []string, values []any) error {
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field uuid", values[i])
|
||||
} else if value.Valid {
|
||||
_m.UUID = value.String
|
||||
_m.UUID = new(string)
|
||||
*_m.UUID = value.String
|
||||
}
|
||||
case keybinding.FieldCreatedAt:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
@@ -152,8 +153,10 @@ func (_m *KeyBinding) String() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("KeyBinding(")
|
||||
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
||||
builder.WriteString("uuid=")
|
||||
builder.WriteString(_m.UUID)
|
||||
if v := _m.UUID; v != nil {
|
||||
builder.WriteString("uuid=")
|
||||
builder.WriteString(*v)
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("created_at=")
|
||||
builder.WriteString(_m.CreatedAt)
|
||||
|
||||
@@ -240,7 +240,7 @@ func (_c *KeyBindingCreate) createSpec() (*KeyBinding, *sqlgraph.CreateSpec) {
|
||||
)
|
||||
if value, ok := _c.mutation.UUID(); ok {
|
||||
_spec.SetField(keybinding.FieldUUID, field.TypeString, value)
|
||||
_node.UUID = value
|
||||
_node.UUID = &value
|
||||
}
|
||||
if value, ok := _c.mutation.CreatedAt(); ok {
|
||||
_spec.SetField(keybinding.FieldCreatedAt, field.TypeString, value)
|
||||
|
||||
@@ -166,7 +166,7 @@ func (m *DocumentMutation) UUID() (r string, exists bool) {
|
||||
// OldUUID returns the old "uuid" field's value of the Document entity.
|
||||
// If the Document object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *DocumentMutation) OldUUID(ctx context.Context) (v string, err error) {
|
||||
func (m *DocumentMutation) OldUUID(ctx context.Context) (v *string, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldUUID is only allowed on UpdateOne operations")
|
||||
}
|
||||
@@ -876,7 +876,7 @@ func (m *ExtensionMutation) UUID() (r string, exists bool) {
|
||||
// OldUUID returns the old "uuid" field's value of the Extension entity.
|
||||
// If the Extension object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *ExtensionMutation) OldUUID(ctx context.Context) (v string, err error) {
|
||||
func (m *ExtensionMutation) OldUUID(ctx context.Context) (v *string, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldUUID is only allowed on UpdateOne operations")
|
||||
}
|
||||
@@ -1587,7 +1587,7 @@ func (m *KeyBindingMutation) UUID() (r string, exists bool) {
|
||||
// OldUUID returns the old "uuid" field's value of the KeyBinding entity.
|
||||
// If the KeyBinding object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *KeyBindingMutation) OldUUID(ctx context.Context) (v string, err error) {
|
||||
func (m *KeyBindingMutation) OldUUID(ctx context.Context) (v *string, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldUUID is only allowed on UpdateOne operations")
|
||||
}
|
||||
@@ -2350,7 +2350,7 @@ func (m *ThemeMutation) UUID() (r string, exists bool) {
|
||||
// OldUUID returns the old "uuid" field's value of the Theme entity.
|
||||
// If the Theme object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *ThemeMutation) OldUUID(ctx context.Context) (v string, err error) {
|
||||
func (m *ThemeMutation) OldUUID(ctx context.Context) (v *string, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldUUID is only allowed on UpdateOne operations")
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ type Theme struct {
|
||||
// ID of the ent.
|
||||
ID int `json:"id,omitempty"`
|
||||
// UUID for cross-device sync (UUIDv7)
|
||||
UUID string `json:"uuid"`
|
||||
UUID *string `json:"uuid"`
|
||||
// creation time
|
||||
CreatedAt string `json:"created_at"`
|
||||
// update time
|
||||
@@ -70,7 +70,8 @@ func (_m *Theme) assignValues(columns []string, values []any) error {
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field uuid", values[i])
|
||||
} else if value.Valid {
|
||||
_m.UUID = value.String
|
||||
_m.UUID = new(string)
|
||||
*_m.UUID = value.String
|
||||
}
|
||||
case theme.FieldCreatedAt:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
@@ -147,8 +148,10 @@ func (_m *Theme) String() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("Theme(")
|
||||
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
||||
builder.WriteString("uuid=")
|
||||
builder.WriteString(_m.UUID)
|
||||
if v := _m.UUID; v != nil {
|
||||
builder.WriteString("uuid=")
|
||||
builder.WriteString(*v)
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("created_at=")
|
||||
builder.WriteString(_m.CreatedAt)
|
||||
|
||||
@@ -206,7 +206,7 @@ func (_c *ThemeCreate) createSpec() (*Theme, *sqlgraph.CreateSpec) {
|
||||
)
|
||||
if value, ok := _c.mutation.UUID(); ok {
|
||||
_spec.SetField(theme.FieldUUID, field.TypeString, value)
|
||||
_node.UUID = value
|
||||
_node.UUID = &value
|
||||
}
|
||||
if value, ok := _c.mutation.CreatedAt(); ok {
|
||||
_spec.SetField(theme.FieldCreatedAt, field.TypeString, value)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"voidraft/internal/common/helper"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
gitConfig "github.com/go-git/go-git/v5/config"
|
||||
@@ -61,7 +62,7 @@ type BackupService struct {
|
||||
autoBackupStop chan bool
|
||||
autoBackupWg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
cancelObserver CancelFunc
|
||||
cancelObservers []helper.CancelFunc
|
||||
}
|
||||
|
||||
// NewBackupService 创建新的备份服务实例
|
||||
@@ -74,7 +75,11 @@ func NewBackupService(configService *ConfigService, dbService *DatabaseService,
|
||||
}
|
||||
|
||||
func (s *BackupService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||
s.cancelObserver = s.configService.Watch("backup.enabled", s.onBackupConfigChange)
|
||||
// 监听 backup 配置变化
|
||||
s.cancelObservers = []helper.CancelFunc{
|
||||
s.configService.Watch("backup", s.onBackupConfigChange),
|
||||
s.configService.Watch("general.dataPath", s.onDataPathChange),
|
||||
}
|
||||
if err := s.Initialize(); err != nil {
|
||||
s.logger.Error("initializing backup service: %v", err)
|
||||
}
|
||||
@@ -89,6 +94,12 @@ func (s *BackupService) onBackupConfigChange(oldValue, newValue interface{}) {
|
||||
_ = s.HandleConfigChange(&config.Backup)
|
||||
}
|
||||
|
||||
func (s *BackupService) onDataPathChange(oldValue, newValue interface{}) {
|
||||
if err := s.Reinitialize(); err != nil {
|
||||
s.logger.Error("Failed to reinitialize backup service after data path change: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize 初始化备份服务
|
||||
func (s *BackupService) Initialize() error {
|
||||
config, repoPath, err := s.getConfigAndPath()
|
||||
@@ -100,7 +111,7 @@ func (s *BackupService) Initialize() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 仓库地址为空时不初始化(等待用户配置)
|
||||
// 仓库地址为空时不初始化
|
||||
if strings.TrimSpace(config.RepoURL) == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -1196,8 +1207,10 @@ func (s *BackupService) HandleConfigChange(config *models.GitBackupConfig) error
|
||||
|
||||
// ServiceShutdown 服务关闭
|
||||
func (s *BackupService) ServiceShutdown() {
|
||||
if s.cancelObserver != nil {
|
||||
s.cancelObserver()
|
||||
for _, cancel := range s.cancelObservers {
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
s.StopAutoBackup()
|
||||
}
|
||||
|
||||
@@ -2,33 +2,31 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
"voidraft/internal/common/helper"
|
||||
"voidraft/internal/models"
|
||||
|
||||
jsonparser "github.com/knadh/koanf/parsers/json"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
"github.com/knadh/koanf/providers/structs"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
)
|
||||
|
||||
// ConfigService 应用配置服务
|
||||
type ConfigService struct {
|
||||
koanf *koanf.Koanf // koanf 实例
|
||||
logger *log.LogService // 日志服务
|
||||
configDir string // 配置目录
|
||||
settingsPath string // 设置文件路径
|
||||
mu sync.RWMutex // 读写锁
|
||||
fileProvider *file.File // 文件提供器,用于监听
|
||||
|
||||
observer *ConfigObserver
|
||||
koanf *koanf.Koanf
|
||||
logger *log.LogService
|
||||
configDir string
|
||||
settingsPath string
|
||||
mu sync.RWMutex
|
||||
observer *helper.ConfigObserver
|
||||
|
||||
// 配置迁移器
|
||||
configMigrator *ConfigMigrator
|
||||
@@ -36,49 +34,29 @@ type ConfigService struct {
|
||||
|
||||
// NewConfigService 创建新的配置服务实例
|
||||
func NewConfigService(logger *log.LogService) *ConfigService {
|
||||
// 获取用户主目录
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unable to get the user's home directory: %w", err))
|
||||
}
|
||||
|
||||
// 设置配置目录和设置文件路径
|
||||
configDir := filepath.Join(homeDir, ".voidraft", "config")
|
||||
settingsPath := filepath.Join(configDir, "settings.json")
|
||||
|
||||
observerService := NewConfigObserver(logger)
|
||||
|
||||
configMigrator := NewConfigMigrator(logger, configDir, "settings", settingsPath)
|
||||
|
||||
return &ConfigService{
|
||||
logger: logger,
|
||||
configDir: configDir,
|
||||
settingsPath: settingsPath,
|
||||
koanf: koanf.New("."),
|
||||
observer: observerService,
|
||||
configMigrator: configMigrator,
|
||||
observer: helper.NewConfigObserver(logger),
|
||||
configMigrator: NewConfigMigrator(logger, configDir, "settings", settingsPath),
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceStartup initializes the service when the application starts
|
||||
// ServiceStartup 服务启动时初始化
|
||||
func (cs *ConfigService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||
err := cs.initConfig()
|
||||
if err != nil {
|
||||
if err := cs.initConfig(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// 启动配置文件监听
|
||||
cs.startWatching()
|
||||
return nil
|
||||
}
|
||||
|
||||
// setDefaults 设置默认配置
|
||||
func (cs *ConfigService) setDefaults() error {
|
||||
defaultConfig := models.NewDefaultAppConfig()
|
||||
|
||||
if err := cs.koanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -87,15 +65,38 @@ func (cs *ConfigService) initConfig() error {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
// 检查配置文件是否存在
|
||||
// 确保配置目录存在
|
||||
if err := os.MkdirAll(cs.configDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
// 配置文件不存在,创建默认配置
|
||||
if _, err := os.Stat(cs.settingsPath); os.IsNotExist(err) {
|
||||
return cs.createDefaultConfig()
|
||||
}
|
||||
|
||||
// 配置文件存在,直接加载现有配置
|
||||
cs.fileProvider = file.Provider(cs.settingsPath)
|
||||
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
|
||||
return err
|
||||
// 加载现有配置
|
||||
if err := cs.koanf.Load(file.Provider(cs.settingsPath), jsonparser.Parser()); err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createDefaultConfig 创建默认配置
|
||||
func (cs *ConfigService) createDefaultConfig() error {
|
||||
// 重置 koanf 实例
|
||||
cs.koanf = koanf.New(".")
|
||||
|
||||
// 加载默认配置
|
||||
defaultConfig := models.NewDefaultAppConfig()
|
||||
if err := cs.koanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
|
||||
return fmt.Errorf("failed to load default config: %w", err)
|
||||
}
|
||||
|
||||
// 写入配置文件
|
||||
if err := cs.writeConfigToFile(); err != nil {
|
||||
return fmt.Errorf("failed to write default config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -107,65 +108,12 @@ func (cs *ConfigService) MigrateConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
defaultConfig := models.NewDefaultAppConfig()
|
||||
_, err := cs.configMigrator.AutoMigrate(defaultConfig, cs.koanf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createDefaultConfig 创建默认配置文件
|
||||
func (cs *ConfigService) createDefaultConfig() error {
|
||||
// 确保配置目录存在
|
||||
if err := os.MkdirAll(cs.configDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cs.setDefaults(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cs.writeConfigToFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建文件提供器
|
||||
cs.fileProvider = file.Provider(cs.settingsPath)
|
||||
|
||||
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// startWatching 启动配置文件监听
|
||||
func (cs *ConfigService) startWatching() {
|
||||
if cs.fileProvider == nil {
|
||||
return
|
||||
}
|
||||
cs.fileProvider.Watch(func(event interface{}, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cs.mu.Lock()
|
||||
oldSnapshot := cs.createConfigSnapshot()
|
||||
cs.koanf.Load(cs.fileProvider, jsonparser.Parser())
|
||||
newSnapshot := cs.createConfigSnapshot()
|
||||
cs.mu.Unlock()
|
||||
|
||||
// 检测配置变更并通知观察者
|
||||
cs.notifyChanges(oldSnapshot, newSnapshot)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// stopWatching 停止配置文件监听
|
||||
func (cs *ConfigService) stopWatching() {
|
||||
if cs.fileProvider != nil {
|
||||
cs.fileProvider.Unwatch()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetConfig 获取完整应用配置
|
||||
@@ -177,47 +125,9 @@ func (cs *ConfigService) GetConfig() (*models.AppConfig, error) {
|
||||
if err := cs.koanf.UnmarshalWithConf("", &config, koanf.UnmarshalConf{Tag: "json"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// Set 设置配置项
|
||||
func (cs *ConfigService) Set(key string, value interface{}) error {
|
||||
cs.mu.Lock()
|
||||
|
||||
// 获取旧值用于回滚
|
||||
oldValue := cs.koanf.Get(key)
|
||||
|
||||
// 设置值到koanf
|
||||
cs.koanf.Set(key, value)
|
||||
|
||||
// 更新时间戳
|
||||
newTimestamp := time.Now().Format(time.RFC3339)
|
||||
cs.koanf.Set("metadata.lastUpdated", newTimestamp)
|
||||
|
||||
// 将配置写回文件
|
||||
err := cs.writeConfigToFile()
|
||||
|
||||
if err != nil {
|
||||
// 写文件失败,回滚内存状态
|
||||
if oldValue != nil {
|
||||
cs.koanf.Set(key, oldValue)
|
||||
} else {
|
||||
cs.koanf.Delete(key)
|
||||
}
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
cs.mu.Unlock()
|
||||
|
||||
if cs.observer != nil {
|
||||
cs.observer.Notify(key, oldValue, value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get 获取配置项
|
||||
func (cs *ConfigService) Get(key string) interface{} {
|
||||
cs.mu.RLock()
|
||||
@@ -225,118 +135,113 @@ func (cs *ConfigService) Get(key string) interface{} {
|
||||
return cs.koanf.Get(key)
|
||||
}
|
||||
|
||||
// ResetConfig 强制重置所有配置为默认值
|
||||
// Set 设置配置项
|
||||
func (cs *ConfigService) Set(key string, value interface{}) error {
|
||||
cs.mu.Lock()
|
||||
|
||||
// 获取旧值
|
||||
oldValue := cs.koanf.Get(key)
|
||||
|
||||
// 值未变化,直接返回
|
||||
if reflect.DeepEqual(oldValue, value) {
|
||||
cs.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 设置新值
|
||||
err := cs.koanf.Set(key, value)
|
||||
if err != nil {
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
err = cs.koanf.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339))
|
||||
if err != nil {
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
if err = cs.writeConfigToFile(); err != nil {
|
||||
cs.mu.Unlock()
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
|
||||
cs.mu.Unlock()
|
||||
|
||||
// 通知观察者
|
||||
if cs.observer != nil {
|
||||
cs.observer.Notify(key, oldValue, value)
|
||||
} else {
|
||||
cs.logger.Error("config observer is nil")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetConfig 重置所有配置为默认值
|
||||
func (cs *ConfigService) ResetConfig() error {
|
||||
cs.mu.Lock()
|
||||
|
||||
// 保存旧配置快照
|
||||
oldSnapshot := cs.createConfigSnapshot()
|
||||
oldSnapshot := cs.createSnapshot()
|
||||
|
||||
// 停止文件监听
|
||||
if cs.fileProvider != nil {
|
||||
cs.fileProvider.Unwatch()
|
||||
cs.fileProvider = nil
|
||||
}
|
||||
|
||||
// 设置默认配置
|
||||
if err := cs.setDefaults(); err != nil {
|
||||
// 重置为默认配置
|
||||
cs.koanf = koanf.New(".")
|
||||
defaultConfig := models.NewDefaultAppConfig()
|
||||
if err := cs.koanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
return fmt.Errorf("failed to load default config: %w", err)
|
||||
}
|
||||
|
||||
// 写入配置文件
|
||||
if err := cs.writeConfigToFile(); err != nil {
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
|
||||
// 重新创建koanf实例
|
||||
cs.koanf = koanf.New(".")
|
||||
|
||||
// 重新加载默认配置到koanf
|
||||
if err := cs.setDefaults(); err != nil {
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// 重新创建文件提供器
|
||||
cs.fileProvider = file.Provider(cs.settingsPath)
|
||||
|
||||
// 重新加载配置文件
|
||||
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
|
||||
cs.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
newSnapshot := cs.createConfigSnapshot()
|
||||
newSnapshot := cs.createSnapshot()
|
||||
cs.mu.Unlock()
|
||||
|
||||
// 重新启动文件监听
|
||||
cs.startWatching()
|
||||
|
||||
// 检测配置变更并通知观察者
|
||||
// 通知配置变更
|
||||
cs.notifyChanges(oldSnapshot, newSnapshot)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeConfigToFile 将配置写回JSON文件
|
||||
// writeConfigToFile 将配置写入文件
|
||||
func (cs *ConfigService) writeConfigToFile() error {
|
||||
configBytes, err := cs.koanf.Marshal(jsonparser.Parser())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(cs.settingsPath, configBytes, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return os.WriteFile(cs.settingsPath, configBytes, 0644)
|
||||
}
|
||||
|
||||
// Watch 注册配置变更监听器
|
||||
func (cs *ConfigService) Watch(path string, callback ObserverCallback) CancelFunc {
|
||||
func (cs *ConfigService) Watch(path string, callback helper.ObserverCallback) helper.CancelFunc {
|
||||
return cs.observer.Watch(path, callback)
|
||||
}
|
||||
|
||||
// WatchWithContext 使用 Context 注册监听器
|
||||
func (cs *ConfigService) WatchWithContext(ctx context.Context, path string, callback ObserverCallback) {
|
||||
func (cs *ConfigService) WatchWithContext(ctx context.Context, path string, callback helper.ObserverCallback) {
|
||||
cs.observer.WatchWithContext(ctx, path, callback)
|
||||
}
|
||||
|
||||
// createConfigSnapshot 创建当前配置的快照(调用者需确保已持有锁)
|
||||
func (cs *ConfigService) createConfigSnapshot() map[string]interface{} {
|
||||
// createSnapshotLocked 创建配置快照
|
||||
func (cs *ConfigService) createSnapshot() map[string]interface{} {
|
||||
snapshot := make(map[string]interface{})
|
||||
allKeys := cs.koanf.All()
|
||||
flattenMap("", allKeys, snapshot)
|
||||
return snapshot
|
||||
}
|
||||
|
||||
// flattenMap 递归展平嵌套的 map(使用 strings.Builder 优化字符串拼接)
|
||||
func flattenMap(prefix string, data map[string]interface{}, result map[string]interface{}) {
|
||||
var builder strings.Builder
|
||||
for key, value := range data {
|
||||
builder.Reset()
|
||||
if prefix != "" {
|
||||
builder.WriteString(prefix)
|
||||
builder.WriteString(".")
|
||||
}
|
||||
builder.WriteString(key)
|
||||
fullKey := builder.String()
|
||||
|
||||
if valueMap, ok := value.(map[string]interface{}); ok {
|
||||
// 递归处理嵌套 map
|
||||
flattenMap(fullKey, valueMap, result)
|
||||
} else {
|
||||
// 保存叶子节点
|
||||
result[fullKey] = value
|
||||
}
|
||||
for _, key := range cs.koanf.Keys() {
|
||||
snapshot[key] = cs.koanf.Get(key)
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
// notifyChanges 检测配置变更并通知观察者
|
||||
func (cs *ConfigService) notifyChanges(oldSnapshot, newSnapshot map[string]interface{}) {
|
||||
// 检测变更
|
||||
if cs.observer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
changes := make(map[string]struct {
|
||||
OldValue interface{}
|
||||
NewValue interface{}
|
||||
@@ -345,14 +250,11 @@ func (cs *ConfigService) notifyChanges(oldSnapshot, newSnapshot map[string]inter
|
||||
// 检查新增和修改的键
|
||||
for key, newValue := range newSnapshot {
|
||||
oldValue, exists := oldSnapshot[key]
|
||||
if !exists || !isEqual(oldValue, newValue) {
|
||||
if !exists || !reflect.DeepEqual(oldValue, newValue) {
|
||||
changes[key] = struct {
|
||||
OldValue interface{}
|
||||
NewValue interface{}
|
||||
}{
|
||||
OldValue: oldValue,
|
||||
NewValue: newValue,
|
||||
}
|
||||
}{oldValue, newValue}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,29 +264,18 @@ func (cs *ConfigService) notifyChanges(oldSnapshot, newSnapshot map[string]inter
|
||||
changes[key] = struct {
|
||||
OldValue interface{}
|
||||
NewValue interface{}
|
||||
}{
|
||||
OldValue: oldValue,
|
||||
NewValue: nil,
|
||||
}
|
||||
}{oldValue, nil}
|
||||
}
|
||||
}
|
||||
|
||||
// 通知所有变更
|
||||
if cs.observer != nil && len(changes) > 0 {
|
||||
// 批量通知
|
||||
if len(changes) > 0 {
|
||||
cs.observer.NotifyAll(changes)
|
||||
}
|
||||
}
|
||||
|
||||
// isEqual 值相等比较
|
||||
func isEqual(a, b interface{}) bool {
|
||||
aJSON, _ := json.Marshal(a)
|
||||
bJSON, _ := json.Marshal(b)
|
||||
return string(aJSON) == string(bJSON)
|
||||
}
|
||||
|
||||
// ServiceShutdown 关闭服务
|
||||
func (cs *ConfigService) ServiceShutdown() error {
|
||||
cs.stopWatching()
|
||||
if cs.observer != nil {
|
||||
cs.observer.Shutdown()
|
||||
}
|
||||
|
||||
@@ -24,9 +24,10 @@ type MigrationProgress struct {
|
||||
|
||||
// MigrationService 迁移服务
|
||||
type MigrationService struct {
|
||||
logger *log.LogService
|
||||
dbService *DatabaseService
|
||||
progress atomic.Value // stores MigrationProgress
|
||||
logger *log.LogService
|
||||
dbService *DatabaseService
|
||||
configService *ConfigService
|
||||
progress atomic.Value // stores MigrationProgress
|
||||
|
||||
mu sync.Mutex
|
||||
ctx context.Context
|
||||
@@ -34,13 +35,14 @@ type MigrationService struct {
|
||||
}
|
||||
|
||||
// NewMigrationService 创建迁移服务
|
||||
func NewMigrationService(dbService *DatabaseService, logger *log.LogService) *MigrationService {
|
||||
func NewMigrationService(dbService *DatabaseService, configService *ConfigService, logger *log.LogService) *MigrationService {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
}
|
||||
ms := &MigrationService{
|
||||
logger: logger,
|
||||
dbService: dbService,
|
||||
logger: logger,
|
||||
dbService: dbService,
|
||||
configService: configService,
|
||||
}
|
||||
ms.progress.Store(MigrationProgress{})
|
||||
return ms
|
||||
@@ -94,9 +96,12 @@ func (ms *MigrationService) MigrateDirectory(srcPath, dstPath string) error {
|
||||
if err := ms.dbService.ServiceShutdown(); err != nil {
|
||||
ms.logger.Error("Failed to close database connection", "error", err)
|
||||
}
|
||||
|
||||
// 等待文件句柄释放(Windows 特有问题)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 确保失败时恢复数据库连接
|
||||
// 确保恢复数据库连接
|
||||
defer func() {
|
||||
if ms.dbService != nil {
|
||||
if err := ms.dbService.ServiceStartup(ctx, application.ServiceOptions{}); err != nil {
|
||||
@@ -110,17 +115,59 @@ func (ms *MigrationService) MigrateDirectory(srcPath, dstPath string) error {
|
||||
return ms.fail(err)
|
||||
}
|
||||
|
||||
// 迁移成功后,立即更新配置到新路径
|
||||
if ms.configService != nil {
|
||||
if err := ms.configService.Set("general.dataPath", dstPath); err != nil {
|
||||
return ms.fail(fmt.Errorf("migration succeeded but failed to update config: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
ms.setProgress(100)
|
||||
return nil
|
||||
}
|
||||
|
||||
// preCheck 预检查,返回是否需要迁移
|
||||
func (ms *MigrationService) preCheck(srcPath, dstPath string) (bool, error) {
|
||||
// 源目录不存在,无需迁移
|
||||
if _, err := os.Stat(srcPath); os.IsNotExist(err) {
|
||||
// 检查源目录状态
|
||||
srcStat, srcErr := os.Stat(srcPath)
|
||||
srcNotExist := os.IsNotExist(srcErr)
|
||||
|
||||
// 检查目标目录状态
|
||||
dstStat, dstErr := os.Stat(dstPath)
|
||||
dstNotExist := os.IsNotExist(dstErr)
|
||||
|
||||
// 1:源目录不存在
|
||||
if srcNotExist {
|
||||
// 如果目标目录存在且有内容,说明迁移已经完成
|
||||
if !dstNotExist && dstStat.IsDir() {
|
||||
isEmpty, err := isDirEmpty(dstPath)
|
||||
if err == nil && !isEmpty {
|
||||
ms.logger.Info("Migration already completed, source not exist but target has content", "dst", dstPath)
|
||||
return false, nil // 无需迁移
|
||||
}
|
||||
}
|
||||
// 源不存在且目标也不存在/为空,无需迁移
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 2. 源目录存在但为空
|
||||
if srcStat.IsDir() {
|
||||
srcEmpty, err := isDirEmpty(srcPath)
|
||||
if err == nil && srcEmpty {
|
||||
// 源为空,目标有内容 → 迁移已完成
|
||||
if !dstNotExist && dstStat.IsDir() {
|
||||
dstEmpty, _ := isDirEmpty(dstPath)
|
||||
if !dstEmpty {
|
||||
ms.logger.Info("Migration already completed, source is empty but target has content", "dst", dstPath)
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
// 源为空,目标也为空 → 无需迁移
|
||||
ms.logger.Info("Both source and target are empty, no migration needed")
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 路径相同,无需迁移
|
||||
srcAbs, _ := filepath.Abs(srcPath)
|
||||
dstAbs, _ := filepath.Abs(dstPath)
|
||||
|
||||
@@ -26,7 +26,7 @@ func NewDialogService(logger *log.LogService) *DialogService {
|
||||
|
||||
// SelectDirectory 打开目录选择对话框
|
||||
func (ds *DialogService) SelectDirectory() (string, error) {
|
||||
dialog := application.OpenFileDialog()
|
||||
dialog := application.OpenFileDialogStruct{}
|
||||
dialog.SetOptions(&application.OpenFileDialogOptions{
|
||||
// 目录选择配置
|
||||
CanChooseDirectories: true, // 允许选择目录
|
||||
@@ -67,7 +67,7 @@ func (ds *DialogService) SelectDirectory() (string, error) {
|
||||
|
||||
// SelectFile 打开文件选择对话框
|
||||
func (ds *DialogService) SelectFile() (string, error) {
|
||||
dialog := application.OpenFileDialog()
|
||||
dialog := application.OpenFileDialogStruct{}
|
||||
dialog.SetOptions(&application.OpenFileDialogOptions{
|
||||
// 目录选择配置
|
||||
CanChooseDirectories: false, // 允许选择目录
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"voidraft/internal/common/helper"
|
||||
"voidraft/internal/common/hotkey"
|
||||
"voidraft/internal/models"
|
||||
@@ -33,7 +32,7 @@ type HotkeyService struct {
|
||||
isShutdown atomic.Bool
|
||||
|
||||
// 配置观察者取消函数
|
||||
cancelObservers []CancelFunc
|
||||
cancelObservers []helper.CancelFunc
|
||||
}
|
||||
|
||||
// NewHotkeyService 创建热键服务实例
|
||||
@@ -61,7 +60,7 @@ func (hs *HotkeyService) ServiceStartup(ctx context.Context, options application
|
||||
// Initialize 初始化热键服务
|
||||
func (hs *HotkeyService) Initialize() error {
|
||||
// 注册配置监听
|
||||
hs.cancelObservers = []CancelFunc{
|
||||
hs.cancelObservers = []helper.CancelFunc{
|
||||
hs.configService.Watch("general.enableGlobalHotkey", hs.onHotkeyConfigChange),
|
||||
hs.configService.Watch("general.globalHotkey", hs.onHotkeyConfigChange),
|
||||
}
|
||||
@@ -84,11 +83,14 @@ func (hs *HotkeyService) onHotkeyConfigChange(oldValue, newValue interface{}) {
|
||||
// 重新加载配置
|
||||
config, err := hs.configService.GetConfig()
|
||||
if err != nil {
|
||||
hs.logger.Error("failed to get config", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新热键
|
||||
_ = hs.UpdateHotkey(config.General.EnableGlobalHotkey, &config.General.GlobalHotkey)
|
||||
if err := hs.UpdateHotkey(config.General.EnableGlobalHotkey, &config.General.GlobalHotkey); err != nil {
|
||||
hs.logger.Error("failed to update hotkey", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterHotkey 注册全局热键
|
||||
@@ -101,22 +103,34 @@ func (hs *HotkeyService) RegisterHotkey(combo *models.HotkeyCombo) error {
|
||||
return errors.New("invalid hotkey combination")
|
||||
}
|
||||
|
||||
// 如果已注册,先取消
|
||||
if hs.registered.Load() {
|
||||
_ = hs.UnregisterHotkey()
|
||||
}
|
||||
|
||||
// 转换为 hotkey 库的格式
|
||||
key, mods, err := hs.convertHotkey(combo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert hotkey: %w", err)
|
||||
}
|
||||
|
||||
hs.mu.Lock()
|
||||
// 如果已注册,异步清理旧热键
|
||||
if hs.registered.Load() {
|
||||
hs.mu.RLock()
|
||||
oldHk := hs.hk
|
||||
hs.mu.RUnlock()
|
||||
|
||||
if oldHk != nil {
|
||||
// 异步清理,不阻塞当前流程
|
||||
go func() {
|
||||
if err := oldHk.Close(); err != nil {
|
||||
hs.logger.Error("failed to close old hotkey (ignored)", "error", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新的热键实例
|
||||
hs.mu.Lock()
|
||||
hs.hk = hotkey.New(mods, key)
|
||||
if err := hs.hk.Register(); err != nil {
|
||||
hs.mu.Unlock()
|
||||
hs.logger.Error("failed to register hotkey", "error", err)
|
||||
return fmt.Errorf("register hotkey: %w", err)
|
||||
}
|
||||
|
||||
@@ -141,35 +155,23 @@ func (hs *HotkeyService) UnregisterHotkey() error {
|
||||
hs.registered.Store(false)
|
||||
|
||||
// 获取热键实例的引用
|
||||
hs.mu.RLock()
|
||||
hs.mu.Lock()
|
||||
hk := hs.hk
|
||||
hs.mu.RUnlock()
|
||||
hs.hk = nil
|
||||
hs.currentHotkey = nil
|
||||
hs.mu.Unlock()
|
||||
|
||||
if hk == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 调用 Close() 确保完全清理
|
||||
_ = hk.Close()
|
||||
|
||||
// 等待监听 goroutine 退出
|
||||
done := make(chan struct{})
|
||||
// 异步清理
|
||||
go func() {
|
||||
hs.wg.Wait()
|
||||
close(done)
|
||||
if err := hk.Close(); err != nil {
|
||||
hs.logger.Error("failed to close hotkey (ignored)", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
|
||||
// 清理状态
|
||||
hs.mu.Lock()
|
||||
hs.hk = nil
|
||||
hs.currentHotkey = nil
|
||||
hs.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -321,6 +323,24 @@ func (hs *HotkeyService) showAllWindows() {
|
||||
}
|
||||
}
|
||||
|
||||
// GetSupportedKeys 返回系统支持的快捷键列表
|
||||
func (hs *HotkeyService) GetSupportedKeys() []string {
|
||||
// 返回当前系统支持的所有键
|
||||
// 这个列表与 convertKey 方法中的 keyMap 保持一致
|
||||
return []string{
|
||||
// 字母键
|
||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
|
||||
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
|
||||
// 数字键
|
||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
// 功能键
|
||||
"F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12",
|
||||
// 特殊键
|
||||
"Space", "Tab", "Enter", "Escape", "Delete",
|
||||
"ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight",
|
||||
}
|
||||
}
|
||||
|
||||
// isValidHotkey 验证热键组合
|
||||
func (hs *HotkeyService) isValidHotkey(combo *models.HotkeyCombo) bool {
|
||||
if combo == nil || combo.Key == "" {
|
||||
@@ -333,24 +353,6 @@ func (hs *HotkeyService) isValidHotkey(combo *models.HotkeyCombo) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetCurrentHotkey 获取当前热键
|
||||
func (hs *HotkeyService) GetCurrentHotkey() *models.HotkeyCombo {
|
||||
hs.mu.RLock()
|
||||
defer hs.mu.RUnlock()
|
||||
|
||||
if hs.currentHotkey == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.HotkeyCombo{
|
||||
Ctrl: hs.currentHotkey.Ctrl,
|
||||
Shift: hs.currentHotkey.Shift,
|
||||
Alt: hs.currentHotkey.Alt,
|
||||
Win: hs.currentHotkey.Win,
|
||||
Key: hs.currentHotkey.Key,
|
||||
}
|
||||
}
|
||||
|
||||
// IsRegistered 检查是否已注册
|
||||
func (hs *HotkeyService) IsRegistered() bool {
|
||||
return hs.registered.Load()
|
||||
|
||||
@@ -1,387 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
"voidraft/internal/models"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
)
|
||||
|
||||
// TestHotkeyServiceCreation 测试服务创建
|
||||
func TestHotkeyServiceCreation(t *testing.T) {
|
||||
logger := log.New()
|
||||
configService := &ConfigService{} // Mock
|
||||
|
||||
service := NewHotkeyService(configService, logger)
|
||||
if service == nil {
|
||||
t.Fatal("Failed to create hotkey service")
|
||||
}
|
||||
|
||||
if service.logger == nil {
|
||||
t.Error("Logger should not be nil")
|
||||
}
|
||||
|
||||
if service.registered.Load() {
|
||||
t.Error("Service should not have registered hotkey initially")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHotkeyValidation 测试热键验证
|
||||
func TestHotkeyValidation(t *testing.T) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
combo *models.HotkeyCombo
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
name: "Nil combo",
|
||||
combo: nil,
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "Empty key",
|
||||
combo: &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Key: "",
|
||||
},
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "No modifiers",
|
||||
combo: &models.HotkeyCombo{
|
||||
Key: "A",
|
||||
},
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "Valid: Ctrl+A",
|
||||
combo: &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Key: "A",
|
||||
},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Valid: Ctrl+Shift+F1",
|
||||
combo: &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Shift: true,
|
||||
Key: "F1",
|
||||
},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "Valid: Alt+Space",
|
||||
combo: &models.HotkeyCombo{
|
||||
Alt: true,
|
||||
Key: "Space",
|
||||
},
|
||||
valid: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := service.isValidHotkey(tt.combo)
|
||||
if result != tt.valid {
|
||||
t.Errorf("Expected valid=%v, got %v", tt.valid, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHotkeyConversion 测试热键转换
|
||||
func TestHotkeyConversion(t *testing.T) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
combo *models.HotkeyCombo
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid letter key",
|
||||
combo: &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Key: "A",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid number key",
|
||||
combo: &models.HotkeyCombo{
|
||||
Shift: true,
|
||||
Key: "1",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid function key",
|
||||
combo: &models.HotkeyCombo{
|
||||
Alt: true,
|
||||
Key: "F5",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid key",
|
||||
combo: &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Key: "InvalidKey",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
key, mods, err := service.convertHotkey(tt.combo)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if key == 0 {
|
||||
t.Error("Key should not be 0")
|
||||
}
|
||||
|
||||
if len(mods) == 0 {
|
||||
t.Error("Should have at least one modifier")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHotkeyRegisterUnregister 测试注册和注销
|
||||
func TestHotkeyRegisterUnregister(t *testing.T) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
combo := &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Shift: true,
|
||||
Key: "F10",
|
||||
}
|
||||
|
||||
// 测试注册
|
||||
err := service.RegisterHotkey(combo)
|
||||
if err != nil {
|
||||
t.Logf("Register failed (may be expected in test environment): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !service.IsRegistered() {
|
||||
t.Error("Service should be registered")
|
||||
}
|
||||
|
||||
// 验证当前热键
|
||||
current := service.GetCurrentHotkey()
|
||||
if current == nil {
|
||||
t.Error("Current hotkey should not be nil")
|
||||
}
|
||||
|
||||
if current.Key != combo.Key {
|
||||
t.Errorf("Expected key %s, got %s", combo.Key, current.Key)
|
||||
}
|
||||
|
||||
// 测试注销
|
||||
err = service.UnregisterHotkey()
|
||||
if err != nil {
|
||||
t.Fatalf("Unregister failed: %v", err)
|
||||
}
|
||||
|
||||
if service.IsRegistered() {
|
||||
t.Error("Service should not be registered after unregister")
|
||||
}
|
||||
|
||||
current = service.GetCurrentHotkey()
|
||||
if current != nil {
|
||||
t.Error("Current hotkey should be nil after unregister")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHotkeyUpdate 测试更新热键
|
||||
func TestHotkeyUpdate(t *testing.T) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
combo1 := &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Key: "F11",
|
||||
}
|
||||
|
||||
// 启用热键
|
||||
err := service.UpdateHotkey(true, combo1)
|
||||
if err != nil {
|
||||
t.Logf("Update (enable) failed: %v", err)
|
||||
return
|
||||
}
|
||||
defer service.UnregisterHotkey()
|
||||
|
||||
if !service.IsRegistered() {
|
||||
t.Error("Should be registered after enable")
|
||||
}
|
||||
|
||||
// 禁用热键
|
||||
err = service.UpdateHotkey(false, combo1)
|
||||
if err != nil {
|
||||
t.Fatalf("Update (disable) failed: %v", err)
|
||||
}
|
||||
|
||||
if service.IsRegistered() {
|
||||
t.Error("Should not be registered after disable")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHotkeyDoubleRegister 测试重复注册
|
||||
func TestHotkeyDoubleRegister(t *testing.T) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
combo := &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Alt: true,
|
||||
Key: "F12",
|
||||
}
|
||||
|
||||
err := service.RegisterHotkey(combo)
|
||||
if err != nil {
|
||||
t.Skip("First registration failed")
|
||||
}
|
||||
defer service.UnregisterHotkey()
|
||||
|
||||
// 第二次注册应该先取消第一次注册,然后重新注册
|
||||
combo2 := &models.HotkeyCombo{
|
||||
Shift: true,
|
||||
Key: "F12",
|
||||
}
|
||||
|
||||
err = service.RegisterHotkey(combo2)
|
||||
if err != nil {
|
||||
t.Logf("Second registration failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证当前热键是新的
|
||||
current := service.GetCurrentHotkey()
|
||||
if current != nil && current.Shift != combo2.Shift {
|
||||
t.Error("Should have updated to new hotkey")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHotkeyConcurrentAccess 测试并发访问
|
||||
func TestHotkeyConcurrentAccess(t *testing.T) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
combo := &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Key: "G",
|
||||
}
|
||||
|
||||
const goroutines = 10
|
||||
done := make(chan bool, goroutines)
|
||||
|
||||
// 并发读取
|
||||
for i := 0; i < goroutines; i++ {
|
||||
go func() {
|
||||
for j := 0; j < 100; j++ {
|
||||
_ = service.IsRegistered()
|
||||
_ = service.GetCurrentHotkey()
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
}
|
||||
|
||||
// 主协程进行注册/注销操作
|
||||
go func() {
|
||||
for i := 0; i < 5; i++ {
|
||||
service.RegisterHotkey(combo)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
service.UnregisterHotkey()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待所有 goroutine 完成
|
||||
for i := 0; i < goroutines; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
t.Log("Concurrent access test completed without panics")
|
||||
}
|
||||
|
||||
// TestHotkeyServiceShutdown 测试服务关闭
|
||||
func TestHotkeyServiceShutdown(t *testing.T) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
combo := &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Shift: true,
|
||||
Key: "H",
|
||||
}
|
||||
|
||||
err := service.RegisterHotkey(combo)
|
||||
if err != nil {
|
||||
t.Skip("Registration failed")
|
||||
}
|
||||
|
||||
// 测试 ServiceShutdown
|
||||
err = service.ServiceShutdown()
|
||||
if err != nil {
|
||||
t.Fatalf("ServiceShutdown failed: %v", err)
|
||||
}
|
||||
|
||||
if service.IsRegistered() {
|
||||
t.Error("Should not be registered after shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkHotkeyRegistration 基准测试:热键注册
|
||||
func BenchmarkHotkeyRegistration(b *testing.B) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
combo := &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Key: "B",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
service.RegisterHotkey(combo)
|
||||
service.UnregisterHotkey()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkHotkeyConversion 基准测试:热键转换
|
||||
func BenchmarkHotkeyConversion(b *testing.B) {
|
||||
logger := log.New()
|
||||
service := NewHotkeyService(&ConfigService{}, logger)
|
||||
|
||||
combo := &models.HotkeyCombo{
|
||||
Ctrl: true,
|
||||
Shift: true,
|
||||
Alt: true,
|
||||
Key: "F5",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
service.convertHotkey(combo)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/wailsapp/wails/v3/pkg/services/dock"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// ServiceManager 服务管理器,负责协调各个服务
|
||||
@@ -36,7 +37,9 @@ type ServiceManager struct {
|
||||
// NewServiceManager 创建新的服务管理器实例
|
||||
func NewServiceManager() *ServiceManager {
|
||||
// 初始化日志服务
|
||||
logger := log.New()
|
||||
logger := log.NewWithConfig(&log.Config{
|
||||
LogLevel: slog.LevelDebug,
|
||||
})
|
||||
|
||||
// 初始化badge服务
|
||||
badgeService := dock.New()
|
||||
@@ -51,7 +54,7 @@ func NewServiceManager() *ServiceManager {
|
||||
databaseService := NewDatabaseService(configService, logger)
|
||||
|
||||
// 初始化迁移服务
|
||||
migrationService := NewMigrationService(databaseService, logger)
|
||||
migrationService := NewMigrationService(databaseService, configService, logger)
|
||||
|
||||
// 初始化文档服务
|
||||
documentService := NewDocumentService(databaseService, logger)
|
||||
|
||||
@@ -113,7 +113,7 @@ func (ws *WindowService) onWindowClosing(documentID int64) {
|
||||
}
|
||||
|
||||
// GetOpenWindows 获取所有打开的文档窗口
|
||||
func (ws *WindowService) GetOpenWindows() []application.Window {
|
||||
func (ws *WindowService) getOpenWindows() []application.Window {
|
||||
app := application.Get()
|
||||
return app.Window.GetAll()
|
||||
}
|
||||
@@ -130,7 +130,7 @@ func (ws *WindowService) IsDocumentWindowOpen(documentID int64) bool {
|
||||
func (ws *WindowService) ServiceShutdown() error {
|
||||
// 从吸附服务中取消注册所有窗口
|
||||
if ws.windowSnapService != nil {
|
||||
windows := ws.GetOpenWindows()
|
||||
windows := ws.getOpenWindows()
|
||||
for _, window := range windows {
|
||||
if documentID, err := strconv.ParseInt(window.Name(), 10, 64); err == nil {
|
||||
ws.windowSnapService.UnregisterWindow(documentID)
|
||||
|
||||
@@ -91,7 +91,7 @@ type WindowSnapService struct {
|
||||
windowMoveUnhooks map[int64]func() // documentID -> 子窗口移动监听清理函数
|
||||
|
||||
// 配置观察者取消函数
|
||||
cancelObserver CancelFunc
|
||||
cancelObserver helper.CancelFunc
|
||||
}
|
||||
|
||||
// NewWindowSnapService 创建新的窗口吸附服务实例
|
||||
|
||||
Reference in New Issue
Block a user