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:
|
jobs:
|
||||||
# 准备构建配置
|
# 准备构建配置
|
||||||
prepare:
|
prepare:
|
||||||
|
permissions: {}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||||
@@ -84,6 +85,8 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
needs: prepare
|
needs: prepare
|
||||||
if: ${{ fromJson(needs.prepare.outputs.matrix).include[0] != null }}
|
if: ${{ fromJson(needs.prepare.outputs.matrix).include[0] != null }}
|
||||||
strategy:
|
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://opensource.org/licenses/MIT)
|
||||||
[](https://github.com/yourusername/voidraft)
|
[](https://github.com/yourusername/voidraft)
|
||||||
[](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>);
|
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 for cross-device sync (UUIDv7)
|
||||||
*/
|
*/
|
||||||
"uuid": string;
|
"uuid": string | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* creation time
|
* creation time
|
||||||
@@ -56,7 +56,7 @@ export class Document {
|
|||||||
/** Creates a new Document instance. */
|
/** Creates a new Document instance. */
|
||||||
constructor($$source: Partial<Document> = {}) {
|
constructor($$source: Partial<Document> = {}) {
|
||||||
if (!("uuid" in $$source)) {
|
if (!("uuid" in $$source)) {
|
||||||
this["uuid"] = "";
|
this["uuid"] = null;
|
||||||
}
|
}
|
||||||
if (!("created_at" in $$source)) {
|
if (!("created_at" in $$source)) {
|
||||||
this["created_at"] = "";
|
this["created_at"] = "";
|
||||||
@@ -98,7 +98,7 @@ export class Extension {
|
|||||||
/**
|
/**
|
||||||
* UUID for cross-device sync (UUIDv7)
|
* UUID for cross-device sync (UUIDv7)
|
||||||
*/
|
*/
|
||||||
"uuid": string;
|
"uuid": string | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* creation time
|
* creation time
|
||||||
@@ -133,7 +133,7 @@ export class Extension {
|
|||||||
/** Creates a new Extension instance. */
|
/** Creates a new Extension instance. */
|
||||||
constructor($$source: Partial<Extension> = {}) {
|
constructor($$source: Partial<Extension> = {}) {
|
||||||
if (!("uuid" in $$source)) {
|
if (!("uuid" in $$source)) {
|
||||||
this["uuid"] = "";
|
this["uuid"] = null;
|
||||||
}
|
}
|
||||||
if (!("created_at" in $$source)) {
|
if (!("created_at" in $$source)) {
|
||||||
this["created_at"] = "";
|
this["created_at"] = "";
|
||||||
@@ -179,7 +179,7 @@ export class KeyBinding {
|
|||||||
/**
|
/**
|
||||||
* UUID for cross-device sync (UUIDv7)
|
* UUID for cross-device sync (UUIDv7)
|
||||||
*/
|
*/
|
||||||
"uuid": string;
|
"uuid": string | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* creation time
|
* creation time
|
||||||
@@ -219,7 +219,7 @@ export class KeyBinding {
|
|||||||
/** Creates a new KeyBinding instance. */
|
/** Creates a new KeyBinding instance. */
|
||||||
constructor($$source: Partial<KeyBinding> = {}) {
|
constructor($$source: Partial<KeyBinding> = {}) {
|
||||||
if (!("uuid" in $$source)) {
|
if (!("uuid" in $$source)) {
|
||||||
this["uuid"] = "";
|
this["uuid"] = null;
|
||||||
}
|
}
|
||||||
if (!("created_at" in $$source)) {
|
if (!("created_at" in $$source)) {
|
||||||
this["created_at"] = "";
|
this["created_at"] = "";
|
||||||
@@ -261,7 +261,7 @@ export class Theme {
|
|||||||
/**
|
/**
|
||||||
* UUID for cross-device sync (UUIDv7)
|
* UUID for cross-device sync (UUIDv7)
|
||||||
*/
|
*/
|
||||||
"uuid": string;
|
"uuid": string | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* creation time
|
* creation time
|
||||||
@@ -296,7 +296,7 @@ export class Theme {
|
|||||||
/** Creates a new Theme instance. */
|
/** Creates a new Theme instance. */
|
||||||
constructor($$source: Partial<Theme> = {}) {
|
constructor($$source: Partial<Theme> = {}) {
|
||||||
if (!("uuid" in $$source)) {
|
if (!("uuid" in $$source)) {
|
||||||
this["uuid"] = "";
|
this["uuid"] = null;
|
||||||
}
|
}
|
||||||
if (!("created_at" in $$source)) {
|
if (!("created_at" in $$source)) {
|
||||||
this["created_at"] = "";
|
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";
|
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore: Unused imports
|
// @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
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore: Unused imports
|
// @ts-ignore: Unused imports
|
||||||
import * as $models from "./models.js";
|
import * as models$0 from "../models/models.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get 获取配置项
|
* Get 获取配置项
|
||||||
@@ -50,7 +49,7 @@ export function MigrateConfig(): Promise<void> & { cancel(): void } {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ResetConfig 强制重置所有配置为默认值
|
* ResetConfig 重置所有配置为默认值
|
||||||
*/
|
*/
|
||||||
export function ResetConfig(): Promise<void> & { cancel(): void } {
|
export function ResetConfig(): Promise<void> & { cancel(): void } {
|
||||||
let $resultPromise = $Call.ByID(3593047389) as any;
|
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 } {
|
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||||
let $resultPromise = $Call.ByID(3311949428, options) as any;
|
let $resultPromise = $Call.ByID(3311949428, options) as any;
|
||||||
@@ -84,7 +83,7 @@ export function Set(key: string, value: any): Promise<void> & { cancel(): void }
|
|||||||
/**
|
/**
|
||||||
* Watch 注册配置变更监听器
|
* 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;
|
let $resultPromise = $Call.ByID(1143583035, path, callback) as any;
|
||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
@@ -92,7 +91,7 @@ export function Watch(path: string, callback: $models.ObserverCallback): Promise
|
|||||||
/**
|
/**
|
||||||
* WatchWithContext 使用 Context 注册监听器
|
* 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;
|
let $resultPromise = $Call.ByID(1454973098, path, callback) as any;
|
||||||
return $resultPromise;
|
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";
|
import * as models$0 from "../models/models.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GetCurrentHotkey 获取当前热键
|
* GetSupportedKeys 返回系统支持的快捷键列表
|
||||||
*/
|
*/
|
||||||
export function GetCurrentHotkey(): Promise<models$0.HotkeyCombo | null> & { cancel(): void } {
|
export function GetSupportedKeys(): Promise<string[]> & { cancel(): void } {
|
||||||
let $resultPromise = $Call.ByID(2572811187) as any;
|
let $resultPromise = $Call.ByID(1511528650) as any;
|
||||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||||
return $$createType1($result);
|
return $$createType0($result);
|
||||||
}) as any;
|
}) as any;
|
||||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||||
return $typingPromise;
|
return $typingPromise;
|
||||||
@@ -86,5 +86,4 @@ export function UpdateHotkey(enable: boolean, combo: models$0.HotkeyCombo | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Private type creation functions
|
// Private type creation functions
|
||||||
const $$createType0 = models$0.HotkeyCombo.createFrom;
|
const $$createType0 = $Create.Array($Create.Any);
|
||||||
const $$createType1 = $Create.Nullable($$createType0);
|
|
||||||
|
|||||||
@@ -12,12 +12,6 @@ import * as http$0 from "../../../net/http/models.js";
|
|||||||
// @ts-ignore: Unused imports
|
// @ts-ignore: Unused imports
|
||||||
import * as time$0 from "../../../time/models.js";
|
import * as time$0 from "../../../time/models.js";
|
||||||
|
|
||||||
/**
|
|
||||||
* CancelFunc 取消订阅函数
|
|
||||||
* 调用此函数可以取消对配置的监听
|
|
||||||
*/
|
|
||||||
export type CancelFunc = any;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HttpRequest HTTP请求结构
|
* HttpRequest HTTP请求结构
|
||||||
*/
|
*/
|
||||||
@@ -251,11 +245,6 @@ export class OSInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ObserverCallback 观察者回调函数
|
|
||||||
*/
|
|
||||||
export type ObserverCallback = any;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SelfUpdateResult 自我更新结果
|
* SelfUpdateResult 自我更新结果
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,18 +14,6 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
|||||||
// @ts-ignore: Unused imports
|
// @ts-ignore: Unused imports
|
||||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
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 检查指定文档的窗口是否已打开
|
* IsDocumentWindowOpen 检查指定文档的窗口是否已打开
|
||||||
*/
|
*/
|
||||||
@@ -57,6 +45,3 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
|
|||||||
let $resultPromise = $Call.ByID(2432987694, options) as any;
|
let $resultPromise = $Call.ByID(2432987694, options) as any;
|
||||||
return $resultPromise;
|
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": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.20.0",
|
"@codemirror/autocomplete": "^6.20.0",
|
||||||
"@codemirror/commands": "^6.10.0",
|
"@codemirror/commands": "^6.10.1",
|
||||||
"@codemirror/lang-angular": "^0.1.4",
|
"@codemirror/lang-angular": "^0.1.4",
|
||||||
"@codemirror/lang-cpp": "^6.0.3",
|
"@codemirror/lang-cpp": "^6.0.3",
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"@codemirror/lang-json": "^6.0.2",
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
"@codemirror/lang-less": "^6.0.2",
|
"@codemirror/lang-less": "^6.0.2",
|
||||||
"@codemirror/lang-lezer": "^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-markdown": "^6.5.0",
|
||||||
"@codemirror/lang-php": "^6.0.2",
|
"@codemirror/lang-php": "^6.0.2",
|
||||||
"@codemirror/lang-python": "^6.2.1",
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
@@ -50,10 +50,10 @@
|
|||||||
"@codemirror/lint": "^6.9.2",
|
"@codemirror/lint": "^6.9.2",
|
||||||
"@codemirror/search": "^6.5.11",
|
"@codemirror/search": "^6.5.11",
|
||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/view": "^6.38.8",
|
"@codemirror/view": "^6.39.4",
|
||||||
"@cospaia/prettier-plugin-clojure": "^0.0.2",
|
"@cospaia/prettier-plugin-clojure": "^0.0.2",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@lezer/lr": "^1.4.4",
|
"@lezer/lr": "^1.4.5",
|
||||||
"@prettier/plugin-xml": "^3.4.2",
|
"@prettier/plugin-xml": "^3.4.2",
|
||||||
"@replit/codemirror-lang-svelte": "^6.0.0",
|
"@replit/codemirror-lang-svelte": "^6.0.0",
|
||||||
"@toml-tools/lexer": "^1.0.0",
|
"@toml-tools/lexer": "^1.0.0",
|
||||||
@@ -61,45 +61,45 @@
|
|||||||
"@types/katex": "^0.16.7",
|
"@types/katex": "^0.16.7",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"codemirror-lang-elixir": "^4.0.0",
|
"codemirror-lang-elixir": "^4.0.0",
|
||||||
"colors-named": "^1.0.2",
|
"colors-named": "^1.0.4",
|
||||||
"colors-named-hex": "^1.0.2",
|
"colors-named-hex": "^1.0.3",
|
||||||
"groovy-beautify": "^0.0.17",
|
"groovy-beautify": "^0.0.17",
|
||||||
"hsl-matcher": "^1.2.4",
|
"hsl-matcher": "^1.2.4",
|
||||||
"java-parser": "^3.0.1",
|
"java-parser": "^3.0.1",
|
||||||
"katex": "^0.16.25",
|
"katex": "^0.16.27",
|
||||||
"linguist-languages": "^9.1.0",
|
"linguist-languages": "^9.1.11",
|
||||||
"marked": "^17.0.1",
|
"marked": "^17.0.1",
|
||||||
"mermaid": "^11.12.1",
|
"mermaid": "^11.12.2",
|
||||||
"php-parser": "^3.2.5",
|
"php-parser": "^3.2.5",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"pinia-plugin-persistedstate": "^4.7.1",
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
"prettier": "^3.7.2",
|
"prettier": "^3.7.4",
|
||||||
"sass": "^1.94.2",
|
"sass": "^1.97.0",
|
||||||
"vue": "^3.5.25",
|
"vue": "^3.5.25",
|
||||||
"vue-i18n": "^11.2.2",
|
"vue-i18n": "^11.2.2",
|
||||||
"vue-pick-colors": "^1.8.0",
|
"vue-pick-colors": "^1.8.0",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.2",
|
||||||
"@lezer/generator": "^1.8.0",
|
"@lezer/generator": "^1.8.0",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^25.0.3",
|
||||||
"@vitejs/plugin-vue": "^6.0.2",
|
"@vitejs/plugin-vue": "^6.0.3",
|
||||||
"@wailsio/runtime": "latest",
|
"@wailsio/runtime": "^3.0.0-alpha.76",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-vue": "^10.6.2",
|
"eslint-plugin-vue": "^10.6.2",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
"happy-dom": "^20.0.11",
|
"happy-dom": "^20.0.11",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.48.0",
|
"typescript-eslint": "^8.50.0",
|
||||||
"unplugin-vue-components": "^30.0.0",
|
"unplugin-vue-components": "^30.0.0",
|
||||||
"vite": "npm:rolldown-vite@latest",
|
"vite": "npm:rolldown-vite@latest",
|
||||||
"vite-plugin-node-polyfills": "^0.24.0",
|
"vite-plugin-node-polyfills": "^0.24.0",
|
||||||
"vitepress": "^2.0.0-alpha.12",
|
"vitepress": "^2.0.0-alpha.12",
|
||||||
"vitest": "^4.0.14",
|
"vitest": "^4.0.16",
|
||||||
"vue-eslint-parser": "^10.2.0",
|
"vue-eslint-parser": "^10.2.0",
|
||||||
"vue-tsc": "^3.1.5"
|
"vue-tsc": "^3.1.8"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"vite": "npm:rolldown-vite@latest"
|
"vite": "npm:rolldown-vite@latest"
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
setFontWeight: (value: string) => updateConfig('fontWeight', value),
|
setFontWeight: (value: string) => updateConfig('fontWeight', value),
|
||||||
|
|
||||||
// 路径操作
|
// 路径操作
|
||||||
setDataPath: (value: string) => updateConfig('dataPath', value),
|
setDataPath: (value: string) => updateConfigLocal('dataPath', value),
|
||||||
|
|
||||||
// 保存配置相关方法
|
// 保存配置相关方法
|
||||||
setAutoSaveDelay: (value: number) => updateConfig('autoSaveDelay', value),
|
setAutoSaveDelay: (value: number) => updateConfig('autoSaveDelay', value),
|
||||||
|
|||||||
@@ -70,12 +70,11 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
// 在新窗口中打开文档
|
// 在新窗口中打开文档
|
||||||
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
|
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
await OpenDocumentWindow(docId);
|
|
||||||
const tabStore = useTabStore();
|
const tabStore = useTabStore();
|
||||||
if (tabStore.isTabsEnabled && tabStore.hasTab(docId)) {
|
if (tabStore.isTabsEnabled && tabStore.hasTab(docId)) {
|
||||||
tabStore.closeTab(docId);
|
tabStore.closeTab(docId);
|
||||||
}
|
}
|
||||||
|
await OpenDocumentWindow(docId);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to open document in new window:', 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 {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
|
||||||
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
|
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
|
||||||
import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension';
|
import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension';
|
||||||
|
import {createCursorPositionExtension, scrollToCursor} from '@/views/editor/basic/cursorPositionExtension';
|
||||||
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
|
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
|
||||||
import {
|
import {
|
||||||
createDynamicExtensions,
|
createDynamicExtensions,
|
||||||
@@ -21,7 +22,7 @@ import {
|
|||||||
setExtensionManagerView
|
setExtensionManagerView
|
||||||
} from '@/views/editor/manager';
|
} from '@/views/editor/manager';
|
||||||
import {useExtensionStore} from './extensionStore';
|
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 {LruCache} from '@/common/utils/lruCache';
|
||||||
import {AsyncManager} from '@/common/utils/asyncManager';
|
import {AsyncManager} from '@/common/utils/asyncManager';
|
||||||
import {generateContentHash} from "@/common/utils/hashUtils";
|
import {generateContentHash} from "@/common/utils/hashUtils";
|
||||||
@@ -35,7 +36,7 @@ export interface DocumentStats {
|
|||||||
selectedCharacters: number;
|
selectedCharacters: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修复:只保存光标位置,恢复时自动滚动到光标处(更简单可靠)
|
// 修复:只保存光标位置,恢复时自动滚动到光标处
|
||||||
export interface EditorViewState {
|
export interface EditorViewState {
|
||||||
cursorPos: number;
|
cursorPos: number;
|
||||||
}
|
}
|
||||||
@@ -180,6 +181,9 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
enableAutoDetection: true
|
enableAutoDetection: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 光标位置持久化扩展
|
||||||
|
const cursorPositionExtension = createCursorPositionExtension(documentId);
|
||||||
|
|
||||||
// 再次检查操作有效性
|
// 再次检查操作有效性
|
||||||
if (!operationManager.isOperationValid(operationId, documentId)) {
|
if (!operationManager.isOperationValid(operationId, documentId)) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
@@ -212,13 +216,23 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
statsExtension,
|
statsExtension,
|
||||||
contentChangeExtension,
|
contentChangeExtension,
|
||||||
codeBlockExtension,
|
codeBlockExtension,
|
||||||
|
cursorPositionExtension,
|
||||||
...dynamicExtensions,
|
...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({
|
const state = EditorState.create({
|
||||||
doc: content,
|
doc: content,
|
||||||
extensions
|
extensions,
|
||||||
|
selection: { anchor: initialCursorPos, head: initialCursorPos }
|
||||||
});
|
});
|
||||||
|
|
||||||
return new EditorView({
|
return new EditorView({
|
||||||
@@ -316,6 +330,9 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
//使用 nextTick + requestAnimationFrame 确保 DOM 完全渲染
|
//使用 nextTick + requestAnimationFrame 确保 DOM 完全渲染
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
|
// 滚动到当前光标位置
|
||||||
|
scrollToCursor(instance.view);
|
||||||
|
|
||||||
// 聚焦编辑器
|
// 聚焦编辑器
|
||||||
instance.view.focus();
|
instance.view.focus();
|
||||||
|
|
||||||
@@ -487,15 +504,6 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
await saveEditorContent(documentId);
|
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();
|
instance.autoSaveTimer.clear();
|
||||||
|
|
||||||
@@ -578,22 +586,10 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
operationManager.cancelAllOperations();
|
operationManager.cancelAllOperations();
|
||||||
|
|
||||||
editorCache.clear((_documentId, instance) => {
|
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();
|
instance.autoSaveTimer.clear();
|
||||||
|
|
||||||
// 从扩展管理器移除
|
// 从扩展管理器移除
|
||||||
removeExtensionManagerView(instance.documentId);
|
removeExtensionManagerView(instance.documentId);
|
||||||
|
|
||||||
// 移除DOM元素
|
// 移除DOM元素
|
||||||
if (instance.view.dom.parentElement) {
|
if (instance.view.dom.parentElement) {
|
||||||
instance.view.dom.remove();
|
instance.view.dom.remove();
|
||||||
|
|||||||
@@ -156,10 +156,10 @@ export const useTabStore = defineStore('tab', () => {
|
|||||||
*/
|
*/
|
||||||
const validateTabs = () => {
|
const validateTabs = () => {
|
||||||
const validDocIds = Object.keys(documentStore.documents).map(Number);
|
const validDocIds = Object.keys(documentStore.documents).map(Number);
|
||||||
|
|
||||||
// 找出无效的标签页(文档已被删除)
|
// 找出无效的标签页(文档已被删除)
|
||||||
const invalidTabIds = tabOrder.value.filter(docId => !validDocIds.includes(docId));
|
const invalidTabIds = tabOrder.value.filter(docId => !validDocIds.includes(docId));
|
||||||
|
|
||||||
if (invalidTabIds.length > 0) {
|
if (invalidTabIds.length > 0) {
|
||||||
// 批量清理无效标签页
|
// 批量清理无效标签页
|
||||||
invalidTabIds.forEach(docId => {
|
invalidTabIds.forEach(docId => {
|
||||||
@@ -175,7 +175,7 @@ export const useTabStore = defineStore('tab', () => {
|
|||||||
const initializeTab = () => {
|
const initializeTab = () => {
|
||||||
// 先验证并清理无效的标签页(处理持久化的脏数据)
|
// 先验证并清理无效的标签页(处理持久化的脏数据)
|
||||||
validateTabs();
|
validateTabs();
|
||||||
|
|
||||||
if (isTabsEnabled.value) {
|
if (isTabsEnabled.value) {
|
||||||
const currentDoc = documentStore.currentDocument;
|
const currentDoc = documentStore.currentDocument;
|
||||||
if (currentDoc) {
|
if (currentDoc) {
|
||||||
@@ -254,7 +254,7 @@ export const useTabStore = defineStore('tab', () => {
|
|||||||
return {
|
return {
|
||||||
tabsMap,
|
tabsMap,
|
||||||
tabOrder,
|
tabOrder,
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
tabs: readonly(tabs),
|
tabs: readonly(tabs),
|
||||||
draggedTabId,
|
draggedTabId,
|
||||||
@@ -283,9 +283,5 @@ export const useTabStore = defineStore('tab', () => {
|
|||||||
getTab
|
getTab
|
||||||
};
|
};
|
||||||
}, {
|
}, {
|
||||||
persist: {
|
persist: false,
|
||||||
key: 'voidraft-tabs',
|
|
||||||
storage: localStorage,
|
|
||||||
pick: ['tabsMap', 'tabOrder'],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
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
|
return keyBinding
|
||||||
.replace("Mod", isMac ? "Cmd" : "Ctrl")
|
.replace("Mod", isMac ? "Cmd" : "Ctrl")
|
||||||
.replace("Shift", "Shift")
|
|
||||||
.replace("Alt", isMac ? "Option" : "Alt")
|
.replace("Alt", isMac ? "Option" : "Alt")
|
||||||
.replace("Ctrl", isMac ? "Ctrl" : "Ctrl")
|
|
||||||
.replace(/-/g, " + ");
|
.replace(/-/g, " + ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,28 @@
|
|||||||
import {useConfigStore} from '@/stores/configStore';
|
import {useConfigStore} from '@/stores/configStore';
|
||||||
import {useTabStore} from '@/stores/tabStore';
|
import {useTabStore} from '@/stores/tabStore';
|
||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import {computed, ref} from 'vue';
|
import {computed, ref, onMounted} from 'vue';
|
||||||
import SettingSection from '../components/SettingSection.vue';
|
import SettingSection from '../components/SettingSection.vue';
|
||||||
import SettingItem from '../components/SettingItem.vue';
|
import SettingItem from '../components/SettingItem.vue';
|
||||||
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||||
import {DialogService, MigrationService} from '@/../bindings/voidraft/internal/services';
|
import {DialogService, HotkeyService, MigrationService} from '@/../bindings/voidraft/internal/services';
|
||||||
import {useSystemStore} from "@/stores/systemStore";
|
import {useSystemStore} from "@/stores/systemStore";
|
||||||
import {useConfirm, usePolling} from '@/composables';
|
import {useConfirm, usePolling} from '@/composables';
|
||||||
|
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
const configStore = useConfigStore();
|
const {
|
||||||
|
config: {general},
|
||||||
|
resetConfig,
|
||||||
|
setAlwaysOnTop,
|
||||||
|
setDataPath,
|
||||||
|
setEnableGlobalHotkey,
|
||||||
|
setEnableLoadingAnimation,
|
||||||
|
setEnableSystemTray,
|
||||||
|
setEnableTabs,
|
||||||
|
setEnableWindowSnap,
|
||||||
|
setGlobalHotkey,
|
||||||
|
setStartAtLogin
|
||||||
|
} = useConfigStore();
|
||||||
const systemStore = useSystemStore();
|
const systemStore = useSystemStore();
|
||||||
const tabStore = useTabStore();
|
const tabStore = useTabStore();
|
||||||
|
|
||||||
@@ -60,57 +72,57 @@ const hideAll = () => {
|
|||||||
const {isConfirming: isResetConfirming, requestConfirm: requestResetConfirm} = useConfirm({
|
const {isConfirming: isResetConfirming, requestConfirm: requestResetConfirm} = useConfirm({
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await configStore.resetConfig();
|
await resetConfig();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 可选键列表
|
// 可选键列表 - 从后端获取系统支持的快捷键
|
||||||
const keyOptions = [
|
const keyOptions = ref<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',
|
onMounted(async () => {
|
||||||
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12'
|
keyOptions.value = await HotkeyService.GetSupportedKeys();
|
||||||
];
|
});
|
||||||
|
|
||||||
// 计算属性 - 启用全局热键
|
// 计算属性 - 启用全局热键
|
||||||
const enableGlobalHotkey = computed({
|
const enableGlobalHotkey = computed({
|
||||||
get: () => configStore.config.general.enableGlobalHotkey,
|
get: () => general.enableGlobalHotkey,
|
||||||
set: (value: boolean) => configStore.setEnableGlobalHotkey(value)
|
set: (value: boolean) => setEnableGlobalHotkey(value)
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算属性 - 窗口始终置顶
|
// 计算属性 - 窗口始终置顶
|
||||||
const alwaysOnTop = computed({
|
const alwaysOnTop = computed({
|
||||||
get: () => configStore.config.general.alwaysOnTop,
|
get: () => general.alwaysOnTop,
|
||||||
set: async (value: boolean) => {
|
set: async (value: boolean) => {
|
||||||
// 先更新配置
|
// 先更新配置
|
||||||
await configStore.setAlwaysOnTop(value);
|
await setAlwaysOnTop(value);
|
||||||
await systemStore.setWindowOnTop(value);
|
await systemStore.setWindowOnTop(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算属性 - 启用系统托盘
|
// 计算属性 - 启用系统托盘
|
||||||
const enableSystemTray = computed({
|
const enableSystemTray = computed({
|
||||||
get: () => configStore.config.general.enableSystemTray,
|
get: () => general.enableSystemTray,
|
||||||
set: (value: boolean) => configStore.setEnableSystemTray(value)
|
set: (value: boolean) => setEnableSystemTray(value)
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算属性 - 启用窗口吸附
|
// 计算属性 - 启用窗口吸附
|
||||||
const enableWindowSnap = computed({
|
const enableWindowSnap = computed({
|
||||||
get: () => configStore.config.general.enableWindowSnap,
|
get: () => general.enableWindowSnap,
|
||||||
set: (value: boolean) => configStore.setEnableWindowSnap(value)
|
set: (value: boolean) => setEnableWindowSnap(value)
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算属性 - 启用加载动画
|
// 计算属性 - 启用加载动画
|
||||||
const enableLoadingAnimation = computed({
|
const enableLoadingAnimation = computed({
|
||||||
get: () => configStore.config.general.enableLoadingAnimation,
|
get: () => general.enableLoadingAnimation,
|
||||||
set: (value: boolean) => configStore.setEnableLoadingAnimation(value)
|
set: (value: boolean) => setEnableLoadingAnimation(value)
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算属性 - 启用标签页
|
// 计算属性 - 启用标签页
|
||||||
const enableTabs = computed({
|
const enableTabs = computed({
|
||||||
get: () => configStore.config.general.enableTabs,
|
get: () => general.enableTabs,
|
||||||
set: async (value: boolean) => {
|
set: async (value: boolean) => {
|
||||||
await configStore.setEnableTabs(value);
|
await setEnableTabs(value);
|
||||||
if (value) {
|
if (value) {
|
||||||
// 开启tabs功能时,初始化当前文档到标签页
|
// 开启tabs功能时,初始化当前文档到标签页
|
||||||
tabStore.initializeTab();
|
tabStore.initializeTab();
|
||||||
@@ -123,33 +135,33 @@ const enableTabs = computed({
|
|||||||
|
|
||||||
// 计算属性 - 开机启动
|
// 计算属性 - 开机启动
|
||||||
const startAtLogin = computed({
|
const startAtLogin = computed({
|
||||||
get: () => configStore.config.general.startAtLogin,
|
get: () => general.startAtLogin,
|
||||||
set: (value: boolean) => configStore.setStartAtLogin(value)
|
set: (value: boolean) => setStartAtLogin(value)
|
||||||
});
|
});
|
||||||
|
|
||||||
// 修饰键配置 - 只读计算属性
|
// 修饰键配置 - 只读计算属性
|
||||||
const modifierKeys = computed(() => ({
|
const modifierKeys = computed(() => ({
|
||||||
ctrl: configStore.config.general.globalHotkey.ctrl,
|
ctrl: general.globalHotkey.ctrl,
|
||||||
shift: configStore.config.general.globalHotkey.shift,
|
shift: general.globalHotkey.shift,
|
||||||
alt: configStore.config.general.globalHotkey.alt,
|
alt: general.globalHotkey.alt,
|
||||||
win: configStore.config.general.globalHotkey.win
|
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 toggleModifier = (key: 'ctrl' | 'shift' | 'alt' | 'win') => {
|
||||||
const currentHotkey = configStore.config.general.globalHotkey;
|
const currentHotkey = general.globalHotkey;
|
||||||
const newHotkey = {...currentHotkey, [key]: !currentHotkey[key]};
|
const newHotkey = {...currentHotkey, [key]: !currentHotkey[key]};
|
||||||
configStore.setGlobalHotkey(newHotkey);
|
setGlobalHotkey(newHotkey);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新选择的键
|
// 更新选择的键
|
||||||
const updateSelectedKey = (event: Event) => {
|
const updateSelectedKey = (event: Event) => {
|
||||||
const select = event.target as HTMLSelectElement;
|
const select = event.target as HTMLSelectElement;
|
||||||
const newHotkey = {...configStore.config.general.globalHotkey, key: select.value};
|
const newHotkey = {...general.globalHotkey, key: select.value};
|
||||||
configStore.setGlobalHotkey(newHotkey);
|
setGlobalHotkey(newHotkey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -157,7 +169,7 @@ const updateSelectedKey = (event: Event) => {
|
|||||||
const hotkeyPreview = computed(() => {
|
const hotkeyPreview = computed(() => {
|
||||||
if (!enableGlobalHotkey.value) return '';
|
if (!enableGlobalHotkey.value) return '';
|
||||||
|
|
||||||
const {ctrl, shift, alt, win, key} = configStore.config.general.globalHotkey;
|
const {ctrl, shift, alt, win, key} = general.globalHotkey;
|
||||||
const modifiers = [
|
const modifiers = [
|
||||||
ctrl && 'Ctrl',
|
ctrl && 'Ctrl',
|
||||||
shift && 'Shift',
|
shift && 'Shift',
|
||||||
@@ -170,7 +182,7 @@ const hotkeyPreview = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 数据路径配置
|
// 数据路径配置
|
||||||
const currentDataPath = computed(() => configStore.config.general.dataPath);
|
const currentDataPath = computed(() => general.dataPath);
|
||||||
|
|
||||||
// 选择数据存储目录
|
// 选择数据存储目录
|
||||||
const selectDataDirectory = async () => {
|
const selectDataDirectory = async () => {
|
||||||
@@ -189,7 +201,7 @@ const selectDataDirectory = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await MigrationService.MigrateDirectory(oldPath, newPath);
|
await MigrationService.MigrateDirectory(oldPath, newPath);
|
||||||
await configStore.setDataPath(newPath);
|
await setDataPath(newPath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
stop();
|
stop();
|
||||||
// 设置手动捕获的错误(当轮询还没获取到错误时)
|
// 设置手动捕获的错误(当轮询还没获取到错误时)
|
||||||
@@ -314,10 +326,6 @@ const selectDataDirectory = async () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.settings-page {
|
|
||||||
//max-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hotkey-selector {
|
.hotkey-selector {
|
||||||
padding: 15px 0 5px 20px;
|
padding: 15px 0 5px 20px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|||||||
35
go.mod
35
go.mod
@@ -5,7 +5,7 @@ go 1.25
|
|||||||
require (
|
require (
|
||||||
entgo.io/ent v0.14.5
|
entgo.io/ent v0.14.5
|
||||||
github.com/creativeprojects/go-selfupdate v1.5.1
|
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/google/uuid v1.6.0
|
||||||
github.com/knadh/koanf/parsers/json v1.0.0
|
github.com/knadh/koanf/parsers/json v1.0.0
|
||||||
github.com/knadh/koanf/providers/file v1.2.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/knadh/koanf/v2 v2.3.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.32
|
github.com/mattn/go-sqlite3 v1.14.32
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.41
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.48
|
||||||
golang.org/x/net v0.47.0
|
golang.org/x/net v0.48.0
|
||||||
golang.org/x/sys v0.38.0
|
golang.org/x/sys v0.39.0
|
||||||
golang.org/x/text v0.31.0
|
golang.org/x/text v0.32.0
|
||||||
resty.dev/v3 v3.0.0-beta.3
|
resty.dev/v3 v3.0.0-beta.5
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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
|
code.gitea.io/sdk/gitea v0.22.1 // indirect
|
||||||
dario.cat/mergo v1.0.2 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||||
@@ -44,9 +44,9 @@ require (
|
|||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-fed/httpsig v1.1.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/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-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/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.2.0 // indirect
|
github.com/godbus/dbus/v5 v5.2.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // 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/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||||
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
|
github.com/hashicorp/hcl/v2 v2.24.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
|
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.4.0 // 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/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // 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/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/samber/lo v1.52.0 // indirect
|
github.com/samber/lo v1.52.0 // indirect
|
||||||
@@ -83,14 +82,16 @@ require (
|
|||||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
github.com/xanzy/go-gitlab v0.115.0 // indirect
|
github.com/xanzy/go-gitlab v0.115.0 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
github.com/zclconf/go-cty v1.14.4 // indirect
|
github.com/zclconf/go-cty v1.17.0 // indirect
|
||||||
github.com/zclconf/go-cty-yaml v1.1.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/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
|
||||||
golang.org/x/image v0.33.0 // indirect
|
golang.org/x/image v0.34.0 // indirect
|
||||||
golang.org/x/mod v0.30.0 // indirect
|
golang.org/x/mod v0.31.0 // indirect
|
||||||
golang.org/x/oauth2 v0.33.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/time v0.14.0 // indirect
|
||||||
|
golang.org/x/tools v0.40.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // 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.38.0 h1:MwbtwVtDWJFq+ECyeTAz2ArvewDnpeiw/t/sgNdDsdo=
|
||||||
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9/go.mod h1:Oe1xWPuu5q9LzyrWfbZmEZxFYeu4BHTyzfjeW2aZp/w=
|
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 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA=
|
||||||
code.gitea.io/sdk/gitea v0.22.1/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
code.gitea.io/sdk/gitea v0.22.1/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
@@ -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-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 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
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.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
|
||||||
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/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 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-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.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
|
||||||
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/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 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
github.com/go-openapi/inflect v0.21.5 h1:M2RCq6PPS3YbIaL7CXosGL3BbzAcmfBAT0nC3YfesZA=
|
||||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
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 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
@@ -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-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 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
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.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo=
|
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
|
||||||
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
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 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
|
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
|
||||||
@@ -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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
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/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 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
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.48 h1:m22PcankYJI/lKbv7NnNekxsEJYPbvIUnvHvH4WD1xQ=
|
||||||
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/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 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
|
||||||
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
|
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 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=
|
github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=
|
||||||
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
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 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=
|
||||||
github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
|
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
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 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
||||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
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.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||||
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
|
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
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-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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
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.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.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -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.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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
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.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.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
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 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||||
|
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
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=
|
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.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E=
|
resty.dev/v3 v3.0.0-beta.5 h1:NV1xbqOLzSq7XMTs1t/HLPvu7xrxoXzF90SR4OO6faQ=
|
||||||
resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=
|
resty.dev/v3 v3.0.0-beta.5/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM=
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package services
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const pathSeparator = '.'
|
||||||
|
|
||||||
// ObserverCallback 观察者回调函数
|
// ObserverCallback 观察者回调函数
|
||||||
type ObserverCallback func(oldValue, newValue interface{})
|
type ObserverCallback func(oldValue, newValue interface{})
|
||||||
|
|
||||||
@@ -49,6 +51,8 @@ func NewConfigObserver(logger *log.LogService) *ConfigObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Watch 注册配置变更监听器
|
// Watch 注册配置变更监听器
|
||||||
|
// 支持前缀监听:注册 "generate" 可以监听 "generate.xxx"、"generate.yyy" 等所有子路径的变化
|
||||||
|
// 返回取消函数,调用后停止监听
|
||||||
func (co *ConfigObserver) Watch(path string, callback ObserverCallback) CancelFunc {
|
func (co *ConfigObserver) Watch(path string, callback ObserverCallback) CancelFunc {
|
||||||
// 生成唯一ID
|
// 生成唯一ID
|
||||||
id := fmt.Sprintf("obs_%d", co.nextObserverID.Add(1))
|
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{}) {
|
func (co *ConfigObserver) Notify(path string, oldValue, newValue interface{}) {
|
||||||
// 获取该路径的所有观察者(拷贝以避免并发问题)
|
|
||||||
co.observerMu.RLock()
|
co.observerMu.RLock()
|
||||||
observers := co.observers[path]
|
callbacks := co.collectCallbacks(path)
|
||||||
if len(observers) == 0 {
|
co.observerMu.RUnlock()
|
||||||
co.observerMu.RUnlock()
|
|
||||||
|
if len(callbacks) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拷贝观察者列表
|
// 执行所有回调
|
||||||
callbacks := make([]ObserverCallback, len(observers))
|
|
||||||
for i, obs := range observers {
|
|
||||||
callbacks[i] = obs.callback
|
|
||||||
}
|
|
||||||
co.observerMu.RUnlock()
|
|
||||||
|
|
||||||
// 在独立 goroutine 中执行回调
|
|
||||||
for _, callback := range callbacks {
|
for _, callback := range callbacks {
|
||||||
co.executeCallback(callback, oldValue, newValue)
|
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 {
|
func (co *ConfigObserver) NotifyAll(changes map[string]struct {
|
||||||
OldValue interface{}
|
OldValue interface{}
|
||||||
NewValue 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 {
|
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 of the ent.
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
// UUID for cross-device sync (UUIDv7)
|
// UUID for cross-device sync (UUIDv7)
|
||||||
UUID string `json:"uuid"`
|
UUID *string `json:"uuid"`
|
||||||
// creation time
|
// creation time
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
// update time
|
// update time
|
||||||
@@ -69,7 +69,8 @@ func (_m *Document) assignValues(columns []string, values []any) error {
|
|||||||
if value, ok := values[i].(*sql.NullString); !ok {
|
if value, ok := values[i].(*sql.NullString); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field uuid", values[i])
|
return fmt.Errorf("unexpected type %T for field uuid", values[i])
|
||||||
} else if value.Valid {
|
} else if value.Valid {
|
||||||
_m.UUID = value.String
|
_m.UUID = new(string)
|
||||||
|
*_m.UUID = value.String
|
||||||
}
|
}
|
||||||
case document.FieldCreatedAt:
|
case document.FieldCreatedAt:
|
||||||
if value, ok := values[i].(*sql.NullString); !ok {
|
if value, ok := values[i].(*sql.NullString); !ok {
|
||||||
@@ -144,8 +145,10 @@ func (_m *Document) String() string {
|
|||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
builder.WriteString("Document(")
|
builder.WriteString("Document(")
|
||||||
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
||||||
builder.WriteString("uuid=")
|
if v := _m.UUID; v != nil {
|
||||||
builder.WriteString(_m.UUID)
|
builder.WriteString("uuid=")
|
||||||
|
builder.WriteString(*v)
|
||||||
|
}
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
builder.WriteString("created_at=")
|
builder.WriteString("created_at=")
|
||||||
builder.WriteString(_m.CreatedAt)
|
builder.WriteString(_m.CreatedAt)
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ func (_c *DocumentCreate) createSpec() (*Document, *sqlgraph.CreateSpec) {
|
|||||||
)
|
)
|
||||||
if value, ok := _c.mutation.UUID(); ok {
|
if value, ok := _c.mutation.UUID(); ok {
|
||||||
_spec.SetField(document.FieldUUID, field.TypeString, value)
|
_spec.SetField(document.FieldUUID, field.TypeString, value)
|
||||||
_node.UUID = value
|
_node.UUID = &value
|
||||||
}
|
}
|
||||||
if value, ok := _c.mutation.CreatedAt(); ok {
|
if value, ok := _c.mutation.CreatedAt(); ok {
|
||||||
_spec.SetField(document.FieldCreatedAt, field.TypeString, value)
|
_spec.SetField(document.FieldCreatedAt, field.TypeString, value)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type Extension struct {
|
|||||||
// ID of the ent.
|
// ID of the ent.
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
// UUID for cross-device sync (UUIDv7)
|
// UUID for cross-device sync (UUIDv7)
|
||||||
UUID string `json:"uuid"`
|
UUID *string `json:"uuid"`
|
||||||
// creation time
|
// creation time
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
// update time
|
// update time
|
||||||
@@ -72,7 +72,8 @@ func (_m *Extension) assignValues(columns []string, values []any) error {
|
|||||||
if value, ok := values[i].(*sql.NullString); !ok {
|
if value, ok := values[i].(*sql.NullString); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field uuid", values[i])
|
return fmt.Errorf("unexpected type %T for field uuid", values[i])
|
||||||
} else if value.Valid {
|
} else if value.Valid {
|
||||||
_m.UUID = value.String
|
_m.UUID = new(string)
|
||||||
|
*_m.UUID = value.String
|
||||||
}
|
}
|
||||||
case extension.FieldCreatedAt:
|
case extension.FieldCreatedAt:
|
||||||
if value, ok := values[i].(*sql.NullString); !ok {
|
if value, ok := values[i].(*sql.NullString); !ok {
|
||||||
@@ -149,8 +150,10 @@ func (_m *Extension) String() string {
|
|||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
builder.WriteString("Extension(")
|
builder.WriteString("Extension(")
|
||||||
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
||||||
builder.WriteString("uuid=")
|
if v := _m.UUID; v != nil {
|
||||||
builder.WriteString(_m.UUID)
|
builder.WriteString("uuid=")
|
||||||
|
builder.WriteString(*v)
|
||||||
|
}
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
builder.WriteString("created_at=")
|
builder.WriteString("created_at=")
|
||||||
builder.WriteString(_m.CreatedAt)
|
builder.WriteString(_m.CreatedAt)
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ func (_c *ExtensionCreate) createSpec() (*Extension, *sqlgraph.CreateSpec) {
|
|||||||
)
|
)
|
||||||
if value, ok := _c.mutation.UUID(); ok {
|
if value, ok := _c.mutation.UUID(); ok {
|
||||||
_spec.SetField(extension.FieldUUID, field.TypeString, value)
|
_spec.SetField(extension.FieldUUID, field.TypeString, value)
|
||||||
_node.UUID = value
|
_node.UUID = &value
|
||||||
}
|
}
|
||||||
if value, ok := _c.mutation.CreatedAt(); ok {
|
if value, ok := _c.mutation.CreatedAt(); ok {
|
||||||
_spec.SetField(extension.FieldCreatedAt, field.TypeString, value)
|
_spec.SetField(extension.FieldCreatedAt, field.TypeString, value)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type KeyBinding struct {
|
|||||||
// ID of the ent.
|
// ID of the ent.
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
// UUID for cross-device sync (UUIDv7)
|
// UUID for cross-device sync (UUIDv7)
|
||||||
UUID string `json:"uuid"`
|
UUID *string `json:"uuid"`
|
||||||
// creation time
|
// creation time
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
// update time
|
// update time
|
||||||
@@ -71,7 +71,8 @@ func (_m *KeyBinding) assignValues(columns []string, values []any) error {
|
|||||||
if value, ok := values[i].(*sql.NullString); !ok {
|
if value, ok := values[i].(*sql.NullString); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field uuid", values[i])
|
return fmt.Errorf("unexpected type %T for field uuid", values[i])
|
||||||
} else if value.Valid {
|
} else if value.Valid {
|
||||||
_m.UUID = value.String
|
_m.UUID = new(string)
|
||||||
|
*_m.UUID = value.String
|
||||||
}
|
}
|
||||||
case keybinding.FieldCreatedAt:
|
case keybinding.FieldCreatedAt:
|
||||||
if value, ok := values[i].(*sql.NullString); !ok {
|
if value, ok := values[i].(*sql.NullString); !ok {
|
||||||
@@ -152,8 +153,10 @@ func (_m *KeyBinding) String() string {
|
|||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
builder.WriteString("KeyBinding(")
|
builder.WriteString("KeyBinding(")
|
||||||
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
||||||
builder.WriteString("uuid=")
|
if v := _m.UUID; v != nil {
|
||||||
builder.WriteString(_m.UUID)
|
builder.WriteString("uuid=")
|
||||||
|
builder.WriteString(*v)
|
||||||
|
}
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
builder.WriteString("created_at=")
|
builder.WriteString("created_at=")
|
||||||
builder.WriteString(_m.CreatedAt)
|
builder.WriteString(_m.CreatedAt)
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ func (_c *KeyBindingCreate) createSpec() (*KeyBinding, *sqlgraph.CreateSpec) {
|
|||||||
)
|
)
|
||||||
if value, ok := _c.mutation.UUID(); ok {
|
if value, ok := _c.mutation.UUID(); ok {
|
||||||
_spec.SetField(keybinding.FieldUUID, field.TypeString, value)
|
_spec.SetField(keybinding.FieldUUID, field.TypeString, value)
|
||||||
_node.UUID = value
|
_node.UUID = &value
|
||||||
}
|
}
|
||||||
if value, ok := _c.mutation.CreatedAt(); ok {
|
if value, ok := _c.mutation.CreatedAt(); ok {
|
||||||
_spec.SetField(keybinding.FieldCreatedAt, field.TypeString, value)
|
_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.
|
// 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.
|
// 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.
|
// 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) {
|
if !m.op.Is(OpUpdateOne) {
|
||||||
return v, errors.New("OldUUID is only allowed on UpdateOne operations")
|
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.
|
// 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.
|
// 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.
|
// 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) {
|
if !m.op.Is(OpUpdateOne) {
|
||||||
return v, errors.New("OldUUID is only allowed on UpdateOne operations")
|
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.
|
// 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.
|
// 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.
|
// 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) {
|
if !m.op.Is(OpUpdateOne) {
|
||||||
return v, errors.New("OldUUID is only allowed on UpdateOne operations")
|
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.
|
// 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.
|
// 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.
|
// 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) {
|
if !m.op.Is(OpUpdateOne) {
|
||||||
return v, errors.New("OldUUID is only allowed on UpdateOne operations")
|
return v, errors.New("OldUUID is only allowed on UpdateOne operations")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type Theme struct {
|
|||||||
// ID of the ent.
|
// ID of the ent.
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
// UUID for cross-device sync (UUIDv7)
|
// UUID for cross-device sync (UUIDv7)
|
||||||
UUID string `json:"uuid"`
|
UUID *string `json:"uuid"`
|
||||||
// creation time
|
// creation time
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
// update time
|
// update time
|
||||||
@@ -70,7 +70,8 @@ func (_m *Theme) assignValues(columns []string, values []any) error {
|
|||||||
if value, ok := values[i].(*sql.NullString); !ok {
|
if value, ok := values[i].(*sql.NullString); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field uuid", values[i])
|
return fmt.Errorf("unexpected type %T for field uuid", values[i])
|
||||||
} else if value.Valid {
|
} else if value.Valid {
|
||||||
_m.UUID = value.String
|
_m.UUID = new(string)
|
||||||
|
*_m.UUID = value.String
|
||||||
}
|
}
|
||||||
case theme.FieldCreatedAt:
|
case theme.FieldCreatedAt:
|
||||||
if value, ok := values[i].(*sql.NullString); !ok {
|
if value, ok := values[i].(*sql.NullString); !ok {
|
||||||
@@ -147,8 +148,10 @@ func (_m *Theme) String() string {
|
|||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
builder.WriteString("Theme(")
|
builder.WriteString("Theme(")
|
||||||
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
||||||
builder.WriteString("uuid=")
|
if v := _m.UUID; v != nil {
|
||||||
builder.WriteString(_m.UUID)
|
builder.WriteString("uuid=")
|
||||||
|
builder.WriteString(*v)
|
||||||
|
}
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
builder.WriteString("created_at=")
|
builder.WriteString("created_at=")
|
||||||
builder.WriteString(_m.CreatedAt)
|
builder.WriteString(_m.CreatedAt)
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ func (_c *ThemeCreate) createSpec() (*Theme, *sqlgraph.CreateSpec) {
|
|||||||
)
|
)
|
||||||
if value, ok := _c.mutation.UUID(); ok {
|
if value, ok := _c.mutation.UUID(); ok {
|
||||||
_spec.SetField(theme.FieldUUID, field.TypeString, value)
|
_spec.SetField(theme.FieldUUID, field.TypeString, value)
|
||||||
_node.UUID = value
|
_node.UUID = &value
|
||||||
}
|
}
|
||||||
if value, ok := _c.mutation.CreatedAt(); ok {
|
if value, ok := _c.mutation.CreatedAt(); ok {
|
||||||
_spec.SetField(theme.FieldCreatedAt, field.TypeString, value)
|
_spec.SetField(theme.FieldCreatedAt, field.TypeString, value)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
"voidraft/internal/common/helper"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
gitConfig "github.com/go-git/go-git/v5/config"
|
gitConfig "github.com/go-git/go-git/v5/config"
|
||||||
@@ -61,7 +62,7 @@ type BackupService struct {
|
|||||||
autoBackupStop chan bool
|
autoBackupStop chan bool
|
||||||
autoBackupWg sync.WaitGroup
|
autoBackupWg sync.WaitGroup
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
cancelObserver CancelFunc
|
cancelObservers []helper.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBackupService 创建新的备份服务实例
|
// NewBackupService 创建新的备份服务实例
|
||||||
@@ -74,7 +75,11 @@ func NewBackupService(configService *ConfigService, dbService *DatabaseService,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *BackupService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
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 {
|
if err := s.Initialize(); err != nil {
|
||||||
s.logger.Error("initializing backup service: %v", err)
|
s.logger.Error("initializing backup service: %v", err)
|
||||||
}
|
}
|
||||||
@@ -89,6 +94,12 @@ func (s *BackupService) onBackupConfigChange(oldValue, newValue interface{}) {
|
|||||||
_ = s.HandleConfigChange(&config.Backup)
|
_ = 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 初始化备份服务
|
// Initialize 初始化备份服务
|
||||||
func (s *BackupService) Initialize() error {
|
func (s *BackupService) Initialize() error {
|
||||||
config, repoPath, err := s.getConfigAndPath()
|
config, repoPath, err := s.getConfigAndPath()
|
||||||
@@ -100,7 +111,7 @@ func (s *BackupService) Initialize() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 仓库地址为空时不初始化(等待用户配置)
|
// 仓库地址为空时不初始化
|
||||||
if strings.TrimSpace(config.RepoURL) == "" {
|
if strings.TrimSpace(config.RepoURL) == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1196,8 +1207,10 @@ func (s *BackupService) HandleConfigChange(config *models.GitBackupConfig) error
|
|||||||
|
|
||||||
// ServiceShutdown 服务关闭
|
// ServiceShutdown 服务关闭
|
||||||
func (s *BackupService) ServiceShutdown() {
|
func (s *BackupService) ServiceShutdown() {
|
||||||
if s.cancelObserver != nil {
|
for _, cancel := range s.cancelObservers {
|
||||||
s.cancelObserver()
|
if cancel != nil {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
s.StopAutoBackup()
|
s.StopAutoBackup()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,33 +2,31 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
"voidraft/internal/common/helper"
|
||||||
"voidraft/internal/models"
|
"voidraft/internal/models"
|
||||||
|
|
||||||
jsonparser "github.com/knadh/koanf/parsers/json"
|
jsonparser "github.com/knadh/koanf/parsers/json"
|
||||||
"github.com/knadh/koanf/providers/file"
|
"github.com/knadh/koanf/providers/file"
|
||||||
"github.com/knadh/koanf/providers/structs"
|
"github.com/knadh/koanf/providers/structs"
|
||||||
"github.com/knadh/koanf/v2"
|
"github.com/knadh/koanf/v2"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigService 应用配置服务
|
// ConfigService 应用配置服务
|
||||||
type ConfigService struct {
|
type ConfigService struct {
|
||||||
koanf *koanf.Koanf // koanf 实例
|
koanf *koanf.Koanf
|
||||||
logger *log.LogService // 日志服务
|
logger *log.LogService
|
||||||
configDir string // 配置目录
|
configDir string
|
||||||
settingsPath string // 设置文件路径
|
settingsPath string
|
||||||
mu sync.RWMutex // 读写锁
|
mu sync.RWMutex
|
||||||
fileProvider *file.File // 文件提供器,用于监听
|
observer *helper.ConfigObserver
|
||||||
|
|
||||||
observer *ConfigObserver
|
|
||||||
|
|
||||||
// 配置迁移器
|
// 配置迁移器
|
||||||
configMigrator *ConfigMigrator
|
configMigrator *ConfigMigrator
|
||||||
@@ -36,49 +34,29 @@ type ConfigService struct {
|
|||||||
|
|
||||||
// NewConfigService 创建新的配置服务实例
|
// NewConfigService 创建新的配置服务实例
|
||||||
func NewConfigService(logger *log.LogService) *ConfigService {
|
func NewConfigService(logger *log.LogService) *ConfigService {
|
||||||
// 获取用户主目录
|
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("unable to get the user's home directory: %w", err))
|
panic(fmt.Errorf("unable to get the user's home directory: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置配置目录和设置文件路径
|
|
||||||
configDir := filepath.Join(homeDir, ".voidraft", "config")
|
configDir := filepath.Join(homeDir, ".voidraft", "config")
|
||||||
settingsPath := filepath.Join(configDir, "settings.json")
|
settingsPath := filepath.Join(configDir, "settings.json")
|
||||||
|
|
||||||
observerService := NewConfigObserver(logger)
|
|
||||||
|
|
||||||
configMigrator := NewConfigMigrator(logger, configDir, "settings", settingsPath)
|
|
||||||
|
|
||||||
return &ConfigService{
|
return &ConfigService{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
configDir: configDir,
|
configDir: configDir,
|
||||||
settingsPath: settingsPath,
|
settingsPath: settingsPath,
|
||||||
koanf: koanf.New("."),
|
koanf: koanf.New("."),
|
||||||
observer: observerService,
|
observer: helper.NewConfigObserver(logger),
|
||||||
configMigrator: configMigrator,
|
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 {
|
func (cs *ConfigService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||||
err := cs.initConfig()
|
if err := cs.initConfig(); err != nil {
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,15 +65,38 @@ func (cs *ConfigService) initConfig() error {
|
|||||||
cs.mu.Lock()
|
cs.mu.Lock()
|
||||||
defer cs.mu.Unlock()
|
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) {
|
if _, err := os.Stat(cs.settingsPath); os.IsNotExist(err) {
|
||||||
return cs.createDefaultConfig()
|
return cs.createDefaultConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 配置文件存在,直接加载现有配置
|
// 加载现有配置
|
||||||
cs.fileProvider = file.Provider(cs.settingsPath)
|
if err := cs.koanf.Load(file.Provider(cs.settingsPath), jsonparser.Parser()); err != nil {
|
||||||
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
|
return fmt.Errorf("failed to load config: %w", err)
|
||||||
return 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
|
return nil
|
||||||
@@ -107,65 +108,12 @@ func (cs *ConfigService) MigrateConfig() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cs.mu.Lock()
|
||||||
|
defer cs.mu.Unlock()
|
||||||
|
|
||||||
defaultConfig := models.NewDefaultAppConfig()
|
defaultConfig := models.NewDefaultAppConfig()
|
||||||
_, err := cs.configMigrator.AutoMigrate(defaultConfig, cs.koanf)
|
_, err := cs.configMigrator.AutoMigrate(defaultConfig, cs.koanf)
|
||||||
if err != nil {
|
return err
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfig 获取完整应用配置
|
// GetConfig 获取完整应用配置
|
||||||
@@ -177,47 +125,9 @@ func (cs *ConfigService) GetConfig() (*models.AppConfig, error) {
|
|||||||
if err := cs.koanf.UnmarshalWithConf("", &config, koanf.UnmarshalConf{Tag: "json"}); err != nil {
|
if err := cs.koanf.UnmarshalWithConf("", &config, koanf.UnmarshalConf{Tag: "json"}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &config, nil
|
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 获取配置项
|
// Get 获取配置项
|
||||||
func (cs *ConfigService) Get(key string) interface{} {
|
func (cs *ConfigService) Get(key string) interface{} {
|
||||||
cs.mu.RLock()
|
cs.mu.RLock()
|
||||||
@@ -225,118 +135,113 @@ func (cs *ConfigService) Get(key string) interface{} {
|
|||||||
return cs.koanf.Get(key)
|
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 {
|
func (cs *ConfigService) ResetConfig() error {
|
||||||
cs.mu.Lock()
|
cs.mu.Lock()
|
||||||
|
|
||||||
// 保存旧配置快照
|
// 保存旧配置快照
|
||||||
oldSnapshot := cs.createConfigSnapshot()
|
oldSnapshot := cs.createSnapshot()
|
||||||
|
|
||||||
// 停止文件监听
|
// 重置为默认配置
|
||||||
if cs.fileProvider != nil {
|
cs.koanf = koanf.New(".")
|
||||||
cs.fileProvider.Unwatch()
|
defaultConfig := models.NewDefaultAppConfig()
|
||||||
cs.fileProvider = nil
|
if err := cs.koanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
// 设置默认配置
|
|
||||||
if err := cs.setDefaults(); err != nil {
|
|
||||||
cs.mu.Unlock()
|
cs.mu.Unlock()
|
||||||
return err
|
return fmt.Errorf("failed to load default config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 写入配置文件
|
// 写入配置文件
|
||||||
if err := cs.writeConfigToFile(); err != nil {
|
if err := cs.writeConfigToFile(); err != nil {
|
||||||
cs.mu.Unlock()
|
cs.mu.Unlock()
|
||||||
return err
|
return fmt.Errorf("failed to write config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新创建koanf实例
|
newSnapshot := cs.createSnapshot()
|
||||||
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()
|
|
||||||
cs.mu.Unlock()
|
cs.mu.Unlock()
|
||||||
|
|
||||||
// 重新启动文件监听
|
// 通知配置变更
|
||||||
cs.startWatching()
|
|
||||||
|
|
||||||
// 检测配置变更并通知观察者
|
|
||||||
cs.notifyChanges(oldSnapshot, newSnapshot)
|
cs.notifyChanges(oldSnapshot, newSnapshot)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeConfigToFile 将配置写回JSON文件
|
// writeConfigToFile 将配置写入文件
|
||||||
func (cs *ConfigService) writeConfigToFile() error {
|
func (cs *ConfigService) writeConfigToFile() error {
|
||||||
configBytes, err := cs.koanf.Marshal(jsonparser.Parser())
|
configBytes, err := cs.koanf.Marshal(jsonparser.Parser())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return os.WriteFile(cs.settingsPath, configBytes, 0644)
|
||||||
if err := os.WriteFile(cs.settingsPath, configBytes, 0644); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch 注册配置变更监听器
|
// 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)
|
return cs.observer.Watch(path, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WatchWithContext 使用 Context 注册监听器
|
// 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)
|
cs.observer.WatchWithContext(ctx, path, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
// createConfigSnapshot 创建当前配置的快照(调用者需确保已持有锁)
|
// createSnapshotLocked 创建配置快照
|
||||||
func (cs *ConfigService) createConfigSnapshot() map[string]interface{} {
|
func (cs *ConfigService) createSnapshot() map[string]interface{} {
|
||||||
snapshot := make(map[string]interface{})
|
snapshot := make(map[string]interface{})
|
||||||
allKeys := cs.koanf.All()
|
for _, key := range cs.koanf.Keys() {
|
||||||
flattenMap("", allKeys, snapshot)
|
snapshot[key] = cs.koanf.Get(key)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
// notifyChanges 检测配置变更并通知观察者
|
// notifyChanges 检测配置变更并通知观察者
|
||||||
func (cs *ConfigService) notifyChanges(oldSnapshot, newSnapshot map[string]interface{}) {
|
func (cs *ConfigService) notifyChanges(oldSnapshot, newSnapshot map[string]interface{}) {
|
||||||
// 检测变更
|
if cs.observer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
changes := make(map[string]struct {
|
changes := make(map[string]struct {
|
||||||
OldValue interface{}
|
OldValue interface{}
|
||||||
NewValue interface{}
|
NewValue interface{}
|
||||||
@@ -345,14 +250,11 @@ func (cs *ConfigService) notifyChanges(oldSnapshot, newSnapshot map[string]inter
|
|||||||
// 检查新增和修改的键
|
// 检查新增和修改的键
|
||||||
for key, newValue := range newSnapshot {
|
for key, newValue := range newSnapshot {
|
||||||
oldValue, exists := oldSnapshot[key]
|
oldValue, exists := oldSnapshot[key]
|
||||||
if !exists || !isEqual(oldValue, newValue) {
|
if !exists || !reflect.DeepEqual(oldValue, newValue) {
|
||||||
changes[key] = struct {
|
changes[key] = struct {
|
||||||
OldValue interface{}
|
OldValue interface{}
|
||||||
NewValue interface{}
|
NewValue interface{}
|
||||||
}{
|
}{oldValue, newValue}
|
||||||
OldValue: oldValue,
|
|
||||||
NewValue: newValue,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,29 +264,18 @@ func (cs *ConfigService) notifyChanges(oldSnapshot, newSnapshot map[string]inter
|
|||||||
changes[key] = struct {
|
changes[key] = struct {
|
||||||
OldValue interface{}
|
OldValue interface{}
|
||||||
NewValue interface{}
|
NewValue interface{}
|
||||||
}{
|
}{oldValue, nil}
|
||||||
OldValue: oldValue,
|
|
||||||
NewValue: nil,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通知所有变更
|
// 批量通知
|
||||||
if cs.observer != nil && len(changes) > 0 {
|
if len(changes) > 0 {
|
||||||
cs.observer.NotifyAll(changes)
|
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 关闭服务
|
// ServiceShutdown 关闭服务
|
||||||
func (cs *ConfigService) ServiceShutdown() error {
|
func (cs *ConfigService) ServiceShutdown() error {
|
||||||
cs.stopWatching()
|
|
||||||
if cs.observer != nil {
|
if cs.observer != nil {
|
||||||
cs.observer.Shutdown()
|
cs.observer.Shutdown()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ type MigrationProgress struct {
|
|||||||
|
|
||||||
// MigrationService 迁移服务
|
// MigrationService 迁移服务
|
||||||
type MigrationService struct {
|
type MigrationService struct {
|
||||||
logger *log.LogService
|
logger *log.LogService
|
||||||
dbService *DatabaseService
|
dbService *DatabaseService
|
||||||
progress atomic.Value // stores MigrationProgress
|
configService *ConfigService
|
||||||
|
progress atomic.Value // stores MigrationProgress
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -34,13 +35,14 @@ type MigrationService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewMigrationService 创建迁移服务
|
// NewMigrationService 创建迁移服务
|
||||||
func NewMigrationService(dbService *DatabaseService, logger *log.LogService) *MigrationService {
|
func NewMigrationService(dbService *DatabaseService, configService *ConfigService, logger *log.LogService) *MigrationService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
ms := &MigrationService{
|
ms := &MigrationService{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
dbService: dbService,
|
dbService: dbService,
|
||||||
|
configService: configService,
|
||||||
}
|
}
|
||||||
ms.progress.Store(MigrationProgress{})
|
ms.progress.Store(MigrationProgress{})
|
||||||
return ms
|
return ms
|
||||||
@@ -94,9 +96,12 @@ func (ms *MigrationService) MigrateDirectory(srcPath, dstPath string) error {
|
|||||||
if err := ms.dbService.ServiceShutdown(); err != nil {
|
if err := ms.dbService.ServiceShutdown(); err != nil {
|
||||||
ms.logger.Error("Failed to close database connection", "error", err)
|
ms.logger.Error("Failed to close database connection", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 等待文件句柄释放(Windows 特有问题)
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保失败时恢复数据库连接
|
// 确保恢复数据库连接
|
||||||
defer func() {
|
defer func() {
|
||||||
if ms.dbService != nil {
|
if ms.dbService != nil {
|
||||||
if err := ms.dbService.ServiceStartup(ctx, application.ServiceOptions{}); err != 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)
|
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)
|
ms.setProgress(100)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// preCheck 预检查,返回是否需要迁移
|
// preCheck 预检查,返回是否需要迁移
|
||||||
func (ms *MigrationService) preCheck(srcPath, dstPath string) (bool, error) {
|
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
|
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)
|
srcAbs, _ := filepath.Abs(srcPath)
|
||||||
dstAbs, _ := filepath.Abs(dstPath)
|
dstAbs, _ := filepath.Abs(dstPath)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func NewDialogService(logger *log.LogService) *DialogService {
|
|||||||
|
|
||||||
// SelectDirectory 打开目录选择对话框
|
// SelectDirectory 打开目录选择对话框
|
||||||
func (ds *DialogService) SelectDirectory() (string, error) {
|
func (ds *DialogService) SelectDirectory() (string, error) {
|
||||||
dialog := application.OpenFileDialog()
|
dialog := application.OpenFileDialogStruct{}
|
||||||
dialog.SetOptions(&application.OpenFileDialogOptions{
|
dialog.SetOptions(&application.OpenFileDialogOptions{
|
||||||
// 目录选择配置
|
// 目录选择配置
|
||||||
CanChooseDirectories: true, // 允许选择目录
|
CanChooseDirectories: true, // 允许选择目录
|
||||||
@@ -67,7 +67,7 @@ func (ds *DialogService) SelectDirectory() (string, error) {
|
|||||||
|
|
||||||
// SelectFile 打开文件选择对话框
|
// SelectFile 打开文件选择对话框
|
||||||
func (ds *DialogService) SelectFile() (string, error) {
|
func (ds *DialogService) SelectFile() (string, error) {
|
||||||
dialog := application.OpenFileDialog()
|
dialog := application.OpenFileDialogStruct{}
|
||||||
dialog.SetOptions(&application.OpenFileDialogOptions{
|
dialog.SetOptions(&application.OpenFileDialogOptions{
|
||||||
// 目录选择配置
|
// 目录选择配置
|
||||||
CanChooseDirectories: false, // 允许选择目录
|
CanChooseDirectories: false, // 允许选择目录
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
|
||||||
"voidraft/internal/common/helper"
|
"voidraft/internal/common/helper"
|
||||||
"voidraft/internal/common/hotkey"
|
"voidraft/internal/common/hotkey"
|
||||||
"voidraft/internal/models"
|
"voidraft/internal/models"
|
||||||
@@ -33,7 +32,7 @@ type HotkeyService struct {
|
|||||||
isShutdown atomic.Bool
|
isShutdown atomic.Bool
|
||||||
|
|
||||||
// 配置观察者取消函数
|
// 配置观察者取消函数
|
||||||
cancelObservers []CancelFunc
|
cancelObservers []helper.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHotkeyService 创建热键服务实例
|
// NewHotkeyService 创建热键服务实例
|
||||||
@@ -61,7 +60,7 @@ func (hs *HotkeyService) ServiceStartup(ctx context.Context, options application
|
|||||||
// Initialize 初始化热键服务
|
// Initialize 初始化热键服务
|
||||||
func (hs *HotkeyService) Initialize() error {
|
func (hs *HotkeyService) Initialize() error {
|
||||||
// 注册配置监听
|
// 注册配置监听
|
||||||
hs.cancelObservers = []CancelFunc{
|
hs.cancelObservers = []helper.CancelFunc{
|
||||||
hs.configService.Watch("general.enableGlobalHotkey", hs.onHotkeyConfigChange),
|
hs.configService.Watch("general.enableGlobalHotkey", hs.onHotkeyConfigChange),
|
||||||
hs.configService.Watch("general.globalHotkey", 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()
|
config, err := hs.configService.GetConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
hs.logger.Error("failed to get config", "error", err)
|
||||||
return
|
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 注册全局热键
|
// RegisterHotkey 注册全局热键
|
||||||
@@ -101,22 +103,34 @@ func (hs *HotkeyService) RegisterHotkey(combo *models.HotkeyCombo) error {
|
|||||||
return errors.New("invalid hotkey combination")
|
return errors.New("invalid hotkey combination")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果已注册,先取消
|
|
||||||
if hs.registered.Load() {
|
|
||||||
_ = hs.UnregisterHotkey()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换为 hotkey 库的格式
|
// 转换为 hotkey 库的格式
|
||||||
key, mods, err := hs.convertHotkey(combo)
|
key, mods, err := hs.convertHotkey(combo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("convert hotkey: %w", err)
|
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)
|
hs.hk = hotkey.New(mods, key)
|
||||||
if err := hs.hk.Register(); err != nil {
|
if err := hs.hk.Register(); err != nil {
|
||||||
hs.mu.Unlock()
|
hs.mu.Unlock()
|
||||||
|
hs.logger.Error("failed to register hotkey", "error", err)
|
||||||
return fmt.Errorf("register hotkey: %w", err)
|
return fmt.Errorf("register hotkey: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,35 +155,23 @@ func (hs *HotkeyService) UnregisterHotkey() error {
|
|||||||
hs.registered.Store(false)
|
hs.registered.Store(false)
|
||||||
|
|
||||||
// 获取热键实例的引用
|
// 获取热键实例的引用
|
||||||
hs.mu.RLock()
|
hs.mu.Lock()
|
||||||
hk := hs.hk
|
hk := hs.hk
|
||||||
hs.mu.RUnlock()
|
hs.hk = nil
|
||||||
|
hs.currentHotkey = nil
|
||||||
|
hs.mu.Unlock()
|
||||||
|
|
||||||
if hk == nil {
|
if hk == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用 Close() 确保完全清理
|
// 异步清理
|
||||||
_ = hk.Close()
|
|
||||||
|
|
||||||
// 等待监听 goroutine 退出
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
go func() {
|
||||||
hs.wg.Wait()
|
if err := hk.Close(); err != nil {
|
||||||
close(done)
|
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
|
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 验证热键组合
|
// isValidHotkey 验证热键组合
|
||||||
func (hs *HotkeyService) isValidHotkey(combo *models.HotkeyCombo) bool {
|
func (hs *HotkeyService) isValidHotkey(combo *models.HotkeyCombo) bool {
|
||||||
if combo == nil || combo.Key == "" {
|
if combo == nil || combo.Key == "" {
|
||||||
@@ -333,24 +353,6 @@ func (hs *HotkeyService) isValidHotkey(combo *models.HotkeyCombo) bool {
|
|||||||
return true
|
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 检查是否已注册
|
// IsRegistered 检查是否已注册
|
||||||
func (hs *HotkeyService) IsRegistered() bool {
|
func (hs *HotkeyService) IsRegistered() bool {
|
||||||
return hs.registered.Load()
|
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/dock"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
||||||
|
"log/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServiceManager 服务管理器,负责协调各个服务
|
// ServiceManager 服务管理器,负责协调各个服务
|
||||||
@@ -36,7 +37,9 @@ type ServiceManager struct {
|
|||||||
// NewServiceManager 创建新的服务管理器实例
|
// NewServiceManager 创建新的服务管理器实例
|
||||||
func NewServiceManager() *ServiceManager {
|
func NewServiceManager() *ServiceManager {
|
||||||
// 初始化日志服务
|
// 初始化日志服务
|
||||||
logger := log.New()
|
logger := log.NewWithConfig(&log.Config{
|
||||||
|
LogLevel: slog.LevelDebug,
|
||||||
|
})
|
||||||
|
|
||||||
// 初始化badge服务
|
// 初始化badge服务
|
||||||
badgeService := dock.New()
|
badgeService := dock.New()
|
||||||
@@ -51,7 +54,7 @@ func NewServiceManager() *ServiceManager {
|
|||||||
databaseService := NewDatabaseService(configService, logger)
|
databaseService := NewDatabaseService(configService, logger)
|
||||||
|
|
||||||
// 初始化迁移服务
|
// 初始化迁移服务
|
||||||
migrationService := NewMigrationService(databaseService, logger)
|
migrationService := NewMigrationService(databaseService, configService, logger)
|
||||||
|
|
||||||
// 初始化文档服务
|
// 初始化文档服务
|
||||||
documentService := NewDocumentService(databaseService, logger)
|
documentService := NewDocumentService(databaseService, logger)
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ func (ws *WindowService) onWindowClosing(documentID int64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetOpenWindows 获取所有打开的文档窗口
|
// GetOpenWindows 获取所有打开的文档窗口
|
||||||
func (ws *WindowService) GetOpenWindows() []application.Window {
|
func (ws *WindowService) getOpenWindows() []application.Window {
|
||||||
app := application.Get()
|
app := application.Get()
|
||||||
return app.Window.GetAll()
|
return app.Window.GetAll()
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ func (ws *WindowService) IsDocumentWindowOpen(documentID int64) bool {
|
|||||||
func (ws *WindowService) ServiceShutdown() error {
|
func (ws *WindowService) ServiceShutdown() error {
|
||||||
// 从吸附服务中取消注册所有窗口
|
// 从吸附服务中取消注册所有窗口
|
||||||
if ws.windowSnapService != nil {
|
if ws.windowSnapService != nil {
|
||||||
windows := ws.GetOpenWindows()
|
windows := ws.getOpenWindows()
|
||||||
for _, window := range windows {
|
for _, window := range windows {
|
||||||
if documentID, err := strconv.ParseInt(window.Name(), 10, 64); err == nil {
|
if documentID, err := strconv.ParseInt(window.Name(), 10, 64); err == nil {
|
||||||
ws.windowSnapService.UnregisterWindow(documentID)
|
ws.windowSnapService.UnregisterWindow(documentID)
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ type WindowSnapService struct {
|
|||||||
windowMoveUnhooks map[int64]func() // documentID -> 子窗口移动监听清理函数
|
windowMoveUnhooks map[int64]func() // documentID -> 子窗口移动监听清理函数
|
||||||
|
|
||||||
// 配置观察者取消函数
|
// 配置观察者取消函数
|
||||||
cancelObserver CancelFunc
|
cancelObserver helper.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWindowSnapService 创建新的窗口吸附服务实例
|
// NewWindowSnapService 创建新的窗口吸附服务实例
|
||||||
|
|||||||
Reference in New Issue
Block a user