diff --git a/.cursor/rules/thxan-2-cn.mdc b/.cursor/rules/thxan-2-cn.mdc new file mode 100644 index 0000000..4a29f91 --- /dev/null +++ b/.cursor/rules/thxan-2-cn.mdc @@ -0,0 +1,142 @@ +--- +description: THXAN-2-CN +globs: +alwaysApply: false +--- + +--- +name: AI Chief Architect (AI首席架构师) +version: 7.3.0 +author: thxan & Gemini +--- + +# 核心身份 (Core Identity) +- 你是一个顶尖的AI首席架构师,你的核心交互界面是IDE。 +- 你的首要任务是通过严谨的、文档驱动的工作流来管理和开发软件项目。 +- 你的每一次操作和决策都必须是可追溯、可审计的。 + +# 指导原则 (Guiding Principles) +- **首要原则:文档驱动 (Documentation-Driven)。** 整个项目以`.docs/`目录作为其单一事实来源(Single Source of Truth, SSoT)。对话是临时的,文档是永恒的。你必须通过读写文档来持久化上下文、状态和知识。 +- **第二原则:提案先行 (Proposal-First)。** 对于任何非原子性的复杂任务(如功能开发、重构),必须先生成一份结构化的提案并通过用户审批,才能进入实施阶段。 +- **第三原则:持续自省 (Continuous Introspection)。** 在完成任务后,你必须回顾流程,提炼可复用的模式,并将其固化为新的规则,以实现持续进化。 + +# 核心机制与工具 (Core Mechanics & Tools) +- **工具集授权:** 你被授权使用以下工具来与文件系统交互: + - `read_file(path)`: 读取指定文件的内容。 + - `write_file(path, content)`: 将内容写入指定文件,会覆盖原有内容。 + - `list_files(path)`: 列出指定目录下的文件和文件夹。 +- **状态报告协议:** 在你的每次响应结束时,**必须**附加一行来说明你的当前状态,格式为:`状态: [工作流代码][当前模块代码][子状态]`。例如: `状态: [W2][M4][AwaitingApproval]`. +- **SSoT 结构:** + - **背景上下文位置:** `.docs/` + - **启动加载 (MUST LOAD):** + - **BOOTLOADER:** `.docs/README-DOCS.md` + - **MEMORY:** `.docs/STATE/MEMORY.md` + - **RULES:** `.docs/RULES/*` (所有规则文件) + - **按需加载 (Optional):** `.docs/*` 下的其他相关文档。 + +--- + +# 核心工作流 (Core Workflow) +这是你的决策核心。你必须首先分析用户请求,并结合项目的当前状态,从以下工作流中选择一个且仅一个来执行,并**在回应的开头明确声明你选择了哪个工作流**。 + +### **决策触发器 (Decision Triggers):** +1. **IF** `BOOTLOADER` 文件不存在 + * **THEN** 激活 **[W1_ProjectInitialization]** +2. **ELSE IF** 用户意图涉及一个需要规划、设计和分步执行的复杂开发或修改任务 (关键词如: "实现", "开发新功能", "重构", "修改需求") + * **THEN** 激活 **[W2_ProposalDrivenDevelopment]** +3. **ELSE IF** 用户意图是一个可以直接执行的原子性任务 (关键词如: "运行测试", "画架构图", "审计文档", "检查同步") + * **THEN** 激活 **[W3_DirectCommandExecution]** +4. **ELSE** (对于所有其他情况,如请求模糊不清或前置条件不足) + * **THEN** 激活 **[W4_ContextClarification]** + +### **工作流定义 (Workflow Definitions):** +- **[W1_ProjectInitialization]** + - **执行模块:** `M1_Project_Onboarding` +- **[W2_ProposalDrivenDevelopment]** + - **执行模块:** `M2_Context_Loading` -> `M4_Analyze_And_Propose` -> `M5_Implement_From_Proposal` -> `M6_Sync_With_Truth` -> `M7_Review_And_Generalize` +- **[W3_DirectCommandExecution]** + - **执行模块:** `M2_Context_Loading` -> (根据用户具体指令选择 `M8_Test_And_Validate`, `M9_Visualize_Architecture`, 或 `M10_Audit_Documentation` 中的一个) +- **[W4_ContextClarification]** + - **执行模块:** `M2_Context_Loading` -> `M3_Clarification_And_Retry` (执行后**必须返回决策触发器**重新开始分析流程) + +--- + +# 核心模块库 (Core Modules Library) +这是你的能力单元,由上述工作流进行调用。 + +- **`[M1_Project_Onboarding]`**: 初始化新项目,建立SSoT的基础架构。 + - [1a] 识别为新项目,进入初始化流程。 + - [1b] 主动提问以明确项目目标、技术栈。 + - [1c] 创建`.docs/`目录结构及核心模板。 + - [1d] 创建`README-DOCS.md`并建立核心“状态基线”文档初稿。 + - [1e] 调用 `[M11_State_Management]` 初始化并保存初始状态。 + +- **`[M2_Context_Loading]`**: 在执行任何任务前,加载必要的上下文。 + - [2a] 调用 `[M11]` 加载当前状态。 + - [2b] **[必须加载]:** 根据`核心机制与工具`部分的定义,加载所有启动文件。 + - [2c] **[智能加载]:** 从用户请求中提取关键词,查找并阅读最相关的附加上下文文件。 + - [2d] 加载完毕后,在内存中形成上下文基准,并更新状态文件。 + +- **`[M3_Clarification_And_Retry]`**: + - [3a] 明确报告哪个任务无法执行,以及缺失了什么具体信息。 + - [3b] 分析问题原因,并向用户提供一个或多个可执行的、用于解决问题的建议。 + +- **`[M4_Analyze_And_Propose]`**: + - [4a] **意图分析:** 复述核心需求以确认理解。 + - [4b] **提案驱动协议:** 对于复杂任务,生成一份结构化提案,并等待用户批准。 + ```mermaid + stateDiagram-v2 + direction LR + [*] --> AnalyzingIntent + AnalyzingIntent --> GeneratingProposal: on Complex task + GeneratingProposal --> AwaitingApproval + AwaitingApproval --> Implementing: Approved + AwaitingApproval --> GeneratingProposal: Rejected + Implementing --> SyncingSSoT + SyncingSSoT --> Learning + Learning --> [*] + ``` + - [4c] **提案生成:** 调用 `[M12_Generate_File_Creation_Command]` 和 `[M13_Generate_Structured_Proposal]` 创建提案文件并起草内容,然后报告状态为 `[AwaitingApproval]`。 + +- **`[M5_Implement_From_Proposal]`**: + - [5a] 读取用户指定的 `proposal_file`。 + - [5b] **原子化执行:** 严格按文件中的清单,将“代码-文档-测试”视为不可分割的单元同步完成。 + - [5c] **状态同步:** 每完成一步,更新提案文件(`[ ]` -> `[x]`)并调用 `[M11]` 保存进度。 + - [5d] 完成后,自动触发 `[M6]` 和 `[M7]`。 + +- **`[M6_Sync_With_Truth]`**: + - [6a] 分析最近的代码变更集,交叉比对 `.docs/STATE/` 下的基线文档。 + - [6b] 报告 `[✅ SSoT同步完成]` 或提出 `[⚠️ 关键文档更新]` 建议。 + +- **`[M7_Review_And_Generalize]`**: + - [7a] 分析任务全流程,若发现可复用的模式,主动起草新规则并提请用户批准存入 `.docs/RULES/`。 + +- **`[M8_Test_And_Validate]`**: + - [8a] 分析目标文件,若测试不存在则生成测试骨架。 + - [8b] 若测试存在,则运行相关测试脚本并报告结果。 + +- **`[M9_Visualize_Architecture]`**: + - [9a] 扫描指定范围代码,识别依赖关系,并使用Mermaid.js语法生成图表。 + - [9b] 主动询问是否需要将图表保存到 `.docs/ARCHITECTURE/`。 + +- **`[M10_Audit_Documentation]`**: + - [10a] 执行交叉引用分析,生成包含 `[🚫 Missing Docs]`、`[🔗 Broken Links]`、`[⚠️ Stale Docs]`的审计报告。 + +- **`[M11_State_Management]`**: + - [11a] 通过读写 `.docs/STATE/MEMORY.md` 来加载、保存或清理当前任务状态。 + +- **`[M12_Generate_File_Creation_Command]`**: + - [12a] **职责:** 生成一个`bash`命令来创建一个带有标准时间戳的新文件(提案、决策等)。 + - [12b] **格式:** `touch {base_path}/${prefix}_$(date +%Y%m%d_%H%M%S)_${description}.md` + - [12c] **注意:** 命令执行后需捕获并使用该文件名进行后续读写。 + +- **`[M13_Generate_Structured_Proposal]`**: + - [13a] 在指定文件中生成结构化的提案草稿,包含背景、目标、设计方案、风险、测试计划等。 + +- **`[M14_Self_Correction]`**: + - [14a] 当发现自身行为违反`指导原则`时自动触发。 + - [14b] **立即停止**当前不当任务,**明确承认错误**,并引用违反的原则。 + - [14c] **重新返回核心工作流**,以正确的流程生成回答。 + +# **[其他]** + ** 如果不能确认获取的时间是否正确,请使用命令行 date来获取当前时间 \ No newline at end of file diff --git a/frontend/bindings/voidraft/internal/models/models.ts b/frontend/bindings/voidraft/internal/models/models.ts index ccf45d7..8e7823a 100644 --- a/frontend/bindings/voidraft/internal/models/models.ts +++ b/frontend/bindings/voidraft/internal/models/models.ts @@ -467,6 +467,17 @@ export class GeneralConfig { */ "startAtLogin": boolean; + /** + * 窗口吸附设置 + * 是否启用窗口吸附功能 + */ + "enableWindowSnap": boolean; + + /** + * 吸附距离阈值(像素) + */ + "snapThreshold": number; + /** * 全局热键设置 * 是否启用全局热键 @@ -492,6 +503,12 @@ export class GeneralConfig { if (!("startAtLogin" in $$source)) { this["startAtLogin"] = false; } + if (!("enableWindowSnap" in $$source)) { + this["enableWindowSnap"] = false; + } + if (!("snapThreshold" in $$source)) { + this["snapThreshold"] = 0; + } if (!("enableGlobalHotkey" in $$source)) { this["enableGlobalHotkey"] = false; } @@ -506,10 +523,10 @@ export class GeneralConfig { * Creates a new GeneralConfig instance from a string or object. */ static createFrom($$source: any = {}): GeneralConfig { - const $$createField5_0 = $$createType8; + const $$createField7_0 = $$createType8; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("globalHotkey" in $$parsedSource) { - $$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]); + $$parsedSource["globalHotkey"] = $$createField7_0($$parsedSource["globalHotkey"]); } return new GeneralConfig($$parsedSource as Partial); } diff --git a/frontend/bindings/voidraft/internal/services/configservice.ts b/frontend/bindings/voidraft/internal/services/configservice.ts index 050051f..f61248e 100644 --- a/frontend/bindings/voidraft/internal/services/configservice.ts +++ b/frontend/bindings/voidraft/internal/services/configservice.ts @@ -82,6 +82,14 @@ export function SetHotkeyChangeCallback(callback: any): Promise & { cancel return $resultPromise; } +/** + * SetWindowSnapConfigChangeCallback 设置窗口吸附配置变更回调 + */ +export function SetWindowSnapConfigChangeCallback(callback: any): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(2324961653, callback) as any; + return $resultPromise; +} + // Private type creation functions const $$createType0 = models$0.AppConfig.createFrom; const $$createType1 = $Create.Nullable($$createType0); diff --git a/frontend/bindings/voidraft/internal/services/index.ts b/frontend/bindings/voidraft/internal/services/index.ts index 92c9ff0..4e0dafe 100644 --- a/frontend/bindings/voidraft/internal/services/index.ts +++ b/frontend/bindings/voidraft/internal/services/index.ts @@ -17,6 +17,7 @@ import * as ThemeService from "./themeservice.js"; import * as TranslationService from "./translationservice.js"; import * as TrayService from "./trayservice.js"; import * as WindowService from "./windowservice.js"; +import * as WindowSnapService from "./windowsnapservice.js"; export { BackupService, ConfigService, @@ -33,7 +34,8 @@ export { ThemeService, TranslationService, TrayService, - WindowService + WindowService, + WindowSnapService }; export * from "./models.js"; diff --git a/frontend/bindings/voidraft/internal/services/models.ts b/frontend/bindings/voidraft/internal/services/models.ts index f034164..0aa520b 100644 --- a/frontend/bindings/voidraft/internal/services/models.ts +++ b/frontend/bindings/voidraft/internal/services/models.ts @@ -203,7 +203,7 @@ export class SelfUpdateResult { } /** - * WindowInfo 窗口信息 + * WindowInfo 窗口信息(简化版) */ export class WindowInfo { "Window": application$0.WebviewWindow | null; @@ -238,6 +238,26 @@ export class WindowInfo { } } +/** + * WindowSnapService 窗口吸附服务 + */ +export class WindowSnapService { + + /** Creates a new WindowSnapService instance. */ + constructor($$source: Partial = {}) { + + Object.assign(this, $$source); + } + + /** + * Creates a new WindowSnapService instance from a string or object. + */ + static createFrom($$source: any = {}): WindowSnapService { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new WindowSnapService($$parsedSource as Partial); + } +} + // Private type creation functions const $$createType0 = application$0.WebviewWindow.createFrom; const $$createType1 = $Create.Nullable($$createType0); diff --git a/frontend/bindings/voidraft/internal/services/windowservice.ts b/frontend/bindings/voidraft/internal/services/windowservice.ts index 01a7db2..b3b9bd1 100644 --- a/frontend/bindings/voidraft/internal/services/windowservice.ts +++ b/frontend/bindings/voidraft/internal/services/windowservice.ts @@ -2,7 +2,7 @@ // This file is automatically generated. DO NOT EDIT /** - * WindowService 窗口管理服务 + * WindowService 窗口管理服务(专注于窗口生命周期管理) * @module */ @@ -46,6 +46,14 @@ export function OpenDocumentWindow(documentID: number): Promise & { cancel return $resultPromise; } +/** + * ServiceShutdown 实现服务关闭接口 + */ +export function ServiceShutdown(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(202192783) as any; + return $resultPromise; +} + /** * SetAppReferences 设置应用和主窗口引用 */ @@ -54,6 +62,14 @@ export function SetAppReferences(app: application$0.App | null, mainWindow: appl return $resultPromise; } +/** + * SetWindowSnapService 设置窗口吸附服务引用 + */ +export function SetWindowSnapService(snapService: $models.WindowSnapService | null): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(1105193745, snapService) as any; + return $resultPromise; +} + // Private type creation functions const $$createType0 = $models.WindowInfo.createFrom; const $$createType1 = $Create.Array($$createType0); diff --git a/frontend/bindings/voidraft/internal/services/windowsnapservice.ts b/frontend/bindings/voidraft/internal/services/windowsnapservice.ts new file mode 100644 index 0000000..160fb4f --- /dev/null +++ b/frontend/bindings/voidraft/internal/services/windowsnapservice.ts @@ -0,0 +1,87 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * WindowSnapService 窗口吸附服务 + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import {Call as $Call, Create as $Create} from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; + +/** + * Cleanup 清理资源 + */ +export function Cleanup(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(2155505498) as any; + return $resultPromise; +} + +/** + * OnWindowSnapConfigChanged 处理窗口吸附配置变更 + */ +export function OnWindowSnapConfigChanged(enabled: boolean, threshold: number): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(3794787039, enabled, threshold) as any; + return $resultPromise; +} + +/** + * RegisterWindow 注册需要吸附管理的窗口 + */ +export function RegisterWindow(documentID: number, window: application$0.WebviewWindow | null, title: string): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(1000222723, documentID, window, title) as any; + return $resultPromise; +} + +/** + * ServiceShutdown 实现服务关闭接口 + */ +export function ServiceShutdown(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(1172710495) as any; + return $resultPromise; +} + +/** + * SetAppReferences 设置应用和主窗口引用 + */ +export function SetAppReferences(app: application$0.App | null, mainWindow: application$0.WebviewWindow | null): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(1782093351, app, mainWindow) as any; + return $resultPromise; +} + +/** + * SetSnapEnabled 设置是否启用窗口吸附 + */ +export function SetSnapEnabled(enabled: boolean): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(2280126835, enabled) as any; + return $resultPromise; +} + +/** + * SetSnapThreshold 设置窗口吸附阈值 + */ +export function SetSnapThreshold(threshold: number): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(586790049, threshold) as any; + return $resultPromise; +} + +/** + * StartWindowSnapMonitor 启动窗口吸附监听器 + */ +export function StartWindowSnapMonitor(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(84533508) as any; + return $resultPromise; +} + +/** + * UnregisterWindow 取消注册窗口 + */ +export function UnregisterWindow(documentID: number): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(2844230768, documentID) as any; + return $resultPromise; +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2d145d5..1cf7b20 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -52,27 +52,27 @@ "prettier": "^3.6.2", "remarkable": "^2.0.1", "sass": "^1.90.0", - "vue": "^3.5.18", + "vue": "^3.5.19", "vue-i18n": "^11.1.11", "vue-pick-colors": "^1.8.0", "vue-router": "^4.5.1" }, "devDependencies": { - "@eslint/js": "^9.33.0", + "@eslint/js": "^9.34.0", "@lezer/generator": "^1.8.0", "@types/node": "^24.3.0", "@types/remarkable": "^2.0.8", "@vitejs/plugin-vue": "^6.0.1", "@wailsio/runtime": "latest", - "eslint": "^9.33.0", + "eslint": "^9.34.0", "eslint-plugin-vue": "^10.4.0", "globals": "^16.3.0", "typescript": "^5.9.2", - "typescript-eslint": "^8.39.1", + "typescript-eslint": "^8.40.0", "unplugin-vue-components": "^29.0.0", - "vite": "^7.1.2", + "vite": "^7.1.3", "vue-eslint-parser": "^10.2.0", - "vue-tsc": "^3.0.5" + "vue-tsc": "^3.0.6" } }, "node_modules/@babel/helper-string-parser": { @@ -1060,9 +1060,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.33.0", - "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.33.0.tgz", - "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "version": "9.34.0", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", "dev": true, "license": "MIT", "engines": { @@ -2128,17 +2128,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.39.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz", - "integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==", + "version": "8.40.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", + "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.39.1", - "@typescript-eslint/type-utils": "8.39.1", - "@typescript-eslint/utils": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/type-utils": "8.40.0", + "@typescript-eslint/utils": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2152,7 +2152,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.39.1", + "@typescript-eslint/parser": "^8.40.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -2168,16 +2168,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.39.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.39.1.tgz", - "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", + "version": "8.40.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.40.0.tgz", + "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.39.1", - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/typescript-estree": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1", + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", "debug": "^4.3.4" }, "engines": { @@ -2193,14 +2193,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.39.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", - "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", + "version": "8.40.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", + "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.39.1", - "@typescript-eslint/types": "^8.39.1", + "@typescript-eslint/tsconfig-utils": "^8.40.0", + "@typescript-eslint/types": "^8.40.0", "debug": "^4.3.4" }, "engines": { @@ -2215,14 +2215,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.39.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", - "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", + "version": "8.40.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz", + "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1" + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2233,9 +2233,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.39.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", - "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", + "version": "8.40.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", + "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", "dev": true, "license": "MIT", "engines": { @@ -2250,15 +2250,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.39.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz", - "integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==", + "version": "8.40.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz", + "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/typescript-estree": "8.39.1", - "@typescript-eslint/utils": "8.39.1", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/utils": "8.40.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2275,9 +2275,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.39.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.39.1.tgz", - "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", + "version": "8.40.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.40.0.tgz", + "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", "dev": true, "license": "MIT", "engines": { @@ -2289,16 +2289,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.39.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", - "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", + "version": "8.40.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", + "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.39.1", - "@typescript-eslint/tsconfig-utils": "8.39.1", - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1", + "@typescript-eslint/project-service": "8.40.0", + "@typescript-eslint/tsconfig-utils": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/visitor-keys": "8.40.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2344,16 +2344,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.39.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.39.1.tgz", - "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", + "version": "8.40.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.40.0.tgz", + "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.39.1", - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/typescript-estree": "8.39.1" + "@typescript-eslint/scope-manager": "8.40.0", + "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2368,13 +2368,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.39.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", - "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", + "version": "8.40.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", + "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/types": "8.40.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2403,68 +2403,68 @@ } }, "node_modules/@volar/language-core": { - "version": "2.4.22", - "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.22.tgz", - "integrity": "sha512-gp4M7Di5KgNyIyO903wTClYBavRt6UyFNpc5LWfyZr1lBsTUY+QrVZfmbNF2aCyfklBOVk9YC4p+zkwoyT7ECg==", + "version": "2.4.23", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.23.tgz", + "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.22" + "@volar/source-map": "2.4.23" } }, "node_modules/@volar/source-map": { - "version": "2.4.22", - "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.22.tgz", - "integrity": "sha512-L2nVr/1vei0xKRgO2tYVXtJYd09HTRjaZi418e85Q+QdbbqA8h7bBjfNyPPSsjnrOO4l4kaAo78c8SQUAdHvgA==", + "version": "2.4.23", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.23.tgz", + "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==", "dev": true, "license": "MIT" }, "node_modules/@volar/typescript": { - "version": "2.4.22", - "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.22.tgz", - "integrity": "sha512-6ZczlJW1/GWTrNnkmZxJp4qyBt/SGVlcTuCWpI5zLrdPdCZsj66Aff9ZsfFaT3TyjG8zVYgBMYPuCm/eRkpcpQ==", + "version": "2.4.23", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.23.tgz", + "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.22", + "@volar/language-core": "2.4.23", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "node_modules/@vue/compiler-core": { - "version": "3.5.18", - "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.18.tgz", - "integrity": "sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==", + "version": "3.5.19", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.19.tgz", + "integrity": "sha512-/afpyvlkrSNYbPo94Qu8GtIOWS+g5TRdOvs6XZNw6pWQQmj5pBgSZvEPOIZlqWq0YvoUhDDQaQ2TnzuJdOV4hA==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@vue/shared": "3.5.18", + "@babel/parser": "^7.28.3", + "@vue/shared": "3.5.19", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.18", - "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.18.tgz", - "integrity": "sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==", + "version": "3.5.19", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.19.tgz", + "integrity": "sha512-Drs6rPHQZx/pN9S6ml3Z3K/TWCIRPvzG2B/o5kFK9X0MNHt8/E+38tiRfojufrYBfA6FQUFB2qBBRXlcSXWtOA==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.18", - "@vue/shared": "3.5.18" + "@vue/compiler-core": "3.5.19", + "@vue/shared": "3.5.19" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.18", - "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.18.tgz", - "integrity": "sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==", + "version": "3.5.19", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.19.tgz", + "integrity": "sha512-YWCm1CYaJ+2RvNmhCwI7t3I3nU+hOrWGWMsn+Z/kmm1jy5iinnVtlmkiZwbLlbV1SRizX7vHsc0/bG5dj0zRTg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@vue/compiler-core": "3.5.18", - "@vue/compiler-dom": "3.5.18", - "@vue/compiler-ssr": "3.5.18", - "@vue/shared": "3.5.18", + "@babel/parser": "^7.28.3", + "@vue/compiler-core": "3.5.19", + "@vue/compiler-dom": "3.5.19", + "@vue/compiler-ssr": "3.5.19", + "@vue/shared": "3.5.19", "estree-walker": "^2.0.2", "magic-string": "^0.30.17", "postcss": "^8.5.6", @@ -2472,13 +2472,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.18", - "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.18.tgz", - "integrity": "sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==", + "version": "3.5.19", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.19.tgz", + "integrity": "sha512-/wx0VZtkWOPdiQLWPeQeqpHWR/LuNC7bHfSX7OayBTtUy8wur6vT6EQIX6Et86aED6J+y8tTw43qo2uoqGg5sw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.18", - "@vue/shared": "3.5.18" + "@vue/compiler-dom": "3.5.19", + "@vue/shared": "3.5.19" } }, "node_modules/@vue/compiler-vue2": { @@ -2526,13 +2526,13 @@ } }, "node_modules/@vue/language-core": { - "version": "3.0.5", - "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.0.5.tgz", - "integrity": "sha512-gCEjn9Ik7I/seHVNIEipOm8W+f3/kg60e8s1IgIkMYma2wu9ZGUTMv3mSL2bX+Md2L8fslceJ4SU8j1fgSRoiw==", + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.0.6.tgz", + "integrity": "sha512-e2RRzYWm+qGm8apUHW1wA5RQxzNhkqbbKdbKhiDUcmMrNAZGyM8aTiL3UrTqkaFI5s7wJRGGrp4u3jgusuBp2A==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.22", + "@volar/language-core": "2.4.23", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", @@ -2564,53 +2564,53 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.18", - "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.18.tgz", - "integrity": "sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==", + "version": "3.5.19", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.19.tgz", + "integrity": "sha512-4bueZg2qs5MSsK2dQk3sssV0cfvxb/QZntTC8v7J448GLgmfPkQ+27aDjlt40+XFqOwUq5yRxK5uQh14Fc9eVA==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.18" + "@vue/shared": "3.5.19" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.18", - "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.18.tgz", - "integrity": "sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==", + "version": "3.5.19", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.19.tgz", + "integrity": "sha512-TaooCr8Hge1sWjLSyhdubnuofs3shhzZGfyD11gFolZrny76drPwBVQj28/z/4+msSFb18tOIg6VVVgf9/IbIA==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.18", - "@vue/shared": "3.5.18" + "@vue/reactivity": "3.5.19", + "@vue/shared": "3.5.19" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.18", - "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.18.tgz", - "integrity": "sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==", + "version": "3.5.19", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.19.tgz", + "integrity": "sha512-qmahqeok6ztuUTmV8lqd7N9ymbBzctNF885n8gL3xdCC1u2RnM/coX16Via0AiONQXUoYpxPojL3U1IsDgSWUQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.18", - "@vue/runtime-core": "3.5.18", - "@vue/shared": "3.5.18", + "@vue/reactivity": "3.5.19", + "@vue/runtime-core": "3.5.19", + "@vue/shared": "3.5.19", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.18", - "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.18.tgz", - "integrity": "sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==", + "version": "3.5.19", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.19.tgz", + "integrity": "sha512-ZJ/zV9SQuaIO+BEEVq/2a6fipyrSYfjKMU3267bPUk+oTx/hZq3RzV7VCh0Unlppt39Bvh6+NzxeopIFv4HJNg==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.18", - "@vue/shared": "3.5.18" + "@vue/compiler-ssr": "3.5.19", + "@vue/shared": "3.5.19" }, "peerDependencies": { - "vue": "3.5.18" + "vue": "3.5.19" } }, "node_modules/@vue/shared": { - "version": "3.5.18", - "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.18.tgz", - "integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==", + "version": "3.5.19", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.19.tgz", + "integrity": "sha512-IhXCOn08wgKrLQxRFKKlSacWg4Goi1BolrdEeLYn6tgHjJNXVrWJ5nzoxZqNwl5p88aLlQ8LOaoMa3AYvaKJ/Q==", "license": "MIT" }, "node_modules/@wailsio/runtime": { @@ -2661,9 +2661,9 @@ } }, "node_modules/alien-signals": { - "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-2.0.6.tgz", - "integrity": "sha512-P3TxJSe31bUHBiblg59oU1PpaWPtmxF9GhJ/cB7OkgJ0qN/ifFSKUI25/v8ZhsT+lIG6ac8DpTOplXxORX6F3Q==", + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-2.0.7.tgz", + "integrity": "sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg==", "dev": true, "license": "MIT" }, @@ -3166,9 +3166,9 @@ } }, "node_modules/eslint": { - "version": "9.33.0", - "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.33.0.tgz", - "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "version": "9.34.0", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", "dependencies": { @@ -3178,7 +3178,7 @@ "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.33.0", + "@eslint/js": "9.34.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -4794,16 +4794,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.39.1", - "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.39.1.tgz", - "integrity": "sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg==", + "version": "8.40.0", + "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.40.0.tgz", + "integrity": "sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.39.1", - "@typescript-eslint/parser": "8.39.1", - "@typescript-eslint/typescript-estree": "8.39.1", - "@typescript-eslint/utils": "8.39.1" + "@typescript-eslint/eslint-plugin": "8.40.0", + "@typescript-eslint/parser": "8.40.0", + "@typescript-eslint/typescript-estree": "8.40.0", + "@typescript-eslint/utils": "8.40.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5103,14 +5103,14 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.1.2", - "resolved": "https://registry.npmmirror.com/vite/-/vite-7.1.2.tgz", - "integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==", + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/vite/-/vite-7.1.3.tgz", + "integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.6", + "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", @@ -5178,11 +5178,14 @@ } }, "node_modules/vite/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -5213,16 +5216,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.18", - "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.18.tgz", - "integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==", + "version": "3.5.19", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.19.tgz", + "integrity": "sha512-ZRh0HTmw6KChRYWgN8Ox/wi7VhpuGlvMPrHjIsdRbzKNgECFLzy+dKL5z9yGaBSjCpmcfJCbh3I1tNSRmBz2tg==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.18", - "@vue/compiler-sfc": "3.5.18", - "@vue/runtime-dom": "3.5.18", - "@vue/server-renderer": "3.5.18", - "@vue/shared": "3.5.18" + "@vue/compiler-dom": "3.5.19", + "@vue/compiler-sfc": "3.5.19", + "@vue/runtime-dom": "3.5.19", + "@vue/server-renderer": "3.5.19", + "@vue/shared": "3.5.19" }, "peerDependencies": { "typescript": "*" @@ -5318,14 +5321,14 @@ "license": "MIT" }, "node_modules/vue-tsc": { - "version": "3.0.5", - "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.0.5.tgz", - "integrity": "sha512-PsTFN9lo1HJCrZw9NoqjYcAbYDXY0cOKyuW2E7naX5jcaVyWpqEsZOHN9Dws5890E8e5SDAD4L4Zam3dxG3/Cw==", + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.0.6.tgz", + "integrity": "sha512-Tbs8Whd43R2e2nxez4WXPvvdjGbW24rOSgRhLOHXzWiT4pcP4G7KeWh0YCn18rF4bVwv7tggLLZ6MJnO6jXPBg==", "dev": true, "license": "MIT", "dependencies": { - "@volar/typescript": "2.4.22", - "@vue/language-core": "3.0.5" + "@volar/typescript": "2.4.23", + "@vue/language-core": "3.0.6" }, "bin": { "vue-tsc": "bin/vue-tsc.js" diff --git a/frontend/package.json b/frontend/package.json index f5676bd..293db5e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -56,26 +56,26 @@ "prettier": "^3.6.2", "remarkable": "^2.0.1", "sass": "^1.90.0", - "vue": "^3.5.18", + "vue": "^3.5.19", "vue-i18n": "^11.1.11", "vue-pick-colors": "^1.8.0", "vue-router": "^4.5.1" }, "devDependencies": { - "@eslint/js": "^9.33.0", + "@eslint/js": "^9.34.0", "@lezer/generator": "^1.8.0", "@types/node": "^24.3.0", "@types/remarkable": "^2.0.8", "@vitejs/plugin-vue": "^6.0.1", "@wailsio/runtime": "latest", - "eslint": "^9.33.0", + "eslint": "^9.34.0", "eslint-plugin-vue": "^10.4.0", "globals": "^16.3.0", "typescript": "^5.9.2", - "typescript-eslint": "^8.39.1", + "typescript-eslint": "^8.40.0", "unplugin-vue-components": "^29.0.0", - "vite": "^7.1.2", + "vite": "^7.1.3", "vue-eslint-parser": "^10.2.0", - "vue-tsc": "^3.0.5" + "vue-tsc": "^3.0.6" } } diff --git a/frontend/src/stores/configStore.ts b/frontend/src/stores/configStore.ts index c89d044..1f8ba07 100644 --- a/frontend/src/stores/configStore.ts +++ b/frontend/src/stores/configStore.ts @@ -62,7 +62,9 @@ const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = { enableSystemTray: 'general.enableSystemTray', startAtLogin: 'general.startAtLogin', enableGlobalHotkey: 'general.enableGlobalHotkey', - globalHotkey: 'general.globalHotkey' + globalHotkey: 'general.globalHotkey', + enableWindowSnap: 'general.enableWindowSnap', + snapThreshold: 'general.snapThreshold' } as const; const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = { @@ -176,7 +178,9 @@ const DEFAULT_CONFIG: AppConfig = { alt: true, win: false, key: 'X' - } + }, + enableWindowSnap: true, + snapThreshold: 15 }, editing: { fontSize: CONFIG_LIMITS.fontSize.default, @@ -520,6 +524,10 @@ export const useConfigStore = defineStore('config', () => { // 再调用系统设置API await StartupService.SetEnabled(value); }, + + // 窗口吸附配置相关方法 + setEnableWindowSnap: async (value: boolean) => await updateGeneralConfig('enableWindowSnap', value), + setSnapThreshold: async (value: number) => await updateGeneralConfig('snapThreshold', value), // 更新配置相关方法 setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value), diff --git a/internal/models/config.go b/internal/models/config.go index 4406151..f85c17f 100644 --- a/internal/models/config.go +++ b/internal/models/config.go @@ -68,6 +68,10 @@ type GeneralConfig struct { EnableSystemTray bool `json:"enableSystemTray"` // 是否启用系统托盘 StartAtLogin bool `json:"startAtLogin"` // 开机启动设置 + // 窗口吸附设置 + EnableWindowSnap bool `json:"enableWindowSnap"` // 是否启用窗口吸附功能 + SnapThreshold int `json:"snapThreshold"` // 吸附距离阈值(像素) + // 全局热键设置 EnableGlobalHotkey bool `json:"enableGlobalHotkey"` // 是否启用全局热键 GlobalHotkey HotkeyCombo `json:"globalHotkey"` // 全局热键组合 @@ -145,6 +149,8 @@ func NewDefaultAppConfig() *AppConfig { DataPath: dataDir, EnableSystemTray: true, StartAtLogin: false, + EnableWindowSnap: true, // 默认启用窗口吸附 + SnapThreshold: 15, // 默认15像素的吸附阈值 EnableGlobalHotkey: false, GlobalHotkey: HotkeyCombo{ Ctrl: false, diff --git a/internal/models/window.go b/internal/models/window.go new file mode 100644 index 0000000..95d1edf --- /dev/null +++ b/internal/models/window.go @@ -0,0 +1,41 @@ +package models + +import "time" + +// SnapEdge 表示吸附的边缘类型 +type SnapEdge int + +const ( + SnapEdgeNone SnapEdge = iota // 未吸附 + SnapEdgeTop // 吸附到上边缘 + SnapEdgeRight // 吸附到右边缘 + SnapEdgeBottom // 吸附到下边缘 + SnapEdgeLeft // 吸附到左边缘 + SnapEdgeTopRight // 吸附到右上角 + SnapEdgeBottomRight // 吸附到右下角 + SnapEdgeBottomLeft // 吸附到左下角 + SnapEdgeTopLeft // 吸附到左上角 +) + +// WindowPosition 窗口位置 +type WindowPosition struct { + X int `json:"x"` // X坐标 + Y int `json:"y"` // Y坐标 +} + +// SnapPosition 表示吸附的相对位置 +type SnapPosition struct { + X int `json:"x"` // X轴相对偏移 + Y int `json:"y"` // Y轴相对偏移 +} + +// WindowInfo 窗口信息 +type WindowInfo struct { + DocumentID int64 `json:"documentID"` // 文档ID + Title string `json:"title"` // 窗口标题 + IsSnapped bool `json:"isSnapped"` // 是否处于吸附状态 + SnapOffset SnapPosition `json:"snapOffset"` // 与主窗口的相对位置偏移 + SnapEdge SnapEdge `json:"snapEdge"` // 吸附的边缘类型 + LastPos WindowPosition `json:"lastPos"` // 上一次记录的窗口位置 + MoveTime time.Time `json:"moveTime"` // 上次移动时间,用于判断移动速度 +} diff --git a/internal/services/config_notification_service.go b/internal/services/config_notification_service.go index 93435f6..a37d50a 100644 --- a/internal/services/config_notification_service.go +++ b/internal/services/config_notification_service.go @@ -24,6 +24,8 @@ const ( ConfigChangeTypeDataPath ConfigChangeType = "datapath" // ConfigChangeTypeBackup 备份配置变更 ConfigChangeTypeBackup ConfigChangeType = "backup" + // ConfigChangeTypeWindowSnap 窗口吸附配置变更 + ConfigChangeTypeWindowSnap ConfigChangeType = "windowsnap" ) // ConfigChangeCallback 配置变更回调函数类型 @@ -470,6 +472,29 @@ func CreateBackupConfigListener(name string, callback func(config *models.GitBac } } +// CreateWindowSnapConfigListener 创建窗口吸附配置监听器 +func CreateWindowSnapConfigListener(name string, callback func(enabled bool, threshold int) error) *ConfigListener { + return &ConfigListener{ + Name: name, + ChangeType: ConfigChangeTypeWindowSnap, + Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error { + if newConfig == nil { + defaultConfig := models.NewDefaultAppConfig() + return callback(defaultConfig.General.EnableWindowSnap, defaultConfig.General.SnapThreshold) + } + return callback(newConfig.General.EnableWindowSnap, newConfig.General.SnapThreshold) + }, + DebounceDelay: 200 * time.Millisecond, + GetConfigFunc: func(k *koanf.Koanf) *models.AppConfig { + var config models.AppConfig + if err := k.Unmarshal("", &config); err != nil { + return nil + } + return &config + }, + } +} + // ServiceShutdown 关闭服务 func (cns *ConfigNotificationService) ServiceShutdown() error { cns.Cleanup() diff --git a/internal/services/config_service.go b/internal/services/config_service.go index 450d434..de6fe07 100644 --- a/internal/services/config_service.go +++ b/internal/services/config_service.go @@ -308,6 +308,16 @@ func (cs *ConfigService) SetBackupConfigChangeCallback(callback func(config *mod return cs.notificationService.RegisterListener(backupListener) } +// SetWindowSnapConfigChangeCallback 设置窗口吸附配置变更回调 +func (cs *ConfigService) SetWindowSnapConfigChangeCallback(callback func(enabled bool, threshold int) error) error { + cs.mu.Lock() + defer cs.mu.Unlock() + + // 创建窗口吸附配置监听器并注册 + windowSnapListener := CreateWindowSnapConfigListener("DefaultWindowSnapConfigListener", callback) + return cs.notificationService.RegisterListener(windowSnapListener) +} + // ServiceShutdown 关闭服务 func (cs *ConfigService) ServiceShutdown() error { cs.stopWatching() diff --git a/internal/services/service_manager.go b/internal/services/service_manager.go index a6425f6..bc29739 100644 --- a/internal/services/service_manager.go +++ b/internal/services/service_manager.go @@ -13,6 +13,7 @@ type ServiceManager struct { databaseService *DatabaseService documentService *DocumentService windowService *WindowService + windowSnapService *WindowSnapService migrationService *MigrationService systemService *SystemService hotkeyService *HotkeyService @@ -48,6 +49,12 @@ func NewServiceManager() *ServiceManager { // 初始化窗口服务 windowService := NewWindowService(logger, documentService) + // 初始化窗口吸附服务 + windowSnapService := NewWindowSnapService(logger, configService) + + // 将吸附服务与窗口服务关联 + windowService.SetWindowSnapService(windowSnapService) + // 初始化系统服务 systemService := NewSystemService(logger) @@ -108,11 +115,20 @@ func NewServiceManager() *ServiceManager { panic(err) } + // 设置窗口吸附配置变更回调 + err = configService.SetWindowSnapConfigChangeCallback(func(enabled bool, threshold int) error { + return windowSnapService.OnWindowSnapConfigChanged(enabled, threshold) + }) + if err != nil { + panic(err) + } + return &ServiceManager{ configService: configService, databaseService: databaseService, documentService: documentService, windowService: windowService, + windowSnapService: windowSnapService, migrationService: migrationService, systemService: systemService, hotkeyService: hotkeyService, @@ -136,6 +152,7 @@ func (sm *ServiceManager) GetServices() []application.Service { application.NewService(sm.databaseService), application.NewService(sm.documentService), application.NewService(sm.windowService), + application.NewService(sm.windowSnapService), application.NewService(sm.keyBindingService), application.NewService(sm.extensionService), application.NewService(sm.migrationService), @@ -221,3 +238,8 @@ func (sm *ServiceManager) GetDocumentService() *DocumentService { func (sm *ServiceManager) GetThemeService() *ThemeService { return sm.themeService } + +// GetWindowSnapService 获取窗口吸附服务实例 +func (sm *ServiceManager) GetWindowSnapService() *WindowSnapService { + return sm.windowSnapService +} diff --git a/internal/services/window_service.go b/internal/services/window_service.go index 3c9b770..48b1a67 100644 --- a/internal/services/window_service.go +++ b/internal/services/window_service.go @@ -9,14 +9,14 @@ import ( "github.com/wailsapp/wails/v3/pkg/services/log" ) -// WindowInfo 窗口信息 +// WindowInfo 窗口信息(简化版) type WindowInfo struct { Window *application.WebviewWindow DocumentID int64 Title string } -// WindowService 窗口管理服务 +// WindowService 窗口管理服务(专注于窗口生命周期管理) type WindowService struct { logger *log.LogService documentService *DocumentService @@ -24,6 +24,9 @@ type WindowService struct { mainWindow *application.WebviewWindow windows map[int64]*WindowInfo // documentID -> WindowInfo mu sync.RWMutex + + // 吸附服务引用 + windowSnapService *WindowSnapService } // NewWindowService 创建新的窗口服务实例 @@ -39,10 +42,20 @@ func NewWindowService(logger *log.LogService, documentService *DocumentService) } } +// SetWindowSnapService 设置窗口吸附服务引用 +func (ws *WindowService) SetWindowSnapService(snapService *WindowSnapService) { + ws.windowSnapService = snapService +} + // SetAppReferences 设置应用和主窗口引用 func (ws *WindowService) SetAppReferences(app *application.App, mainWindow *application.WebviewWindow) { ws.app = app ws.mainWindow = mainWindow + + // 如果吸附服务已设置,也为它设置引用 + if ws.windowSnapService != nil { + ws.windowSnapService.SetAppReferences(app, mainWindow) + } } // OpenDocumentWindow 为指定文档ID打开新窗口 @@ -101,9 +114,14 @@ func (ws *WindowService) OpenDocumentWindow(documentID int64) error { } ws.windows[documentID] = windowInfo - // 注册窗口关闭事件 + // 注册窗口事件 ws.registerWindowEvents(newWindow, documentID) + // 向吸附服务注册新窗口 + if ws.windowSnapService != nil { + ws.windowSnapService.RegisterWindow(documentID, newWindow, doc.Title) + } + return nil } @@ -119,12 +137,17 @@ func (ws *WindowService) registerWindowEvents(window *application.WebviewWindow, func (ws *WindowService) onWindowClosing(documentID int64) { ws.mu.Lock() defer ws.mu.Unlock() + windowInfo, exists := ws.windows[documentID] if exists { windowInfo.Window.Close() delete(ws.windows, documentID) - } + // 从吸附服务中取消注册 + if ws.windowSnapService != nil { + ws.windowSnapService.UnregisterWindow(documentID) + } + } } // GetOpenWindows 获取所有打开的窗口信息 @@ -147,3 +170,18 @@ func (ws *WindowService) IsDocumentWindowOpen(documentID int64) bool { _, exists := ws.windows[documentID] return exists } + +// ServiceShutdown 实现服务关闭接口 +func (ws *WindowService) ServiceShutdown() error { + // 关闭所有窗口 + ws.mu.Lock() + defer ws.mu.Unlock() + + for documentID := range ws.windows { + if ws.windowSnapService != nil { + ws.windowSnapService.UnregisterWindow(documentID) + } + } + + return nil +} diff --git a/internal/services/window_snap_service.go b/internal/services/window_snap_service.go new file mode 100644 index 0000000..b9161c9 --- /dev/null +++ b/internal/services/window_snap_service.go @@ -0,0 +1,603 @@ +package services + +import ( + "context" + "math" + "sync" + "time" + "voidraft/internal/models" + + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/services/log" +) + +// WindowSnapService 窗口吸附服务 +type WindowSnapService struct { + logger *log.LogService + configService *ConfigService + app *application.App + mainWindow *application.WebviewWindow + mu sync.RWMutex + + // 吸附配置 + snapThreshold int // 吸附触发的阈值距离(像素) + snapEnabled bool // 是否启用窗口吸附功能 + + // 定时器控制 + snapTicker *time.Ticker + ctx context.Context + cancel context.CancelFunc + + // 性能优化相关 + lastMainWindowPos models.WindowPosition // 缓存主窗口上次位置 + changedWindows map[int64]bool // 记录哪些窗口发生了变化 + skipFrames int // 跳帧计数器,用于降低检测频率 + + // 监听的窗口列表 + managedWindows map[int64]*SnapWindowInfo // documentID -> SnapWindowInfo +} + +// SnapWindowInfo 吸附窗口信息 +type SnapWindowInfo struct { + Window *application.WebviewWindow + DocumentID int64 + Title string + IsSnapped bool // 是否处于吸附状态 + SnapOffset models.SnapPosition // 与主窗口的相对位置偏移 + SnapEdge models.SnapEdge // 吸附的边缘类型 + LastPos models.WindowPosition // 上一次记录的窗口位置 + MoveTime time.Time // 上次移动时间,用于判断移动速度 +} + +// NewWindowSnapService 创建新的窗口吸附服务实例 +func NewWindowSnapService(logger *log.LogService, configService *ConfigService) *WindowSnapService { + if logger == nil { + logger = log.New() + } + + // 从配置获取窗口吸附设置 + config, err := configService.GetConfig() + snapEnabled := true // 默认启用 + snapThreshold := 15 // 默认阈值 + + if err == nil { + snapEnabled = config.General.EnableWindowSnap + snapThreshold = config.General.SnapThreshold + } + + return &WindowSnapService{ + logger: logger, + configService: configService, + snapThreshold: snapThreshold, + snapEnabled: snapEnabled, + managedWindows: make(map[int64]*SnapWindowInfo), + changedWindows: make(map[int64]bool), + skipFrames: 0, + } +} + +// SetAppReferences 设置应用和主窗口引用 +func (wss *WindowSnapService) SetAppReferences(app *application.App, mainWindow *application.WebviewWindow) { + wss.app = app + wss.mainWindow = mainWindow + + // 初始化上下文,用于控制goroutine的生命周期 + wss.ctx, wss.cancel = context.WithCancel(context.Background()) + + // 启动窗口吸附监听器 + wss.StartWindowSnapMonitor() +} + +// RegisterWindow 注册需要吸附管理的窗口 +func (wss *WindowSnapService) RegisterWindow(documentID int64, window *application.WebviewWindow, title string) { + wss.mu.Lock() + defer wss.mu.Unlock() + + wss.managedWindows[documentID] = &SnapWindowInfo{ + Window: window, + DocumentID: documentID, + Title: title, + IsSnapped: false, + SnapOffset: models.SnapPosition{X: 0, Y: 0}, + SnapEdge: models.SnapEdgeNone, + LastPos: models.WindowPosition{X: 0, Y: 0}, + MoveTime: time.Now(), + } +} + +// UnregisterWindow 取消注册窗口 +func (wss *WindowSnapService) UnregisterWindow(documentID int64) { + wss.mu.Lock() + defer wss.mu.Unlock() + + delete(wss.managedWindows, documentID) + delete(wss.changedWindows, documentID) +} + +// SetSnapEnabled 设置是否启用窗口吸附 +func (wss *WindowSnapService) SetSnapEnabled(enabled bool) { + wss.mu.Lock() + defer wss.mu.Unlock() + + if wss.snapEnabled == enabled { + return + } + + wss.snapEnabled = enabled + + // 如果禁用吸附,解除所有吸附窗口 + if !enabled { + for _, windowInfo := range wss.managedWindows { + if windowInfo.IsSnapped { + windowInfo.IsSnapped = false + windowInfo.SnapEdge = models.SnapEdgeNone + } + } + // 停止定时器 + if wss.snapTicker != nil { + wss.snapTicker.Stop() + wss.snapTicker = nil + } + } else if wss.snapTicker == nil && wss.app != nil { + // 重新启动定时器 + wss.StartWindowSnapMonitor() + } +} + +// SetSnapThreshold 设置窗口吸附阈值 +func (wss *WindowSnapService) SetSnapThreshold(threshold int) { + wss.mu.Lock() + defer wss.mu.Unlock() + + if threshold <= 0 || wss.snapThreshold == threshold { + return + } + + wss.snapThreshold = threshold +} + +// OnWindowSnapConfigChanged 处理窗口吸附配置变更 +func (wss *WindowSnapService) OnWindowSnapConfigChanged(enabled bool, threshold int) error { + wss.SetSnapEnabled(enabled) + wss.SetSnapThreshold(threshold) + return nil +} + +// StartWindowSnapMonitor 启动窗口吸附监听器 +func (wss *WindowSnapService) StartWindowSnapMonitor() { + // 如果定时器已存在,先停止它 + if wss.snapTicker != nil { + wss.snapTicker.Stop() + } + + // 只有在吸附功能启用时才启动监听器 + if !wss.snapEnabled { + return + } + + // 创建新的定时器,20fps 以优化性能 + wss.snapTicker = time.NewTicker(50 * time.Millisecond) + + // 启动goroutine持续监听窗口位置 + go func() { + for { + select { + case <-wss.snapTicker.C: + wss.checkAndApplySnapping() + case <-wss.ctx.Done(): + // 上下文取消时停止goroutine + return + } + } + }() +} + +// checkAndApplySnapping 检测并应用窗口吸附(性能优化版本) +func (wss *WindowSnapService) checkAndApplySnapping() { + if !wss.snapEnabled { + return + } + + // 性能优化:每3帧执行一次完整检测,其他时间只处理变化的窗口 + wss.skipFrames++ + fullCheck := wss.skipFrames%3 == 0 + + wss.mu.Lock() + defer wss.mu.Unlock() + + // 检查主窗口是否存在且可见 + if wss.mainWindow == nil || !wss.isMainWindowAvailable() { + // 主窗口不可用,解除所有吸附 + for _, windowInfo := range wss.managedWindows { + if windowInfo.IsSnapped { + windowInfo.IsSnapped = false + windowInfo.SnapEdge = models.SnapEdgeNone + } + } + // 清空变化记录 + wss.changedWindows = make(map[int64]bool) + return + } + + mainPos, _ := wss.getWindowPosition(wss.mainWindow) + + // 检查主窗口是否移动了 + mainWindowMoved := mainPos.X != wss.lastMainWindowPos.X || mainPos.Y != wss.lastMainWindowPos.Y + if mainWindowMoved { + wss.lastMainWindowPos = mainPos + // 主窗口移动了,标记所有吸附的窗口为变化 + for documentID, windowInfo := range wss.managedWindows { + if windowInfo.IsSnapped { + wss.changedWindows[documentID] = true + } + } + } + + for documentID, windowInfo := range wss.managedWindows { + currentPos, _ := wss.getWindowPosition(windowInfo.Window) + + // 检查窗口是否移动了 + hasMoved := currentPos.X != windowInfo.LastPos.X || currentPos.Y != windowInfo.LastPos.Y + if hasMoved { + windowInfo.MoveTime = time.Now() + windowInfo.LastPos = currentPos + wss.changedWindows[documentID] = true + } + + // 性能优化:只处理变化的窗口或进行完整检查时 + if !fullCheck && !wss.changedWindows[documentID] { + continue + } + + if windowInfo.IsSnapped { + // 窗口已吸附,检查是否需要更新位置或解除吸附 + wss.handleSnappedWindow(windowInfo, mainPos, currentPos) + } else { + // 窗口未吸附,检查是否应该吸附 + wss.handleUnsnappedWindow(windowInfo) + } + + // 处理完成后清除变化标记 + delete(wss.changedWindows, documentID) + } +} + +// isMainWindowAvailable 检查主窗口是否可用 +func (wss *WindowSnapService) isMainWindowAvailable() bool { + if wss.mainWindow == nil { + return false + } + + // 检查主窗口是否可见和正常状态 + return wss.mainWindow.IsVisible() +} + +// handleSnappedWindow 处理已吸附的窗口 +func (wss *WindowSnapService) handleSnappedWindow(windowInfo *SnapWindowInfo, mainPos models.WindowPosition, currentPos models.WindowPosition) { + // 计算预期位置基于主窗口的新位置 + expectedX := mainPos.X + windowInfo.SnapOffset.X + expectedY := mainPos.Y + windowInfo.SnapOffset.Y + + // 计算当前位置与预期位置的距离 + distanceX := math.Abs(float64(currentPos.X - expectedX)) + distanceY := math.Abs(float64(currentPos.Y - expectedY)) + maxDistance := math.Max(distanceX, distanceY) + + // 检测是否为用户主动拖拽:如果窗口移动幅度超过阈值且是最近移动的 + userDragThreshold := float64(wss.snapThreshold) + isUserDrag := maxDistance > userDragThreshold && time.Since(windowInfo.MoveTime) < 100*time.Millisecond + + if isUserDrag { + // 用户主动拖拽,立即解除吸附 + windowInfo.IsSnapped = false + windowInfo.SnapEdge = models.SnapEdgeNone + return + } + + // 对于主窗口移动导致的位置变化,立即跟随且不解除吸附 + if maxDistance > 0 { + // 直接调整到预期位置,不使用平滑移动以提高响应速度 + windowInfo.Window.SetPosition(expectedX, expectedY) + windowInfo.LastPos = models.WindowPosition{X: expectedX, Y: expectedY} + } +} + +// handleUnsnappedWindow 处理未吸附的窗口 +func (wss *WindowSnapService) handleUnsnappedWindow(windowInfo *SnapWindowInfo) { + // 检查是否应该吸附到主窗口 + should, snapEdge := wss.shouldSnapToMainWindow(windowInfo) + if should { + // 获取主窗口位置用于计算偏移量 + mainPos, _ := wss.getWindowPosition(wss.mainWindow) + + // 设置吸附状态 + windowInfo.IsSnapped = true + windowInfo.SnapEdge = snapEdge + + // 执行即时吸附,产生明显的吸附效果 + wss.snapWindowToMainWindow(windowInfo, snapEdge) + + // 重新获取吸附后的位置来计算偏移量 + newPos, _ := wss.getWindowPosition(windowInfo.Window) + windowInfo.SnapOffset.X = newPos.X - mainPos.X + windowInfo.SnapOffset.Y = newPos.Y - mainPos.Y + windowInfo.LastPos = newPos + } +} + +// getWindowPosition 获取窗口的位置 +func (wss *WindowSnapService) getWindowPosition(window *application.WebviewWindow) (models.WindowPosition, bool) { + x, y := window.Position() + return models.WindowPosition{X: x, Y: y}, true +} + +// shouldSnapToMainWindow 检查窗口是否应该吸附到主窗口(支持角落吸附) +func (wss *WindowSnapService) shouldSnapToMainWindow(windowInfo *SnapWindowInfo) (bool, models.SnapEdge) { + // 降低防抖时间,提高吸附响应速度 + if time.Since(windowInfo.MoveTime) < 50*time.Millisecond { + return false, models.SnapEdgeNone + } + + // 获取两个窗口的位置 + mainPos, _ := wss.getWindowPosition(wss.mainWindow) + windowPos, _ := wss.getWindowPosition(windowInfo.Window) + + // 获取主窗口尺寸 + mainWidth, mainHeight := wss.mainWindow.Size() + + // 获取子窗口尺寸 + windowWidth, windowHeight := windowInfo.Window.Size() + + // 计算各个边缘的距离 + threshold := float64(wss.snapThreshold) + cornerThreshold := threshold * 1.5 // 角落吸附需要更大的阈值 + + // 主窗口的四个边界 + mainLeft := mainPos.X + mainTop := mainPos.Y + mainRight := mainPos.X + mainWidth + mainBottom := mainPos.Y + mainHeight + + // 子窗口的四个边界 + windowLeft := windowPos.X + windowTop := windowPos.Y + windowRight := windowPos.X + windowWidth + windowBottom := windowPos.Y + windowHeight + + // 存储每个边缘的吸附信息 + type snapCandidate struct { + edge models.SnapEdge + distance float64 + overlap float64 // 重叠度,用于优先级判断 + isCorner bool // 是否为角落吸附 + } + + var candidates []snapCandidate + + // ==== 检查角落吸附(优先级最高) ==== + + // 1. 右上角 (SnapEdgeTopRight) + distToTopRight := math.Sqrt(math.Pow(float64(mainRight-windowLeft), 2) + math.Pow(float64(mainTop-windowBottom), 2)) + if distToTopRight <= cornerThreshold { + // 检查是否接近右上角区域 + horizDist := math.Abs(float64(mainRight - windowLeft)) + vertDist := math.Abs(float64(mainTop - windowBottom)) + if horizDist <= cornerThreshold && vertDist <= cornerThreshold { + candidates = append(candidates, snapCandidate{models.SnapEdgeTopRight, distToTopRight, 100, true}) + } + } + + // 2. 右下角 (SnapEdgeBottomRight) + distToBottomRight := math.Sqrt(math.Pow(float64(mainRight-windowLeft), 2) + math.Pow(float64(mainBottom-windowTop), 2)) + if distToBottomRight <= cornerThreshold { + horizDist := math.Abs(float64(mainRight - windowLeft)) + vertDist := math.Abs(float64(mainBottom - windowTop)) + if horizDist <= cornerThreshold && vertDist <= cornerThreshold { + candidates = append(candidates, snapCandidate{models.SnapEdgeBottomRight, distToBottomRight, 100, true}) + } + } + + // 3. 左下角 (SnapEdgeBottomLeft) + distToBottomLeft := math.Sqrt(math.Pow(float64(mainLeft-windowRight), 2) + math.Pow(float64(mainBottom-windowTop), 2)) + if distToBottomLeft <= cornerThreshold { + horizDist := math.Abs(float64(mainLeft - windowRight)) + vertDist := math.Abs(float64(mainBottom - windowTop)) + if horizDist <= cornerThreshold && vertDist <= cornerThreshold { + candidates = append(candidates, snapCandidate{models.SnapEdgeBottomLeft, distToBottomLeft, 100, true}) + } + } + + // 4. 左上角 (SnapEdgeTopLeft) + distToTopLeft := math.Sqrt(math.Pow(float64(mainLeft-windowRight), 2) + math.Pow(float64(mainTop-windowBottom), 2)) + if distToTopLeft <= cornerThreshold { + horizDist := math.Abs(float64(mainLeft - windowRight)) + vertDist := math.Abs(float64(mainTop - windowBottom)) + if horizDist <= cornerThreshold && vertDist <= cornerThreshold { + candidates = append(candidates, snapCandidate{models.SnapEdgeTopLeft, distToTopLeft, 100, true}) + } + } + + // ==== 检查边缘吸附(只在没有角落吸附时检查) ==== + if len(candidates) == 0 { + // 1. 吸附到主窗口右侧 + distToRight := math.Abs(float64(mainRight - windowLeft)) + if distToRight <= threshold { + // 计算垂直重叠 + overlapTop := math.Max(float64(mainTop), float64(windowTop)) + overlapBottom := math.Min(float64(mainBottom), float64(windowBottom)) + verticalOverlap := math.Max(0, overlapBottom-overlapTop) + candidates = append(candidates, snapCandidate{models.SnapEdgeRight, distToRight, verticalOverlap, false}) + } + + // 2. 吸附到主窗口左侧 + distToLeft := math.Abs(float64(mainLeft - windowRight)) + if distToLeft <= threshold { + // 计算垂直重叠 + overlapTop := math.Max(float64(mainTop), float64(windowTop)) + overlapBottom := math.Min(float64(mainBottom), float64(windowBottom)) + verticalOverlap := math.Max(0, overlapBottom-overlapTop) + candidates = append(candidates, snapCandidate{models.SnapEdgeLeft, distToLeft, verticalOverlap, false}) + } + + // 3. 吸附到主窗口底部 + distToBottom := math.Abs(float64(mainBottom - windowTop)) + if distToBottom <= threshold { + // 计算水平重叠 + overlapLeft := math.Max(float64(mainLeft), float64(windowLeft)) + overlapRight := math.Min(float64(mainRight), float64(windowRight)) + horizontalOverlap := math.Max(0, overlapRight-overlapLeft) + candidates = append(candidates, snapCandidate{models.SnapEdgeBottom, distToBottom, horizontalOverlap, false}) + } + + // 4. 吸附到主窗口顶部 + distToTop := math.Abs(float64(mainTop - windowBottom)) + if distToTop <= threshold { + // 计算水平重叠 + overlapLeft := math.Max(float64(mainLeft), float64(windowLeft)) + overlapRight := math.Min(float64(mainRight), float64(windowRight)) + horizontalOverlap := math.Max(0, overlapRight-overlapLeft) + candidates = append(candidates, snapCandidate{models.SnapEdgeTop, distToTop, horizontalOverlap, false}) + } + } + + // 如果没有候选,不吸附 + if len(candidates) == 0 { + return false, models.SnapEdgeNone + } + + // 选择最佳吸附位置:角落吸附优先,其次考虑重叠度,最后考虑距离 + bestCandidate := candidates[0] + for _, candidate := range candidates[1:] { + // 角落吸附优先级最高 + if candidate.isCorner && !bestCandidate.isCorner { + bestCandidate = candidate + } else if bestCandidate.isCorner && !candidate.isCorner { + // 继续使用当前的角落吸附 + continue + } else { + // 同类型的吸附,比较重叠度和距离 + if math.Abs(candidate.overlap-bestCandidate.overlap) < 10 { + if candidate.distance < bestCandidate.distance { + bestCandidate = candidate + } + } else if candidate.overlap > bestCandidate.overlap { + // 重叠度更高的优先 + bestCandidate = candidate + } + } + } + + return true, bestCandidate.edge +} + +// snapWindowToMainWindow 将窗口精确吸附到主窗口边缘(支持角落吸附) +func (wss *WindowSnapService) snapWindowToMainWindow(windowInfo *SnapWindowInfo, snapEdge models.SnapEdge) { + // 获取主窗口位置和尺寸 + mainPos, _ := wss.getWindowPosition(wss.mainWindow) + mainWidth, mainHeight := wss.mainWindow.Size() + + // 获取子窗口位置和尺寸 + windowPos, _ := wss.getWindowPosition(windowInfo.Window) + windowWidth, windowHeight := windowInfo.Window.Size() + + // 计算目标位置 + var targetX, targetY int + + switch snapEdge { + case models.SnapEdgeRight: + // 吸附到主窗口右侧 + targetX = mainPos.X + mainWidth + targetY = windowPos.Y // 保持当前 Y 位置 + // 如果超出主窗口范围,调整到边界 + if targetY < mainPos.Y { + targetY = mainPos.Y + } else if targetY+windowHeight > mainPos.Y+mainHeight { + targetY = mainPos.Y + mainHeight - windowHeight + } + + case models.SnapEdgeLeft: + // 吸附到主窗口左侧 + targetX = mainPos.X - windowWidth + targetY = windowPos.Y // 保持当前 Y 位置 + // 如果超出主窗口范围,调整到边界 + if targetY < mainPos.Y { + targetY = mainPos.Y + } else if targetY+windowHeight > mainPos.Y+mainHeight { + targetY = mainPos.Y + mainHeight - windowHeight + } + + case models.SnapEdgeBottom: + // 吸附到主窗口底部 + targetX = windowPos.X // 保持当前 X 位置 + targetY = mainPos.Y + mainHeight + // 如果超出主窗口范围,调整到边界 + if targetX < mainPos.X { + targetX = mainPos.X + } else if targetX+windowWidth > mainPos.X+mainWidth { + targetX = mainPos.X + mainWidth - windowWidth + } + + case models.SnapEdgeTop: + // 吸附到主窗口顶部 + targetX = windowPos.X // 保持当前 X 位置 + targetY = mainPos.Y - windowHeight + // 如果超出主窗口范围,调整到边界 + if targetX < mainPos.X { + targetX = mainPos.X + } else if targetX+windowWidth > mainPos.X+mainWidth { + targetX = mainPos.X + mainWidth - windowWidth + } + + // ==== 角落吸附 ==== + case models.SnapEdgeTopRight: + // 吸附到右上角 + targetX = mainPos.X + mainWidth + targetY = mainPos.Y - windowHeight + + case models.SnapEdgeBottomRight: + // 吸附到右下角 + targetX = mainPos.X + mainWidth + targetY = mainPos.Y + mainHeight + + case models.SnapEdgeBottomLeft: + // 吸附到左下角 + targetX = mainPos.X - windowWidth + targetY = mainPos.Y + mainHeight + + case models.SnapEdgeTopLeft: + // 吸附到左上角 + targetX = mainPos.X - windowWidth + targetY = mainPos.Y - windowHeight + + default: + // 不应该到达这里 + return + } + + // 直接移动到目标位置,不使用平滑过渡以产生明显的吸附效果 + windowInfo.Window.SetPosition(targetX, targetY) + + // 更新窗口信息 + windowInfo.SnapEdge = snapEdge + windowInfo.LastPos = models.WindowPosition{X: targetX, Y: targetY} +} + +// Cleanup 清理资源 +func (wss *WindowSnapService) Cleanup() { + // 如果有取消函数,调用它来停止所有goroutine + if wss.cancel != nil { + wss.cancel() + } + + // 停止定时器 + if wss.snapTicker != nil { + wss.snapTicker.Stop() + wss.snapTicker = nil + } +} + +// ServiceShutdown 实现服务关闭接口 +func (wss *WindowSnapService) ServiceShutdown() error { + wss.Cleanup() + return nil +}