11 Commits

Author SHA1 Message Date
1fb4f64cb3 Add update notifications 2025-09-06 01:21:02 +08:00
1f8e8981ce 🐛 Fixed version generation issues 2025-09-05 22:40:09 +08:00
a257d30dba 🎨 Modify configuration migration policy 2025-09-05 22:07:00 +08:00
97ee3b0667 🐛 Fixed configuration merge override issue 2025-09-05 00:36:33 +08:00
8e2bafba5f Optimize window snapping performance 2025-09-04 00:20:30 +08:00
6149bc133d 🐛 Fixed window pinning issue 2025-09-02 23:59:04 +08:00
5f22ee3b1f ♻️ Refactoring configuration migration service 2025-08-31 17:48:41 +08:00
fa72ff8061 Merge remote-tracking branch 'github/dependabot/go_modules/github.com/ulikunitz/xz-0.5.14' 2025-08-30 19:00:36 +08:00
65f24860e6 Added window snapping function toggle 2025-08-30 00:18:29 +08:00
dependabot[bot]
4881233211 ⬆️ Bump github.com/ulikunitz/xz from 0.5.12 to 0.5.14
Bumps [github.com/ulikunitz/xz](https://github.com/ulikunitz/xz) from 0.5.12 to 0.5.14.
- [Commits](https://github.com/ulikunitz/xz/compare/v0.5.12...v0.5.14)

---
updated-dependencies:
- dependency-name: github.com/ulikunitz/xz
  dependency-version: 0.5.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-28 19:44:41 +00:00
bc01fdf362 Added window snapping feature 2025-08-24 16:07:48 +08:00
63 changed files with 3696 additions and 709 deletions

View File

@@ -1,10 +1,10 @@
# <img src="./frontend/public/appicon.png" alt="VoidRaft Logo" width="32" height="32" style="vertical-align: middle;"> VoidRaft # <img src="./frontend/public/appicon.png" alt="voidraft Logo" width="32" height="32" style="vertical-align: middle;"> voidraft
[中文](README_ZH.md) | **English** [中文](README_ZH.md) | **English**
> *An elegant text snippet recording tool designed for developers.* > *An elegant text snippet recording tool designed for developers.*
VoidRaft is a modern developer-focused text editor that allows you to record, organize, and manage various text snippets anytime, anywhere. Whether it's temporary code snippets, API responses, meeting notes, or daily to-do lists, VoidRaft provides a smooth and elegant editing experience. voidraft is a modern developer-focused text editor that allows you to record, organize, and manage various text snippets anytime, anywhere. Whether it's temporary code snippets, API responses, meeting notes, or daily to-do lists, voidraft provides a smooth and elegant editing experience.
## Core Features ## Core Features
@@ -87,7 +87,7 @@ After building, the executable will be generated in the `bin` directory.
## Project Structure ## Project Structure
``` ```
Voidraft/ voidraft/
├── frontend/ # Vue 3 frontend application ├── frontend/ # Vue 3 frontend application
│ ├── src/ │ ├── src/
│ │ ├── views/editor/ # Editor core views │ │ ├── views/editor/ # Editor core views
@@ -129,11 +129,11 @@ Voidraft/
> Standing on the shoulders of giants, paying tribute to the open source spirit > Standing on the shoulders of giants, paying tribute to the open source spirit
The birth of VoidRaft is inseparable from the following excellent open source projects: The birth of voidraft is inseparable from the following excellent open source projects:
### Special Thanks ### Special Thanks
- **[Heynote](https://github.com/heyman/heynote/)** - VoidRaft is a feature-enhanced version based on Heynote's concept - **[Heynote](https://github.com/heyman/heynote/)** - voidraft is a feature-enhanced version based on Heynote's concept
- Inherits Heynote's elegant block editing philosophy - Inherits Heynote's elegant block editing philosophy
- Adds more practical features on the original foundation - Adds more practical features on the original foundation
- Rebuilt with modern technology stack - Rebuilt with modern technology stack
@@ -157,7 +157,7 @@ This project is open source under the [MIT License](LICENSE).
Welcome to Fork, Star, and contribute code. Welcome to Fork, Star, and contribute code.
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub stars](https://img.shields.io/github/stars/landaiqing/Voidraft.svg?style=social&label=Star)](https://github.com/yourusername/Voidraft) [![GitHub stars](https://img.shields.io/github/stars/landaiqing/voidraft.svg?style=social&label=Star)](https://github.com/yourusername/voidraft)
[![GitHub forks](https://img.shields.io/github/forks/landaiqing/Voidraft.svg?style=social&label=Fork)](https://github.com/yourusername/Voidraft) [![GitHub forks](https://img.shields.io/github/forks/landaiqing/voidraft.svg?style=social&label=Fork)](https://github.com/yourusername/voidraft)
*Made with ❤️ by landaiqing* *Made with ❤️ by landaiqing*

View File

@@ -1,10 +1,10 @@
# <img src="./frontend/public/appicon.png" alt="Voidraft Logo" width="32" height="32" style="vertical-align: middle;"> Voidraft # <img src="./frontend/public/appicon.png" alt="voidraft Logo" width="32" height="32" style="vertical-align: middle;"> voidraft
**中文** | [English](README.md) **中文** | [English](README.md)
> *一个专为开发者打造的优雅文本片段记录工具。* > *一个专为开发者打造的优雅文本片段记录工具。*
Voidraft 是一个现代化的开发者专用文本编辑器让你能够随时随地记录、整理和管理各种文本片段。无论是临时的代码片段、API 响应、会议笔记,还是日常的待办事项,Voidraft 都能为你提供流畅而优雅的编辑体验。 voidraft 是一个现代化的开发者专用文本编辑器让你能够随时随地记录、整理和管理各种文本片段。无论是临时的代码片段、API 响应、会议笔记,还是日常的待办事项,voidraft 都能为你提供流畅而优雅的编辑体验。
## 核心特性 ## 核心特性
@@ -88,7 +88,7 @@ wails3 package
## 项目结构 ## 项目结构
``` ```
Voidraft/ voidraft/
├── frontend/ # Vue 3 前端应用 ├── frontend/ # Vue 3 前端应用
│ ├── src/ │ ├── src/
│ │ ├── views/editor/ # 编辑器核心视图 │ │ ├── views/editor/ # 编辑器核心视图
@@ -131,11 +131,11 @@ Voidraft/
> 站在巨人的肩膀上,致敬开源精神 > 站在巨人的肩膀上,致敬开源精神
Voidraft 的诞生离不开以下优秀的开源项目: voidraft 的诞生离不开以下优秀的开源项目:
### 特别感谢 ### 特别感谢
- **[Heynote](https://github.com/heyman/heynote/)** - Voidraft 是基于 Heynote 概念的功能增强版本 - **[Heynote](https://github.com/heyman/heynote/)** - voidraft 是基于 Heynote 概念的功能增强版本
- 继承了 Heynote 优雅的块状编辑理念 - 继承了 Heynote 优雅的块状编辑理念
- 在原有基础上增加了更多实用功能 - 在原有基础上增加了更多实用功能
- 采用现代化技术栈重新构建 - 采用现代化技术栈重新构建
@@ -159,7 +159,7 @@ Voidraft 的诞生离不开以下优秀的开源项目:
欢迎 Fork、Star 和贡献代码。 欢迎 Fork、Star 和贡献代码。
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub stars](https://img.shields.io/github/stars/landaiqing/Voidraft.svg?style=social&label=Star)](https://github.com/yourusername/Voidraft) [![GitHub stars](https://img.shields.io/github/stars/landaiqing/voidraft.svg?style=social&label=Star)](https://github.com/yourusername/voidraft)
[![GitHub forks](https://img.shields.io/github/forks/landaiqing/Voidraft.svg?style=social&label=Fork)](https://github.com/yourusername/Voidraft) [![GitHub forks](https://img.shields.io/github/forks/landaiqing/voidraft.svg?style=social&label=Fork)](https://github.com/yourusername/voidraft)
*Made with ❤️ by landaiqing* *Made with ❤️ by landaiqing*

View File

@@ -12,13 +12,25 @@ vars:
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}' VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
tasks: tasks:
version:
summary: Generate version information
cmds:
- '{{if eq OS "windows"}}cmd /c ".\scripts\version.bat"{{else}}bash ./scripts/version.sh{{end}}'
sources:
- scripts/version.bat
- scripts/version.sh
generates:
- version.txt
build: build:
summary: Builds the application summary: Builds the application
deps: [version]
cmds: cmds:
- task: "{{OS}}:build" - task: "{{OS}}:build"
package: package:
summary: Packages a production build of the application summary: Packages a production build of the application
deps: [version]
cmds: cmds:
- task: "{{OS}}:package" - task: "{{OS}}:package"

View File

@@ -5,12 +5,12 @@ version: '3'
# This information is used to generate the build assets. # This information is used to generate the build assets.
info: info:
companyName: "Voidraft" # The name of the company companyName: "voidraft" # The name of the company
productName: "Voidraft" # The name of the application productName: "voidraft" # The name of the application
productIdentifier: "landaiqing" # The unique product identifier productIdentifier: "landaiqing" # The unique product identifier
description: "Voidraft" # The application description description: "voidraft" # The application description
copyright: "© 2025 Voidraft. All rights reserved." # Copyright text copyright: "© 2025 voidraft. All rights reserved." # Copyright text
comments: "Voidraft" # Comments comments: "voidraft" # Comments
version: "0.0.1.0" # The application version version: "0.0.1.0" # The application version
# Dev mode configuration # Dev mode configuration

View File

@@ -4,15 +4,15 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Voidraft</string> <string>voidraft</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>Voidraft</string> <string>voidraft</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>landaiqing</string> <string>landaiqing</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>0.0.1.0</string> <string>0.0.1.0</string>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>Voidraft</string> <string>voidraft</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.0.1.0</string> <string>0.0.1.0</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
@@ -22,7 +22,7 @@
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<string>true</string> <string>true</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>© 2025 Voidraft. All rights reserved.</string> <string>© 2025 voidraft. All rights reserved.</string>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsLocalNetworking</key> <key>NSAllowsLocalNetworking</key>

View File

@@ -4,15 +4,15 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Voidraft</string> <string>voidraft</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>Voidraft</string> <string>voidraft</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>landaiqing</string> <string>landaiqing</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>0.0.1.0</string> <string>0.0.1.0</string>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>Voidraft</string> <string>voidraft</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.0.1.0</string> <string>0.0.1.0</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
@@ -22,6 +22,6 @@
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<string>true</string> <string>true</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>© 2025 Voidraft. All rights reserved.</string> <string>© 2025 voidraft. All rights reserved.</string>
</dict> </dict>
</plist> </plist>

View File

@@ -11,9 +11,12 @@ tasks:
- task: common:build:frontend - task: common:build:frontend
- task: common:generate:icons - task: common:generate:icons
cmds: cmds:
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} - go build {{.BUILD_FLAGS}} -ldflags="{{.LDFLAGS}} {{.VERSION_FLAGS}}" -o {{.OUTPUT}}
vars: vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
LDFLAGS: '{{if eq .PRODUCTION "true"}}-w -s{{else}}{{end}}'
VERSION_FLAGS:
sh: 'grep "VERSION=" version.txt | cut -d"=" -f2 | xargs -I {} echo "-X voidraft/internal/version.Version={}"'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
env: env:

View File

@@ -11,9 +11,12 @@ tasks:
- task: common:build:frontend - task: common:build:frontend
- task: common:generate:icons - task: common:generate:icons
cmds: cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}} - go build {{.BUILD_FLAGS}} -ldflags="{{.LDFLAGS}} {{.VERSION_FLAGS}}" -o {{.BIN_DIR}}/{{.APP_NAME}}
vars: vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
LDFLAGS: '{{if eq .PRODUCTION "true"}}-w -s{{else}}{{end}}'
VERSION_FLAGS:
sh: 'grep "VERSION=" version.txt | cut -d"=" -f2 | xargs -I {} echo "-X voidraft/internal/version.Version={}"'
env: env:
GOOS: linux GOOS: linux
CGO_ENABLED: 1 CGO_ENABLED: 1

View File

@@ -3,26 +3,26 @@
# #
# The lines below are called `modelines`. See `:help modeline` # The lines below are called `modelines`. See `:help modeline`
name: "Voidraft" name: "voidraft"
arch: ${GOARCH} arch: ${GOARCH}
platform: "linux" platform: "linux"
version: "0.0.1.0" version: "0.0.1.0"
section: "default" section: "default"
priority: "extra" priority: "extra"
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
description: "Voidraft" description: "voidraft"
vendor: "Voidraft" vendor: "voidraft"
homepage: "https://wails.io" homepage: "https://voidraft.landaiqing.cn"
license: "MIT" license: "MIT"
release: "1" release: "1"
contents: contents:
- src: "./bin/Voidraft" - src: "./bin/voidraft"
dst: "/usr/local/bin/Voidraft" dst: "/usr/local/bin/voidraft"
- src: "./build/appicon.png" - src: "./build/appicon.png"
dst: "/usr/share/icons/hicolor/128x128/apps/Voidraft.png" dst: "/usr/share/icons/hicolor/128x128/apps/voidraft.png"
- src: "./build/linux/Voidraft.desktop" - src: "./build/linux/voidraft.desktop"
dst: "/usr/share/applications/Voidraft.desktop" dst: "/usr/share/applications/voidraft.desktop"
depends: depends:
- gtk3 - gtk3

View File

@@ -14,13 +14,16 @@ tasks:
- task: common:generate:icons - task: common:generate:icons
cmds: cmds:
- task: generate:syso - task: generate:syso
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe - go build {{.BUILD_FLAGS}} -ldflags="{{.LDFLAGS}} {{.VERSION_FLAGS}}" -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
- cmd: powershell Remove-item *.syso - cmd: powershell Remove-item *.syso
platforms: [windows] platforms: [windows]
- cmd: rm -f *.syso - cmd: rm -f *.syso
platforms: [linux, darwin] platforms: [linux, darwin]
vars: vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
LDFLAGS: '{{if eq .PRODUCTION "true"}}-w -s -H windowsgui{{else}}{{end}}'
VERSION_FLAGS:
sh: 'powershell -Command "(Get-Content version.txt) -replace ''VERSION='', ''-X voidraft/internal/version.Version=''"'
env: env:
GOOS: windows GOOS: windows
CGO_ENABLED: 1 CGO_ENABLED: 1

View File

@@ -5,11 +5,11 @@
"info": { "info": {
"0000": { "0000": {
"ProductVersion": "0.0.1.0", "ProductVersion": "0.0.1.0",
"CompanyName": "Voidraft", "CompanyName": "voidraft",
"FileDescription": "Voidraft", "FileDescription": "voidraft",
"LegalCopyright": "© 2025 Voidraft. All rights reserved.", "LegalCopyright": "© 2025 voidraft. All rights reserved.",
"ProductName": "Voidraft", "ProductName": "voidraft",
"Comments": "Voidraft" "Comments": "voidraft"
} }
} }
} }

View File

@@ -5,19 +5,19 @@
!include "FileFunc.nsh" !include "FileFunc.nsh"
!ifndef INFO_PROJECTNAME !ifndef INFO_PROJECTNAME
!define INFO_PROJECTNAME "Voidraft" !define INFO_PROJECTNAME "voidraft"
!endif !endif
!ifndef INFO_COMPANYNAME !ifndef INFO_COMPANYNAME
!define INFO_COMPANYNAME "Voidraft" !define INFO_COMPANYNAME "voidraft"
!endif !endif
!ifndef INFO_PRODUCTNAME !ifndef INFO_PRODUCTNAME
!define INFO_PRODUCTNAME "Voidraft" !define INFO_PRODUCTNAME "voidraft"
!endif !endif
!ifndef INFO_PRODUCTVERSION !ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "0.0.1.0" !define INFO_PRODUCTVERSION "0.0.1.0"
!endif !endif
!ifndef INFO_COPYRIGHT !ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "© 2025 Voidraft. All rights reserved." !define INFO_COPYRIGHT "© 2025 voidraft. All rights reserved."
!endif !endif
!ifndef PRODUCT_EXECUTABLE !ifndef PRODUCT_EXECUTABLE
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VoidRaft - Changelog</title> <title>voidraft - Changelog</title>
<link rel="stylesheet" href="css/styles.css"> <link rel="stylesheet" href="css/styles.css">
<link rel="stylesheet" href="css/changelog.css"> <link rel="stylesheet" href="css/changelog.css">
<link rel="icon" href="img/favicon.ico"> <link rel="icon" href="img/favicon.ico">
@@ -16,7 +16,7 @@
<!-- 主卡片 --> <!-- 主卡片 -->
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h1 class="card-title" data-en="VoidRaft Changelog" data-zh="VoidRaft 更新日志">VoidRaft Changelog</h1> <h1 class="card-title" data-en="voidraft Changelog" data-zh="voidraft 更新日志">voidraft Changelog</h1>
<div class="card-controls"> <div class="card-controls">
<button id="theme-toggle" class="btn btn-secondary" title="切换主题"> <button id="theme-toggle" class="btn btn-secondary" title="切换主题">
<i class="fas fa-sun"></i> <span data-en="Theme" data-zh="主题">Theme</span> <i class="fas fa-sun"></i> <span data-en="Theme" data-zh="主题">Theme</span>
@@ -59,7 +59,7 @@
<!-- 页脚 --> <!-- 页脚 -->
<footer class="footer"> <footer class="footer">
<p class="footer-text" data-en="© 2025 VoidRaft - An elegant text snippet recording tool designed for developers" data-zh="© 2025 VoidRaft - 专为开发者打造的优雅文本片段记录工具">© 2023-2024 VoidRaft - An elegant text snippet recording tool designed for developers</p> <p class="footer-text" data-en="© 2025 voidraft - An elegant text snippet recording tool designed for developers" data-zh="© 2025 voidraft - 专为开发者打造的优雅文本片段记录工具">© 2023-2024 voidraft - An elegant text snippet recording tool designed for developers</p>
<div class="footer-links"> <div class="footer-links">
<a href="https://github.com/landaiqing/voidraft" target="_blank" class="footer-link">GitHub</a> <a href="https://github.com/landaiqing/voidraft" target="_blank" class="footer-link">GitHub</a>
<a href="https://github.com/landaiqing/voidraft/issues" target="_blank" class="footer-link" data-en="Issues" data-zh="问题反馈">Issues</a> <a href="https://github.com/landaiqing/voidraft/issues" target="_blank" class="footer-link" data-en="Issues" data-zh="问题反馈">Issues</a>

View File

@@ -3,10 +3,10 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VoidRaft - An elegant text snippet recording tool designed for developers.</title> <title>voidraft - An elegant text snippet recording tool designed for developers.</title>
<meta name="description" content="VoidRaft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more."> <meta name="description" content="voidraft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
<meta name="keywords" content="text editor, code snippets, developer tools, syntax highlighting, code formatting, multi-language, VoidRaft"> <meta name="keywords" content="text editor, code snippets, developer tools, syntax highlighting, code formatting, multi-language, voidraft">
<meta name="author" content="VoidRaft Team"> <meta name="author" content="voidraft Team">
<meta name="robots" content="index, follow"> <meta name="robots" content="index, follow">
<link rel="canonical" href="https://landaiqing.github.io/voidraft/"> <link rel="canonical" href="https://landaiqing.github.io/voidraft/">
@@ -18,16 +18,16 @@
<!-- Open Graph / Facebook --> <!-- Open Graph / Facebook -->
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:url" content="https://landaiqing.github.io/voidraft/"> <meta property="og:url" content="https://landaiqing.github.io/voidraft/">
<meta property="og:title" content="VoidRaft - An elegant text snippet recording tool designed for developers"> <meta property="og:title" content="voidraft - An elegant text snippet recording tool designed for developers">
<meta property="og:description" content="VoidRaft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more."> <meta property="og:description" content="voidraft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
<meta property="og:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png"> <meta property="og:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
<meta property="og:site_name" content="VoidRaft"> <meta property="og:site_name" content="voidraft">
<!-- Twitter --> <!-- Twitter -->
<meta property="twitter:card" content="summary_large_image"> <meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://landaiqing.github.io/voidraft/"> <meta property="twitter:url" content="https://landaiqing.github.io/voidraft/">
<meta property="twitter:title" content="VoidRaft - An elegant text snippet recording tool designed for developers"> <meta property="twitter:title" content="voidraft - An elegant text snippet recording tool designed for developers">
<meta property="twitter:description" content="VoidRaft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more."> <meta property="twitter:description" content="voidraft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
<meta property="twitter:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png"> <meta property="twitter:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
<link rel="stylesheet" href="./css/styles.css"> <link rel="stylesheet" href="./css/styles.css">
@@ -39,13 +39,13 @@
{ {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "SoftwareApplication", "@type": "SoftwareApplication",
"name": "VoidRaft", "name": "voidraft",
"description": "An elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.", "description": "An elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.",
"url": "https://landaiqing.github.io/voidraft/", "url": "https://landaiqing.github.io/voidraft/",
"downloadUrl": "https://github.com/landaiqing/voidraft/releases", "downloadUrl": "https://github.com/landaiqing/voidraft/releases",
"author": { "author": {
"@type": "Organization", "@type": "Organization",
"name": "VoidRaft" "name": "voidraft"
}, },
"operatingSystem": ["Windows", "macOS", "Linux"], "operatingSystem": ["Windows", "macOS", "Linux"],
"applicationCategory": "DeveloperApplication", "applicationCategory": "DeveloperApplication",
@@ -66,7 +66,7 @@
<!-- 主卡片 --> <!-- 主卡片 -->
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h1 class="card-title">VoidRaft</h1> <h1 class="card-title">voidraft</h1>
<div class="card-controls"> <div class="card-controls">
<button id="theme-toggle" class="btn btn-secondary" title="切换主题"> <button id="theme-toggle" class="btn btn-secondary" title="切换主题">
<i class="fas fa-sun"></i> <span data-en="Theme" data-zh="主题">Theme</span> <i class="fas fa-sun"></i> <span data-en="Theme" data-zh="主题">Theme</span>
@@ -81,9 +81,9 @@
<!-- Logo和介绍 --> <!-- Logo和介绍 -->
<div class="logo-container"> <div class="logo-container">
<div class="logo-frame"> <div class="logo-frame">
<img src="img/logo.png" alt="VoidRaft Logo" class="logo-image"> <img src="img/logo.png" alt="voidraft Logo" class="logo-image">
</div> </div>
<h2 class="logo-text" data-en="VoidRaft" data-zh="VoidRaft">VoidRaft</h2> <h2 class="logo-text" data-en="voidraft" data-zh="voidraft">voidraft</h2>
<p class="tagline" data-en="An elegant text snippet recording tool" data-zh="优雅的文本片段记录工具">An elegant text snippet recording tool</p> <p class="tagline" data-en="An elegant text snippet recording tool" data-zh="优雅的文本片段记录工具">An elegant text snippet recording tool</p>
</div> </div>
@@ -195,15 +195,15 @@
<div class="block-language">text</div> <div class="block-language">text</div>
</div> </div>
<pre class="code-block"> <pre class="code-block">
<span class="comment">// VoidRaft - An elegant text snippet recording tool</span> <span class="comment">// voidraft - An elegant text snippet recording tool</span>
<span class="comment">// Multi-language support | Code formatting | Custom themes</span> <span class="comment">// Multi-language support | Code formatting | Custom themes</span>
<span class="comment">// A modern text editor designed for developers</span></pre> <span class="comment">// A modern text editor designed for developers</span></pre>
</div> </div>
</div> </div>
</div> </div>
<div class="preview-window"> <div class="preview-window">
<img src="img/screenshot-dark.png" alt="VoidRaft 界面预览" class="preview-image dark-theme-img"> <img src="img/screenshot-dark.png" alt="voidraft 界面预览" class="preview-image dark-theme-img">
<img src="img/screenshot-light.png" alt="VoidRaft 界面预览" class="preview-image light-theme-img" style="display: none;"> <img src="img/screenshot-light.png" alt="voidraft 界面预览" class="preview-image light-theme-img" style="display: none;">
</div> </div>
</div> </div>
@@ -241,7 +241,7 @@
<!-- 页脚 --> <!-- 页脚 -->
<footer class="footer"> <footer class="footer">
<p class="footer-text" data-en="© 2025 VoidRaft - An elegant text snippet recording tool designed for developers" data-zh="© 2025 VoidRaft - 专为开发者打造的优雅文本片段记录工具">© 2025 VoidRaft - An elegant text snippet recording tool designed for developers</p> <p class="footer-text" data-en="© 2025 voidraft - An elegant text snippet recording tool designed for developers" data-zh="© 2025 voidraft - 专为开发者打造的优雅文本片段记录工具">© 2025 voidraft - An elegant text snippet recording tool designed for developers</p>
<div class="footer-links"> <div class="footer-links">
<a href="https://github.com/landaiqing/voidraft" target="_blank" class="footer-link">GitHub</a> <a href="https://github.com/landaiqing/voidraft" target="_blank" class="footer-link">GitHub</a>
<a href="https://github.com/landaiqing/voidraft/issues" target="_blank" class="footer-link" data-en="Issues" data-zh="问题反馈">Issues</a> <a href="https://github.com/landaiqing/voidraft/issues" target="_blank" class="footer-link" data-en="Issues" data-zh="问题反馈">Issues</a>

View File

@@ -1,5 +1,5 @@
/** /**
* VoidRaft - Changelog Script * voidraft - Changelog Script
* 从GitHub API获取发布信息支持Gitea备用源 * 从GitHub API获取发布信息支持Gitea备用源
*/ */

View File

@@ -1,5 +1,5 @@
/** /**
* VoidRaft - Website Script * voidraft - Website Script
*/ */
/** /**
@@ -233,14 +233,14 @@ class SEOManager {
this.languageManager = languageManager; this.languageManager = languageManager;
this.metaTexts = { this.metaTexts = {
en: { en: {
description: 'VoidRaft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.', description: 'voidraft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.',
title: 'VoidRaft - An elegant text snippet recording tool designed for developers.', title: 'voidraft - An elegant text snippet recording tool designed for developers.',
ogTitle: 'VoidRaft - An elegant text snippet recording tool designed for developers' ogTitle: 'voidraft - An elegant text snippet recording tool designed for developers'
}, },
zh: { zh: {
description: 'VoidRaft 是专为开发者打造的优雅文本片段记录工具。支持多语言代码块、语法高亮、代码格式化、自定义主题等功能。', description: 'voidraft 是专为开发者打造的优雅文本片段记录工具。支持多语言代码块、语法高亮、代码格式化、自定义主题等功能。',
title: 'VoidRaft - 专为开发者打造的优雅文本片段记录工具', title: 'voidraft - 专为开发者打造的优雅文本片段记录工具',
ogTitle: 'VoidRaft - 专为开发者打造的优雅文本片段记录工具' ogTitle: 'voidraft - 专为开发者打造的优雅文本片段记录工具'
} }
}; };
this.init(); this.init();
@@ -371,9 +371,9 @@ class UIEffects {
} }
/** /**
* VoidRaft主应用类 * voidraft主应用类
*/ */
class VoidRaftApp { class voidraftApp {
constructor() { constructor() {
this.themeManager = null; this.themeManager = null;
this.languageManager = null; this.languageManager = null;
@@ -404,7 +404,7 @@ class VoidRaftApp {
* 显示控制台品牌信息 * 显示控制台品牌信息
*/ */
showConsoleBranding() { showConsoleBranding() {
console.log('%c VoidRaft', 'color: #ff006e; font-size: 20px; font-family: "Space Mono", monospace;'); console.log('%c voidraft', 'color: #ff006e; font-size: 20px; font-family: "Space Mono", monospace;');
console.log('%c An elegant text snippet recording tool designed for developers.', 'color: #073B4C; font-family: "Space Mono", monospace;'); console.log('%c An elegant text snippet recording tool designed for developers.', 'color: #073B4C; font-family: "Space Mono", monospace;');
} }
@@ -439,5 +439,5 @@ class VoidRaftApp {
// 当DOM加载完成时初始化应用 // 当DOM加载完成时初始化应用
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
window.voidRaftApp = new VoidRaftApp(); window.voidRaftApp = new voidraftApp();
}); });

View File

@@ -0,0 +1,64 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* Service represents the notifications service
* @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 "../../application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as $models from "./models.js";
/**
* RemoveBadge removes the badge label from the application icon.
*/
export function RemoveBadge(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2374916939) as any;
return $resultPromise;
}
/**
* ServiceName returns the name of the service.
*/
export function ServiceName(): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(2428202016) as any;
return $resultPromise;
}
/**
* ServiceShutdown is called when the service is unloaded.
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3893755233) as any;
return $resultPromise;
}
/**
* ServiceStartup is called when the service is loaded.
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(4078800764, options) as any;
return $resultPromise;
}
/**
* SetBadge sets the badge label on the application icon.
*/
export function SetBadge(label: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(784276339, label) as any;
return $resultPromise;
}
export function SetCustomBadge(label: string, options: $models.Options): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3058653106, label, options) as any;
return $resultPromise;
}

View File

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

View File

@@ -0,0 +1,58 @@
// 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";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as color$0 from "../../../../../../../image/color/models.js";
export class Options {
"TextColour": color$0.RGBA;
"BackgroundColour": color$0.RGBA;
"FontName": string;
"FontSize": number;
"SmallFontSize": number;
/** Creates a new Options instance. */
constructor($$source: Partial<Options> = {}) {
if (!("TextColour" in $$source)) {
this["TextColour"] = (new color$0.RGBA());
}
if (!("BackgroundColour" in $$source)) {
this["BackgroundColour"] = (new color$0.RGBA());
}
if (!("FontName" in $$source)) {
this["FontName"] = "";
}
if (!("FontSize" in $$source)) {
this["FontSize"] = 0;
}
if (!("SmallFontSize" in $$source)) {
this["SmallFontSize"] = 0;
}
Object.assign(this, $$source);
}
/**
* Creates a new Options instance from a string or object.
*/
static createFrom($$source: any = {}): Options {
const $$createField0_0 = $$createType0;
const $$createField1_0 = $$createType0;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("TextColour" in $$parsedSource) {
$$parsedSource["TextColour"] = $$createField0_0($$parsedSource["TextColour"]);
}
if ("BackgroundColour" in $$parsedSource) {
$$parsedSource["BackgroundColour"] = $$createField1_0($$parsedSource["BackgroundColour"]);
}
return new Options($$parsedSource as Partial<Options>);
}
}
// Private type creation functions
const $$createType0 = color$0.RGBA.createFrom;

View File

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

View File

@@ -0,0 +1,107 @@
// 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";
/**
* NotificationAction represents an action button for a notification.
*/
export class NotificationAction {
"id"?: string;
"title"?: string;
/**
* (macOS-specific)
*/
"destructive"?: boolean;
/** Creates a new NotificationAction instance. */
constructor($$source: Partial<NotificationAction> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new NotificationAction instance from a string or object.
*/
static createFrom($$source: any = {}): NotificationAction {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new NotificationAction($$parsedSource as Partial<NotificationAction>);
}
}
/**
* NotificationCategory groups actions for notifications.
*/
export class NotificationCategory {
"id"?: string;
"actions"?: NotificationAction[];
"hasReplyField"?: boolean;
"replyPlaceholder"?: string;
"replyButtonTitle"?: string;
/** Creates a new NotificationCategory instance. */
constructor($$source: Partial<NotificationCategory> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new NotificationCategory instance from a string or object.
*/
static createFrom($$source: any = {}): NotificationCategory {
const $$createField1_0 = $$createType1;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("actions" in $$parsedSource) {
$$parsedSource["actions"] = $$createField1_0($$parsedSource["actions"]);
}
return new NotificationCategory($$parsedSource as Partial<NotificationCategory>);
}
}
/**
* NotificationOptions contains configuration for a notification
*/
export class NotificationOptions {
"id": string;
"title": string;
/**
* (macOS and Linux only)
*/
"subtitle"?: string;
"body"?: string;
"categoryId"?: string;
"data"?: { [_: string]: any };
/** Creates a new NotificationOptions instance. */
constructor($$source: Partial<NotificationOptions> = {}) {
if (!("id" in $$source)) {
this["id"] = "";
}
if (!("title" in $$source)) {
this["title"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new NotificationOptions instance from a string or object.
*/
static createFrom($$source: any = {}): NotificationOptions {
const $$createField5_0 = $$createType2;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("data" in $$parsedSource) {
$$parsedSource["data"] = $$createField5_0($$parsedSource["data"]);
}
return new NotificationOptions($$parsedSource as Partial<NotificationOptions>);
}
}
// Private type creation functions
const $$createType0 = NotificationAction.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType2 = $Create.Map($Create.Any, $Create.Any);

View File

@@ -0,0 +1,110 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* Service represents the notifications service
* @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 "../../application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as $models from "./models.js";
export function CheckNotificationAuthorization(): Promise<boolean> & { cancel(): void } {
let $resultPromise = $Call.ByID(2216952893) as any;
return $resultPromise;
}
/**
* OnNotificationResponse registers a callback function that will be called when
* a notification response is received from the user.
*/
export function OnNotificationResponse(callback: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1642697808, callback) as any;
return $resultPromise;
}
export function RegisterNotificationCategory(category: $models.NotificationCategory): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2917562919, category) as any;
return $resultPromise;
}
export function RemoveAllDeliveredNotifications(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3956282340) as any;
return $resultPromise;
}
export function RemoveAllPendingNotifications(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(108821341) as any;
return $resultPromise;
}
export function RemoveDeliveredNotification(identifier: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(975691940, identifier) as any;
return $resultPromise;
}
export function RemoveNotification(identifier: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3966653866, identifier) as any;
return $resultPromise;
}
export function RemoveNotificationCategory(categoryID: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2032615554, categoryID) as any;
return $resultPromise;
}
export function RemovePendingNotification(identifier: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3729049703, identifier) as any;
return $resultPromise;
}
/**
* Public methods that delegate to the implementation.
*/
export function RequestNotificationAuthorization(): Promise<boolean> & { cancel(): void } {
let $resultPromise = $Call.ByID(3933442950) as any;
return $resultPromise;
}
export function SendNotification(options: $models.NotificationOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3968228732, options) as any;
return $resultPromise;
}
export function SendNotificationWithActions(options: $models.NotificationOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1886542847, options) as any;
return $resultPromise;
}
/**
* ServiceName returns the name of the service.
*/
export function ServiceName(): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(2704532675) as any;
return $resultPromise;
}
/**
* ServiceShutdown is called when the service is unloaded.
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2550195434) as any;
return $resultPromise;
}
/**
* ServiceStartup is called when the service is loaded.
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(4047820929, options) as any;
return $resultPromise;
}

View File

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

View File

@@ -0,0 +1,46 @@
// 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";
/**
* RGBA represents a traditional 32-bit alpha-premultiplied color, having 8
* bits for each of red, green, blue and alpha.
*
* An alpha-premultiplied color component C has been scaled by alpha (A), so
* has valid values 0 <= C <= A.
*/
export class RGBA {
"R": number;
"G": number;
"B": number;
"A": number;
/** Creates a new RGBA instance. */
constructor($$source: Partial<RGBA> = {}) {
if (!("R" in $$source)) {
this["R"] = 0;
}
if (!("G" in $$source)) {
this["G"] = 0;
}
if (!("B" in $$source)) {
this["B"] = 0;
}
if (!("A" in $$source)) {
this["A"] = 0;
}
Object.assign(this, $$source);
}
/**
* Creates a new RGBA instance from a string or object.
*/
static createFrom($$source: any = {}): RGBA {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new RGBA($$parsedSource as Partial<RGBA>);
}
}

View File

@@ -467,6 +467,12 @@ export class GeneralConfig {
*/ */
"startAtLogin": boolean; "startAtLogin": boolean;
/**
* 窗口吸附设置
* 是否启用窗口吸附功能(阈值现在是自适应的)
*/
"enableWindowSnap": boolean;
/** /**
* 全局热键设置 * 全局热键设置
* 是否启用全局热键 * 是否启用全局热键
@@ -492,6 +498,9 @@ export class GeneralConfig {
if (!("startAtLogin" in $$source)) { if (!("startAtLogin" in $$source)) {
this["startAtLogin"] = false; this["startAtLogin"] = false;
} }
if (!("enableWindowSnap" in $$source)) {
this["enableWindowSnap"] = false;
}
if (!("enableGlobalHotkey" in $$source)) { if (!("enableGlobalHotkey" in $$source)) {
this["enableGlobalHotkey"] = false; this["enableGlobalHotkey"] = false;
} }
@@ -506,10 +515,10 @@ export class GeneralConfig {
* Creates a new GeneralConfig instance from a string or object. * Creates a new GeneralConfig instance from a string or object.
*/ */
static createFrom($$source: any = {}): GeneralConfig { static createFrom($$source: any = {}): GeneralConfig {
const $$createField5_0 = $$createType8; const $$createField6_0 = $$createType8;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("globalHotkey" in $$parsedSource) { if ("globalHotkey" in $$parsedSource) {
$$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]); $$parsedSource["globalHotkey"] = $$createField6_0($$parsedSource["globalHotkey"]);
} }
return new GeneralConfig($$parsedSource as Partial<GeneralConfig>); return new GeneralConfig($$parsedSource as Partial<GeneralConfig>);
} }

View File

@@ -34,6 +34,30 @@ export function GetConfig(): Promise<models$0.AppConfig | null> & { cancel(): vo
return $typingPromise; return $typingPromise;
} }
/**
* GetConfigDir 获取配置目录
*/
export function GetConfigDir(): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(2275626561) as any;
return $resultPromise;
}
/**
* GetSettingsPath 获取设置文件路径
*/
export function GetSettingsPath(): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(2175583370) as any;
return $resultPromise;
}
/**
* MigrateConfig 执行配置迁移
*/
export function MigrateConfig(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(434292783) as any;
return $resultPromise;
}
/** /**
* ResetConfig 强制重置所有配置为默认值 * ResetConfig 强制重置所有配置为默认值
*/ */
@@ -82,6 +106,14 @@ export function SetHotkeyChangeCallback(callback: any): Promise<void> & { cancel
return $resultPromise; return $resultPromise;
} }
/**
* SetWindowSnapConfigChangeCallback 设置窗口吸附配置变更回调
*/
export function SetWindowSnapConfigChangeCallback(callback: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2324961653, callback) as any;
return $resultPromise;
}
// Private type creation functions // Private type creation functions
const $$createType0 = models$0.AppConfig.createFrom; const $$createType0 = models$0.AppConfig.createFrom;
const $$createType1 = $Create.Nullable($$createType0); const $$createType1 = $Create.Nullable($$createType0);

View File

@@ -13,10 +13,12 @@ import * as MigrationService from "./migrationservice.js";
import * as SelfUpdateService from "./selfupdateservice.js"; import * as SelfUpdateService from "./selfupdateservice.js";
import * as StartupService from "./startupservice.js"; import * as StartupService from "./startupservice.js";
import * as SystemService from "./systemservice.js"; import * as SystemService from "./systemservice.js";
import * as TestService from "./testservice.js";
import * as ThemeService from "./themeservice.js"; import * as ThemeService from "./themeservice.js";
import * as TranslationService from "./translationservice.js"; import * as TranslationService from "./translationservice.js";
import * as TrayService from "./trayservice.js"; import * as TrayService from "./trayservice.js";
import * as WindowService from "./windowservice.js"; import * as WindowService from "./windowservice.js";
import * as WindowSnapService from "./windowsnapservice.js";
export { export {
BackupService, BackupService,
ConfigService, ConfigService,
@@ -30,10 +32,12 @@ export {
SelfUpdateService, SelfUpdateService,
StartupService, StartupService,
SystemService, SystemService,
TestService,
ThemeService, ThemeService,
TranslationService, TranslationService,
TrayService, TrayService,
WindowService WindowService,
WindowSnapService
}; };
export * from "./models.js"; export * from "./models.js";

View File

@@ -203,7 +203,7 @@ export class SelfUpdateResult {
} }
/** /**
* WindowInfo 窗口信息 * WindowInfo 窗口信息(简化版)
*/ */
export class WindowInfo { export class WindowInfo {
"Window": application$0.WebviewWindow | null; "Window": application$0.WebviewWindow | null;
@@ -238,6 +238,26 @@ export class WindowInfo {
} }
} }
/**
* WindowSnapService 窗口吸附服务
*/
export class WindowSnapService {
/** Creates a new WindowSnapService instance. */
constructor($$source: Partial<WindowSnapService> = {}) {
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<WindowSnapService>);
}
}
// Private type creation functions // Private type creation functions
const $$createType0 = application$0.WebviewWindow.createFrom; const $$createType0 = application$0.WebviewWindow.createFrom;
const $$createType1 = $Create.Nullable($$createType0); const $$createType1 = $Create.Nullable($$createType0);

View File

@@ -0,0 +1,55 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* TestService 测试服务 - 仅在开发环境使用
* @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";
/**
* ClearAll 清除所有测试状态
*/
export function ClearAll(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2179720854) as any;
return $resultPromise;
}
/**
* ServiceStartup 服务启动时调用
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(617408198, options) as any;
return $resultPromise;
}
/**
* TestBadge 测试Badge功能
*/
export function TestBadge(text: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(4242952145, text) as any;
return $resultPromise;
}
/**
* TestNotification 测试通知功能
*/
export function TestNotification(title: string, subtitle: string, body: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1697553289, title, subtitle, body) as any;
return $resultPromise;
}
/**
* TestUpdateNotification 测试更新通知
*/
export function TestUpdateNotification(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3091730060) as any;
return $resultPromise;
}

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
/** /**
* WindowService 窗口管理服务 * WindowService 窗口管理服务(专注于窗口生命周期管理)
* @module * @module
*/ */
@@ -46,6 +46,14 @@ export function OpenDocumentWindow(documentID: number): Promise<void> & { cancel
return $resultPromise; return $resultPromise;
} }
/**
* ServiceShutdown 实现服务关闭接口
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(202192783) as any;
return $resultPromise;
}
/** /**
* SetAppReferences 设置应用和主窗口引用 * SetAppReferences 设置应用和主窗口引用
*/ */
@@ -54,6 +62,14 @@ export function SetAppReferences(app: application$0.App | null, mainWindow: appl
return $resultPromise; return $resultPromise;
} }
/**
* SetWindowSnapService 设置窗口吸附服务引用
*/
export function SetWindowSnapService(snapService: $models.WindowSnapService | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1105193745, snapService) as any;
return $resultPromise;
}
// Private type creation functions // Private type creation functions
const $$createType0 = $models.WindowInfo.createFrom; const $$createType0 = $models.WindowInfo.createFrom;
const $$createType1 = $Create.Array($$createType0); const $$createType1 = $Create.Array($$createType0);

View File

@@ -0,0 +1,79 @@
// 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<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2155505498) as any;
return $resultPromise;
}
/**
* GetCurrentThreshold 获取当前自适应阈值(用于调试或显示)
*/
export function GetCurrentThreshold(): Promise<number> & { cancel(): void } {
let $resultPromise = $Call.ByID(3176419026) as any;
return $resultPromise;
}
/**
* OnWindowSnapConfigChanged 处理窗口吸附配置变更
*/
export function OnWindowSnapConfigChanged(enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3794787039, enabled) as any;
return $resultPromise;
}
/**
* RegisterWindow 注册需要吸附管理的窗口
*/
export function RegisterWindow(documentID: number, window: application$0.WebviewWindow | null, title: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1000222723, documentID, window, title) as any;
return $resultPromise;
}
/**
* ServiceShutdown 实现服务关闭接口
*/
export function ServiceShutdown(): Promise<void> & { 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<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1782093351, app, mainWindow) as any;
return $resultPromise;
}
/**
* SetSnapEnabled 设置是否启用窗口吸附
*/
export function SetSnapEnabled(enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2280126835, enabled) as any;
return $resultPromise;
}
/**
* UnregisterWindow 取消注册窗口
*/
export function UnregisterWindow(documentID: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2844230768, documentID) as any;
return $resultPromise;
}

View File

@@ -52,27 +52,27 @@
"prettier": "^3.6.2", "prettier": "^3.6.2",
"remarkable": "^2.0.1", "remarkable": "^2.0.1",
"sass": "^1.90.0", "sass": "^1.90.0",
"vue": "^3.5.18", "vue": "^3.5.19",
"vue-i18n": "^11.1.11", "vue-i18n": "^11.1.11",
"vue-pick-colors": "^1.8.0", "vue-pick-colors": "^1.8.0",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.33.0", "@eslint/js": "^9.34.0",
"@lezer/generator": "^1.8.0", "@lezer/generator": "^1.8.0",
"@types/node": "^24.3.0", "@types/node": "^24.3.0",
"@types/remarkable": "^2.0.8", "@types/remarkable": "^2.0.8",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"@wailsio/runtime": "latest", "@wailsio/runtime": "latest",
"eslint": "^9.33.0", "eslint": "^9.34.0",
"eslint-plugin-vue": "^10.4.0", "eslint-plugin-vue": "^10.4.0",
"globals": "^16.3.0", "globals": "^16.3.0",
"typescript": "^5.9.2", "typescript": "^5.9.2",
"typescript-eslint": "^8.39.1", "typescript-eslint": "^8.40.0",
"unplugin-vue-components": "^29.0.0", "unplugin-vue-components": "^29.0.0",
"vite": "^7.1.2", "vite": "^7.1.3",
"vue-eslint-parser": "^10.2.0", "vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.0.5" "vue-tsc": "^3.0.6"
} }
}, },
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
@@ -1060,9 +1060,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.33.0", "version": "9.34.0",
"resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.33.0.tgz", "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.34.0.tgz",
"integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2128,17 +2128,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.39.1", "version": "8.40.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz", "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz",
"integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==", "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/scope-manager": "8.40.0",
"@typescript-eslint/type-utils": "8.39.1", "@typescript-eslint/type-utils": "8.40.0",
"@typescript-eslint/utils": "8.39.1", "@typescript-eslint/utils": "8.40.0",
"@typescript-eslint/visitor-keys": "8.39.1", "@typescript-eslint/visitor-keys": "8.40.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^7.0.0", "ignore": "^7.0.0",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -2152,7 +2152,7 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/parser": "^8.39.1", "@typescript-eslint/parser": "^8.40.0",
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0" "typescript": ">=4.8.4 <6.0.0"
} }
@@ -2168,16 +2168,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.39.1", "version": "8.40.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.39.1.tgz", "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.40.0.tgz",
"integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/scope-manager": "8.40.0",
"@typescript-eslint/types": "8.39.1", "@typescript-eslint/types": "8.40.0",
"@typescript-eslint/typescript-estree": "8.39.1", "@typescript-eslint/typescript-estree": "8.40.0",
"@typescript-eslint/visitor-keys": "8.39.1", "@typescript-eslint/visitor-keys": "8.40.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -2193,14 +2193,14 @@
} }
}, },
"node_modules/@typescript-eslint/project-service": { "node_modules/@typescript-eslint/project-service": {
"version": "8.39.1", "version": "8.40.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.40.0.tgz",
"integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.39.1", "@typescript-eslint/tsconfig-utils": "^8.40.0",
"@typescript-eslint/types": "^8.39.1", "@typescript-eslint/types": "^8.40.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -2215,14 +2215,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.39.1", "version": "8.40.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz",
"integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.39.1", "@typescript-eslint/types": "8.40.0",
"@typescript-eslint/visitor-keys": "8.39.1" "@typescript-eslint/visitor-keys": "8.40.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2233,9 +2233,9 @@
} }
}, },
"node_modules/@typescript-eslint/tsconfig-utils": { "node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.39.1", "version": "8.40.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz",
"integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2250,15 +2250,15 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.39.1", "version": "8.40.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz", "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz",
"integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==", "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.39.1", "@typescript-eslint/types": "8.40.0",
"@typescript-eslint/typescript-estree": "8.39.1", "@typescript-eslint/typescript-estree": "8.40.0",
"@typescript-eslint/utils": "8.39.1", "@typescript-eslint/utils": "8.40.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^2.1.0" "ts-api-utils": "^2.1.0"
}, },
@@ -2275,9 +2275,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.39.1", "version": "8.40.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.39.1.tgz", "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.40.0.tgz",
"integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -2289,16 +2289,16 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.39.1", "version": "8.40.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz",
"integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/project-service": "8.39.1", "@typescript-eslint/project-service": "8.40.0",
"@typescript-eslint/tsconfig-utils": "8.39.1", "@typescript-eslint/tsconfig-utils": "8.40.0",
"@typescript-eslint/types": "8.39.1", "@typescript-eslint/types": "8.40.0",
"@typescript-eslint/visitor-keys": "8.39.1", "@typescript-eslint/visitor-keys": "8.40.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -2344,16 +2344,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.39.1", "version": "8.40.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.39.1.tgz", "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.40.0.tgz",
"integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.7.0", "@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/scope-manager": "8.40.0",
"@typescript-eslint/types": "8.39.1", "@typescript-eslint/types": "8.40.0",
"@typescript-eslint/typescript-estree": "8.39.1" "@typescript-eslint/typescript-estree": "8.40.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2368,13 +2368,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.39.1", "version": "8.40.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz",
"integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.39.1", "@typescript-eslint/types": "8.40.0",
"eslint-visitor-keys": "^4.2.1" "eslint-visitor-keys": "^4.2.1"
}, },
"engines": { "engines": {
@@ -2403,68 +2403,68 @@
} }
}, },
"node_modules/@volar/language-core": { "node_modules/@volar/language-core": {
"version": "2.4.22", "version": "2.4.23",
"resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.22.tgz", "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.23.tgz",
"integrity": "sha512-gp4M7Di5KgNyIyO903wTClYBavRt6UyFNpc5LWfyZr1lBsTUY+QrVZfmbNF2aCyfklBOVk9YC4p+zkwoyT7ECg==", "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/source-map": "2.4.22" "@volar/source-map": "2.4.23"
} }
}, },
"node_modules/@volar/source-map": { "node_modules/@volar/source-map": {
"version": "2.4.22", "version": "2.4.23",
"resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.22.tgz", "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.23.tgz",
"integrity": "sha512-L2nVr/1vei0xKRgO2tYVXtJYd09HTRjaZi418e85Q+QdbbqA8h7bBjfNyPPSsjnrOO4l4kaAo78c8SQUAdHvgA==", "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@volar/typescript": { "node_modules/@volar/typescript": {
"version": "2.4.22", "version": "2.4.23",
"resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.22.tgz", "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.23.tgz",
"integrity": "sha512-6ZczlJW1/GWTrNnkmZxJp4qyBt/SGVlcTuCWpI5zLrdPdCZsj66Aff9ZsfFaT3TyjG8zVYgBMYPuCm/eRkpcpQ==", "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/language-core": "2.4.22", "@volar/language-core": "2.4.23",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"vscode-uri": "^3.0.8" "vscode-uri": "^3.0.8"
} }
}, },
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
"version": "3.5.18", "version": "3.5.19",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.18.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.19.tgz",
"integrity": "sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==", "integrity": "sha512-/afpyvlkrSNYbPo94Qu8GtIOWS+g5TRdOvs6XZNw6pWQQmj5pBgSZvEPOIZlqWq0YvoUhDDQaQ2TnzuJdOV4hA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.28.0", "@babel/parser": "^7.28.3",
"@vue/shared": "3.5.18", "@vue/shared": "3.5.19",
"entities": "^4.5.0", "entities": "^4.5.0",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
} }
}, },
"node_modules/@vue/compiler-dom": { "node_modules/@vue/compiler-dom": {
"version": "3.5.18", "version": "3.5.19",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.18.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.19.tgz",
"integrity": "sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==", "integrity": "sha512-Drs6rPHQZx/pN9S6ml3Z3K/TWCIRPvzG2B/o5kFK9X0MNHt8/E+38tiRfojufrYBfA6FQUFB2qBBRXlcSXWtOA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-core": "3.5.18", "@vue/compiler-core": "3.5.19",
"@vue/shared": "3.5.18" "@vue/shared": "3.5.19"
} }
}, },
"node_modules/@vue/compiler-sfc": { "node_modules/@vue/compiler-sfc": {
"version": "3.5.18", "version": "3.5.19",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.18.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.19.tgz",
"integrity": "sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==", "integrity": "sha512-YWCm1CYaJ+2RvNmhCwI7t3I3nU+hOrWGWMsn+Z/kmm1jy5iinnVtlmkiZwbLlbV1SRizX7vHsc0/bG5dj0zRTg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.28.0", "@babel/parser": "^7.28.3",
"@vue/compiler-core": "3.5.18", "@vue/compiler-core": "3.5.19",
"@vue/compiler-dom": "3.5.18", "@vue/compiler-dom": "3.5.19",
"@vue/compiler-ssr": "3.5.18", "@vue/compiler-ssr": "3.5.19",
"@vue/shared": "3.5.18", "@vue/shared": "3.5.19",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"postcss": "^8.5.6", "postcss": "^8.5.6",
@@ -2472,13 +2472,13 @@
} }
}, },
"node_modules/@vue/compiler-ssr": { "node_modules/@vue/compiler-ssr": {
"version": "3.5.18", "version": "3.5.19",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.18.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.19.tgz",
"integrity": "sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==", "integrity": "sha512-/wx0VZtkWOPdiQLWPeQeqpHWR/LuNC7bHfSX7OayBTtUy8wur6vT6EQIX6Et86aED6J+y8tTw43qo2uoqGg5sw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.18", "@vue/compiler-dom": "3.5.19",
"@vue/shared": "3.5.18" "@vue/shared": "3.5.19"
} }
}, },
"node_modules/@vue/compiler-vue2": { "node_modules/@vue/compiler-vue2": {
@@ -2526,13 +2526,13 @@
} }
}, },
"node_modules/@vue/language-core": { "node_modules/@vue/language-core": {
"version": "3.0.5", "version": "3.0.6",
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.0.5.tgz", "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.0.6.tgz",
"integrity": "sha512-gCEjn9Ik7I/seHVNIEipOm8W+f3/kg60e8s1IgIkMYma2wu9ZGUTMv3mSL2bX+Md2L8fslceJ4SU8j1fgSRoiw==", "integrity": "sha512-e2RRzYWm+qGm8apUHW1wA5RQxzNhkqbbKdbKhiDUcmMrNAZGyM8aTiL3UrTqkaFI5s7wJRGGrp4u3jgusuBp2A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/language-core": "2.4.22", "@volar/language-core": "2.4.23",
"@vue/compiler-dom": "^3.5.0", "@vue/compiler-dom": "^3.5.0",
"@vue/compiler-vue2": "^2.7.16", "@vue/compiler-vue2": "^2.7.16",
"@vue/shared": "^3.5.0", "@vue/shared": "^3.5.0",
@@ -2564,53 +2564,53 @@
} }
}, },
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
"version": "3.5.18", "version": "3.5.19",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.18.tgz", "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.19.tgz",
"integrity": "sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==", "integrity": "sha512-4bueZg2qs5MSsK2dQk3sssV0cfvxb/QZntTC8v7J448GLgmfPkQ+27aDjlt40+XFqOwUq5yRxK5uQh14Fc9eVA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/shared": "3.5.18" "@vue/shared": "3.5.19"
} }
}, },
"node_modules/@vue/runtime-core": { "node_modules/@vue/runtime-core": {
"version": "3.5.18", "version": "3.5.19",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.18.tgz", "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.19.tgz",
"integrity": "sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==", "integrity": "sha512-TaooCr8Hge1sWjLSyhdubnuofs3shhzZGfyD11gFolZrny76drPwBVQj28/z/4+msSFb18tOIg6VVVgf9/IbIA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/reactivity": "3.5.18", "@vue/reactivity": "3.5.19",
"@vue/shared": "3.5.18" "@vue/shared": "3.5.19"
} }
}, },
"node_modules/@vue/runtime-dom": { "node_modules/@vue/runtime-dom": {
"version": "3.5.18", "version": "3.5.19",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.18.tgz", "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.19.tgz",
"integrity": "sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==", "integrity": "sha512-qmahqeok6ztuUTmV8lqd7N9ymbBzctNF885n8gL3xdCC1u2RnM/coX16Via0AiONQXUoYpxPojL3U1IsDgSWUQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/reactivity": "3.5.18", "@vue/reactivity": "3.5.19",
"@vue/runtime-core": "3.5.18", "@vue/runtime-core": "3.5.19",
"@vue/shared": "3.5.18", "@vue/shared": "3.5.19",
"csstype": "^3.1.3" "csstype": "^3.1.3"
} }
}, },
"node_modules/@vue/server-renderer": { "node_modules/@vue/server-renderer": {
"version": "3.5.18", "version": "3.5.19",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.18.tgz", "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.19.tgz",
"integrity": "sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==", "integrity": "sha512-ZJ/zV9SQuaIO+BEEVq/2a6fipyrSYfjKMU3267bPUk+oTx/hZq3RzV7VCh0Unlppt39Bvh6+NzxeopIFv4HJNg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-ssr": "3.5.18", "@vue/compiler-ssr": "3.5.19",
"@vue/shared": "3.5.18" "@vue/shared": "3.5.19"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "3.5.18" "vue": "3.5.19"
} }
}, },
"node_modules/@vue/shared": { "node_modules/@vue/shared": {
"version": "3.5.18", "version": "3.5.19",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.18.tgz", "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.19.tgz",
"integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==", "integrity": "sha512-IhXCOn08wgKrLQxRFKKlSacWg4Goi1BolrdEeLYn6tgHjJNXVrWJ5nzoxZqNwl5p88aLlQ8LOaoMa3AYvaKJ/Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@wailsio/runtime": { "node_modules/@wailsio/runtime": {
@@ -2661,9 +2661,9 @@
} }
}, },
"node_modules/alien-signals": { "node_modules/alien-signals": {
"version": "2.0.6", "version": "2.0.7",
"resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-2.0.6.tgz", "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-2.0.7.tgz",
"integrity": "sha512-P3TxJSe31bUHBiblg59oU1PpaWPtmxF9GhJ/cB7OkgJ0qN/ifFSKUI25/v8ZhsT+lIG6ac8DpTOplXxORX6F3Q==", "integrity": "sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -3166,9 +3166,9 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.33.0", "version": "9.34.0",
"resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.33.0.tgz", "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.34.0.tgz",
"integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -3178,7 +3178,7 @@
"@eslint/config-helpers": "^0.3.1", "@eslint/config-helpers": "^0.3.1",
"@eslint/core": "^0.15.2", "@eslint/core": "^0.15.2",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.33.0", "@eslint/js": "9.34.0",
"@eslint/plugin-kit": "^0.3.5", "@eslint/plugin-kit": "^0.3.5",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
@@ -4794,16 +4794,16 @@
} }
}, },
"node_modules/typescript-eslint": { "node_modules/typescript-eslint": {
"version": "8.39.1", "version": "8.40.0",
"resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.39.1.tgz", "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.40.0.tgz",
"integrity": "sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg==", "integrity": "sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "8.39.1", "@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.39.1", "@typescript-eslint/parser": "8.40.0",
"@typescript-eslint/typescript-estree": "8.39.1", "@typescript-eslint/typescript-estree": "8.40.0",
"@typescript-eslint/utils": "8.39.1" "@typescript-eslint/utils": "8.40.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -5103,14 +5103,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.1.2", "version": "7.1.3",
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.1.2.tgz", "resolved": "https://registry.npmmirror.com/vite/-/vite-7.1.3.tgz",
"integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==", "integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.6", "fdir": "^6.5.0",
"picomatch": "^4.0.3", "picomatch": "^4.0.3",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"rollup": "^4.43.0", "rollup": "^4.43.0",
@@ -5178,11 +5178,14 @@
} }
}, },
"node_modules/vite/node_modules/fdir": { "node_modules/vite/node_modules/fdir": {
"version": "6.4.6", "version": "6.5.0",
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.4.6.tgz", "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": { "peerDependencies": {
"picomatch": "^3 || ^4" "picomatch": "^3 || ^4"
}, },
@@ -5213,16 +5216,16 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vue": { "node_modules/vue": {
"version": "3.5.18", "version": "3.5.19",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.18.tgz", "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.19.tgz",
"integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==", "integrity": "sha512-ZRh0HTmw6KChRYWgN8Ox/wi7VhpuGlvMPrHjIsdRbzKNgECFLzy+dKL5z9yGaBSjCpmcfJCbh3I1tNSRmBz2tg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.18", "@vue/compiler-dom": "3.5.19",
"@vue/compiler-sfc": "3.5.18", "@vue/compiler-sfc": "3.5.19",
"@vue/runtime-dom": "3.5.18", "@vue/runtime-dom": "3.5.19",
"@vue/server-renderer": "3.5.18", "@vue/server-renderer": "3.5.19",
"@vue/shared": "3.5.18" "@vue/shared": "3.5.19"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "*" "typescript": "*"
@@ -5318,14 +5321,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vue-tsc": { "node_modules/vue-tsc": {
"version": "3.0.5", "version": "3.0.6",
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.0.5.tgz", "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.0.6.tgz",
"integrity": "sha512-PsTFN9lo1HJCrZw9NoqjYcAbYDXY0cOKyuW2E7naX5jcaVyWpqEsZOHN9Dws5890E8e5SDAD4L4Zam3dxG3/Cw==", "integrity": "sha512-Tbs8Whd43R2e2nxez4WXPvvdjGbW24rOSgRhLOHXzWiT4pcP4G7KeWh0YCn18rF4bVwv7tggLLZ6MJnO6jXPBg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/typescript": "2.4.22", "@volar/typescript": "2.4.23",
"@vue/language-core": "3.0.5" "@vue/language-core": "3.0.6"
}, },
"bin": { "bin": {
"vue-tsc": "bin/vue-tsc.js" "vue-tsc": "bin/vue-tsc.js"

View File

@@ -56,26 +56,26 @@
"prettier": "^3.6.2", "prettier": "^3.6.2",
"remarkable": "^2.0.1", "remarkable": "^2.0.1",
"sass": "^1.90.0", "sass": "^1.90.0",
"vue": "^3.5.18", "vue": "^3.5.19",
"vue-i18n": "^11.1.11", "vue-i18n": "^11.1.11",
"vue-pick-colors": "^1.8.0", "vue-pick-colors": "^1.8.0",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.33.0", "@eslint/js": "^9.34.0",
"@lezer/generator": "^1.8.0", "@lezer/generator": "^1.8.0",
"@types/node": "^24.3.0", "@types/node": "^24.3.0",
"@types/remarkable": "^2.0.8", "@types/remarkable": "^2.0.8",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"@wailsio/runtime": "latest", "@wailsio/runtime": "latest",
"eslint": "^9.33.0", "eslint": "^9.34.0",
"eslint-plugin-vue": "^10.4.0", "eslint-plugin-vue": "^10.4.0",
"globals": "^16.3.0", "globals": "^16.3.0",
"typescript": "^5.9.2", "typescript": "^5.9.2",
"typescript-eslint": "^8.39.1", "typescript-eslint": "^8.40.0",
"unplugin-vue-components": "^29.0.0", "unplugin-vue-components": "^29.0.0",
"vite": "^7.1.2", "vite": "^7.1.3",
"vue-eslint-parser": "^10.2.0", "vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.0.5" "vue-tsc": "^3.0.6"
} }
} }

View File

@@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import {useI18n} from 'vue-i18n'; import {useI18n} from 'vue-i18n';
import {onMounted, onUnmounted, ref, watch, computed} from 'vue'; import {computed, onMounted, onUnmounted, ref, watch} from 'vue';
import {useConfigStore} from '@/stores/configStore'; import {useConfigStore} from '@/stores/configStore';
import {useEditorStore} from '@/stores/editorStore'; import {useEditorStore} from '@/stores/editorStore';
import {useUpdateStore} from '@/stores/updateStore'; import {useUpdateStore} from '@/stores/updateStore';
import {useWindowStore} from '@/stores/windowStore'; import {useWindowStore} from '@/stores/windowStore';
import {useSystemStore} from '@/stores/systemStore';
import * as runtime from '@wailsio/runtime'; import * as runtime from '@wailsio/runtime';
import {useRouter} from 'vue-router'; import {useRouter} from 'vue-router';
import BlockLanguageSelector from './BlockLanguageSelector.vue'; import BlockLanguageSelector from './BlockLanguageSelector.vue';
@@ -17,22 +18,32 @@ const editorStore = useEditorStore();
const configStore = useConfigStore(); const configStore = useConfigStore();
const updateStore = useUpdateStore(); const updateStore = useUpdateStore();
const windowStore = useWindowStore(); const windowStore = useWindowStore();
const systemStore = useSystemStore();
const {t} = useI18n(); const {t} = useI18n();
const router = useRouter(); const router = useRouter();
// 当前块是否支持格式化的响应式状态 // 当前块是否支持格式化的响应式状态
const canFormatCurrentBlock = ref(false); const canFormatCurrentBlock = ref(false);
// 窗口置顶状态管理(仅当前窗口,不同步到配置文件) // 窗口置顶状态 - 合并配置和临时状态
const isCurrentWindowOnTop = ref(false); const isCurrentWindowOnTop = computed(() => {
return configStore.config.general.alwaysOnTop || systemStore.isWindowOnTop;
const setWindowAlwaysOnTop = async (isTop: boolean) => { });
await runtime.Window.SetAlwaysOnTop(isTop);
};
// 切换窗口置顶状态
const toggleAlwaysOnTop = async () => { const toggleAlwaysOnTop = async () => {
isCurrentWindowOnTop.value = !isCurrentWindowOnTop.value; const currentlyOnTop = isCurrentWindowOnTop.value;
await runtime.Window.SetAlwaysOnTop(isCurrentWindowOnTop.value);
if (currentlyOnTop) {
// 如果当前是置顶状态,彻底关闭所有置顶
if (configStore.config.general.alwaysOnTop) {
await configStore.setAlwaysOnTop(false);
}
await systemStore.setWindowOnTop(false);
} else {
// 如果当前不是置顶状态,开启临时置顶
await systemStore.setWindowOnTop(true);
}
}; };
// 跳转到设置页面 // 跳转到设置页面
@@ -59,11 +70,11 @@ const updateFormatButtonState = () => {
// 获取活动块和语言信息 // 获取活动块和语言信息
const state = view.state; const state = view.state;
const activeBlock = getActiveNoteBlock(state as any); const activeBlock = getActiveNoteBlock(state as any);
// 检查块和语言格式化支持 // 检查块和语言格式化支持
canFormatCurrentBlock.value = !!( canFormatCurrentBlock.value = !!(
activeBlock && activeBlock &&
getLanguage(activeBlock.language.name as any)?.prettier getLanguage(activeBlock.language.name as any)?.prettier
); );
} catch (error) { } catch (error) {
console.warn('Error checking format capability:', error); console.warn('Error checking format capability:', error);
@@ -74,32 +85,32 @@ const updateFormatButtonState = () => {
// 创建带300ms防抖的更新函数 // 创建带300ms防抖的更新函数
const debouncedUpdateFormatButton = (() => { const debouncedUpdateFormatButton = (() => {
let timeout: number | null = null; let timeout: number | null = null;
return () => { return () => {
if (timeout) clearTimeout(timeout); if (timeout) clearTimeout(timeout);
timeout = window.setTimeout(() => { timeout = window.setTimeout(() => {
updateFormatButtonState(); updateFormatButtonState();
timeout = null; timeout = null;
}, 300); }, 1000);
}; };
})(); })();
// 编辑器事件管理 // 编辑器事件管理
const setupEditorListeners = (view: any) => { const setupEditorListeners = (view: any) => {
if (!view?.dom) return []; if (!view?.dom) return [];
const events = [ const events = [
{ type: 'click', handler: updateFormatButtonState }, {type: 'click', handler: updateFormatButtonState},
{ type: 'keyup', handler: debouncedUpdateFormatButton }, {type: 'keyup', handler: debouncedUpdateFormatButton},
{ type: 'focus', handler: updateFormatButtonState } {type: 'focus', handler: updateFormatButtonState}
]; ];
// 注册所有事件 // 注册所有事件
events.forEach(event => view.dom.addEventListener(event.type, event.handler)); events.forEach(event => view.dom.addEventListener(event.type, event.handler));
// 返回清理函数数组 // 返回清理函数数组
return events.map(event => return events.map(event =>
() => view.dom.removeEventListener(event.type, event.handler) () => view.dom.removeEventListener(event.type, event.handler)
); );
}; };
@@ -107,22 +118,22 @@ const setupEditorListeners = (view: any) => {
let cleanupListeners: (() => void)[] = []; let cleanupListeners: (() => void)[] = [];
watch( watch(
() => editorStore.editorView, () => editorStore.editorView,
(newView) => { (newView) => {
// 清理旧监听器 // 清理旧监听器
cleanupListeners.forEach(cleanup => cleanup()); cleanupListeners.forEach(cleanup => cleanup());
cleanupListeners = []; cleanupListeners = [];
if (newView) { if (newView) {
// 初始更新状态 // 初始更新状态
updateFormatButtonState(); updateFormatButtonState();
// 设置新监听器 // 设置新监听器
cleanupListeners = setupEditorListeners(newView); cleanupListeners = setupEditorListeners(newView);
} else { } else {
canFormatCurrentBlock.value = false; canFormatCurrentBlock.value = false;
} }
}, },
{ immediate: true } {immediate: true}
); );
// 组件生命周期 // 组件生命周期
@@ -143,12 +154,28 @@ onUnmounted(() => {
// 组件加载后初始化置顶状态 // 组件加载后初始化置顶状态
watch(isLoaded, async (loaded) => { watch(isLoaded, async (loaded) => {
if (loaded) { if (loaded) {
// 初始化时从配置文件读取置顶状态 // 应用合并后的置顶状态
isCurrentWindowOnTop.value = configStore.config.general.alwaysOnTop; const shouldBeOnTop = configStore.config.general.alwaysOnTop || systemStore.isWindowOnTop;
await setWindowAlwaysOnTop(isCurrentWindowOnTop.value); try {
await runtime.Window.SetAlwaysOnTop(shouldBeOnTop);
} catch (error) {
console.error('Failed to apply window pin state:', error);
}
} }
}); });
// 监听配置变化,同步窗口状态
watch(
() => isCurrentWindowOnTop.value,
async (shouldBeOnTop) => {
try {
await runtime.Window.SetAlwaysOnTop(shouldBeOnTop);
} catch (error) {
console.error('Failed to sync window pin state:', error);
}
}
);
const handleUpdateButtonClick = async () => { const handleUpdateButtonClick = async () => {
if (updateStore.hasUpdate && !updateStore.isUpdating && !updateStore.updateSuccess) { if (updateStore.hasUpdate && !updateStore.isUpdating && !updateStore.updateSuccess) {
// 开始下载更新 // 开始下载更新
@@ -230,29 +257,33 @@ const updateButtonTitle = computed(() => {
@click="handleUpdateButtonClick" @click="handleUpdateButtonClick"
> >
<!-- 检查更新中 --> <!-- 检查更新中 -->
<svg v-if="updateStore.isChecking" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" <svg v-if="updateStore.isChecking" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotating"> stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotating">
<path d="M21 12a9 9 0 1 1-6.219-8.56"/> <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg> </svg>
<!-- 下载更新中 --> <!-- 下载更新中 -->
<svg v-else-if="updateStore.isUpdating" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" <svg v-else-if="updateStore.isUpdating" xmlns="http://www.w3.org/2000/svg" width="14" height="14"
viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotating"> stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotating">
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path> <path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
<path d="M12 2a10 10 0 1 0 10 10"></path> <path d="M12 2a10 10 0 1 0 10 10"></path>
</svg> </svg>
<!-- 更新成功等待重启 --> <!-- 更新成功等待重启 -->
<svg v-else-if="updateStore.updateSuccess" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" <svg v-else-if="updateStore.updateSuccess" xmlns="http://www.w3.org/2000/svg" width="14" height="14"
viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pulsing"> stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pulsing">
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path> <path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path>
<line x1="12" y1="2" x2="12" y2="12"></line> <line x1="12" y1="2" x2="12" y2="12"></line>
</svg> </svg>
<!-- 有更新可用 --> <!-- 有更新可用 -->
<svg v-else xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" <svg v-else xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/> <path
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="7.5,10.5 12,15 16.5,10.5"/> <polyline points="7.5,10.5 12,15 16.5,10.5"/>
<polyline points="12,15 12,3"/> <polyline points="12,15 12,3"/>
</svg> </svg>
@@ -338,39 +369,39 @@ const updateButtonTitle = computed(() => {
&.available { &.available {
background-color: rgba(76, 175, 80, 0.1); background-color: rgba(76, 175, 80, 0.1);
animation: pulse 2s infinite; animation: pulse 2s infinite;
svg { svg {
stroke: #4caf50; stroke: #4caf50;
} }
&:hover { &:hover {
background-color: rgba(76, 175, 80, 0.2); background-color: rgba(76, 175, 80, 0.2);
transform: scale(1.05); transform: scale(1.05);
} }
} }
/* 检查更新中状态 */ /* 检查更新中状态 */
&.checking { &.checking {
background-color: rgba(255, 193, 7, 0.1); background-color: rgba(255, 193, 7, 0.1);
svg { svg {
stroke: #ffc107; stroke: #ffc107;
} }
} }
/* 更新下载中状态 */ /* 更新下载中状态 */
&.updating { &.updating {
background-color: rgba(33, 150, 243, 0.1); background-color: rgba(33, 150, 243, 0.1);
svg { svg {
stroke: #2196f3; stroke: #2196f3;
} }
} }
/* 更新成功状态 */ /* 更新成功状态 */
&.success { &.success {
background-color: rgba(156, 39, 176, 0.1); background-color: rgba(156, 39, 176, 0.1);
svg { svg {
stroke: #9c27b0; stroke: #9c27b0;
} }
@@ -380,7 +411,7 @@ const updateButtonTitle = computed(() => {
.rotating { .rotating {
animation: rotate 1.5s linear infinite; animation: rotate 1.5s linear infinite;
} }
/* 脉冲动画 */ /* 脉冲动画 */
.pulsing { .pulsing {
animation: pulse-strong 1.2s ease-in-out infinite; animation: pulse-strong 1.2s ease-in-out infinite;
@@ -394,7 +425,7 @@ const updateButtonTitle = computed(() => {
opacity: 0.7; opacity: 0.7;
} }
} }
@keyframes pulse-strong { @keyframes pulse-strong {
0%, 100% { 0%, 100% {
transform: scale(1); transform: scale(1);
@@ -405,7 +436,7 @@ const updateButtonTitle = computed(() => {
opacity: 0.8; opacity: 0.8;
} }
} }
@keyframes rotate { @keyframes rotate {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);

View File

@@ -134,6 +134,7 @@ export default {
showInSystemTray: 'Show in System Tray', showInSystemTray: 'Show in System Tray',
enableSystemTray: 'Enable System Tray', enableSystemTray: 'Enable System Tray',
alwaysOnTop: 'Always on Top', alwaysOnTop: 'Always on Top',
enableWindowSnap: 'Enable Window Snapping',
startup: 'Startup Settings', startup: 'Startup Settings',
startAtLogin: 'Start at Login', startAtLogin: 'Start at Login',
dataStorage: 'Data Storage', dataStorage: 'Data Storage',

View File

@@ -135,6 +135,7 @@ export default {
showInSystemTray: '在系统托盘中显示', showInSystemTray: '在系统托盘中显示',
enableSystemTray: '启用系统托盘', enableSystemTray: '启用系统托盘',
alwaysOnTop: '窗口始终置顶', alwaysOnTop: '窗口始终置顶',
enableWindowSnap: '启用窗口吸附',
startup: '启动设置', startup: '启动设置',
startAtLogin: '开机自启动', startAtLogin: '开机自启动',
dataStorage: '数据存储', dataStorage: '数据存储',

View File

@@ -8,6 +8,56 @@ import KeyBindingsPage from '@/views/settings/pages/KeyBindingsPage.vue';
import UpdatesPage from '@/views/settings/pages/UpdatesPage.vue'; import UpdatesPage from '@/views/settings/pages/UpdatesPage.vue';
import ExtensionsPage from '@/views/settings/pages/ExtensionsPage.vue'; import ExtensionsPage from '@/views/settings/pages/ExtensionsPage.vue';
import BackupPage from '@/views/settings/pages/BackupPage.vue'; import BackupPage from '@/views/settings/pages/BackupPage.vue';
// 测试页面
import TestPage from '@/views/settings/pages/TestPage.vue';
// 基础设置子路由
const settingsChildren: RouteRecordRaw[] = [
{
path: 'general',
name: 'SettingsGeneral',
component: GeneralPage
},
{
path: 'editing',
name: 'SettingsEditing',
component: EditingPage
},
{
path: 'appearance',
name: 'SettingsAppearance',
component: AppearancePage
},
{
path: 'extensions',
name: 'SettingsExtensions',
component: ExtensionsPage
},
{
path: 'key-bindings',
name: 'SettingsKeyBindings',
component: KeyBindingsPage
},
{
path: 'updates',
name: 'SettingsUpdates',
component: UpdatesPage
},
{
path: 'backup',
name: 'SettingsBackup',
component: BackupPage
}
];
// 仅在开发环境添加测试页面路由
if (import.meta.env.DEV) {
settingsChildren.push({
path: 'test',
name: 'SettingsTest',
component: TestPage
});
}
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ {
@@ -20,43 +70,7 @@ const routes: RouteRecordRaw[] = [
name: 'Settings', name: 'Settings',
redirect: '/settings/general', redirect: '/settings/general',
component: Settings, component: Settings,
children: [ children: settingsChildren
{
path: 'general',
name: 'SettingsGeneral',
component: GeneralPage
},
{
path: 'editing',
name: 'SettingsEditing',
component: EditingPage
},
{
path: 'appearance',
name: 'SettingsAppearance',
component: AppearancePage
},
{
path: 'extensions',
name: 'SettingsExtensions',
component: ExtensionsPage
},
{
path: 'key-bindings',
name: 'SettingsKeyBindings',
component: KeyBindingsPage
},
{
path: 'updates',
name: 'SettingsUpdates',
component: UpdatesPage
},
{
path: 'backup',
name: 'SettingsBackup',
component: BackupPage
}
]
} }
]; ];

View File

@@ -62,7 +62,8 @@ const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
enableSystemTray: 'general.enableSystemTray', enableSystemTray: 'general.enableSystemTray',
startAtLogin: 'general.startAtLogin', startAtLogin: 'general.startAtLogin',
enableGlobalHotkey: 'general.enableGlobalHotkey', enableGlobalHotkey: 'general.enableGlobalHotkey',
globalHotkey: 'general.globalHotkey' globalHotkey: 'general.globalHotkey',
enableWindowSnap: 'general.enableWindowSnap',
} as const; } as const;
const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = { const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
@@ -176,7 +177,8 @@ const DEFAULT_CONFIG: AppConfig = {
alt: true, alt: true,
win: false, win: false,
key: 'X' key: 'X'
} },
enableWindowSnap: true,
}, },
editing: { editing: {
fontSize: CONFIG_LIMITS.fontSize.default, fontSize: CONFIG_LIMITS.fontSize.default,
@@ -520,6 +522,9 @@ export const useConfigStore = defineStore('config', () => {
// 再调用系统设置API // 再调用系统设置API
await StartupService.SetEnabled(value); await StartupService.SetEnabled(value);
}, },
// 窗口吸附配置相关方法
setEnableWindowSnap: async (value: boolean) => await updateGeneralConfig('enableWindowSnap', value),
// 更新配置相关方法 // 更新配置相关方法
setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value), setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value),

View File

@@ -3,13 +3,15 @@ import {computed, ref} from 'vue';
import {DocumentService} from '@/../bindings/voidraft/internal/services'; import {DocumentService} from '@/../bindings/voidraft/internal/services';
import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice'; import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice';
import {Document} from '@/../bindings/voidraft/internal/models/models'; import {Document} from '@/../bindings/voidraft/internal/models/models';
import {useSystemStore} from './systemStore';
const SCRATCH_DOCUMENT_ID = 1; // 默认草稿文档ID
export const useDocumentStore = defineStore('document', () => { export const useDocumentStore = defineStore('document', () => {
const DEFAULT_DOCUMENT_ID = ref<number>(1); // 默认草稿文档ID
// === 核心状态 === // === 核心状态 ===
const documents = ref<Record<number, Document>>({}); const documents = ref<Record<number, Document>>({});
const recentDocumentIds = ref<number[]>([SCRATCH_DOCUMENT_ID]); const recentDocumentIds = ref<number[]>([DEFAULT_DOCUMENT_ID.value]);
const currentDocumentId = ref<number | null>(null); const currentDocumentId = ref<number | null>(null);
const currentDocument = ref<Document | null>(null); const currentDocument = ref<Document | null>(null);
@@ -159,7 +161,7 @@ export const useDocumentStore = defineStore('document', () => {
const deleteDocument = async (docId: number): Promise<boolean> => { const deleteDocument = async (docId: number): Promise<boolean> => {
try { try {
// 检查是否是默认文档使用ID判断 // 检查是否是默认文档使用ID判断
if (docId === SCRATCH_DOCUMENT_ID) { if (docId === DEFAULT_DOCUMENT_ID.value) {
return false; return false;
} }
@@ -221,6 +223,7 @@ export const useDocumentStore = defineStore('document', () => {
}; };
return { return {
DEFAULT_DOCUMENT_ID,
// 状态 // 状态
documents, documents,
documentList, documentList,

View File

@@ -19,14 +19,15 @@ export const useSystemStore = defineStore('system', () => {
// 状态 // 状态
const environment = ref<SystemEnvironment | null>(null); const environment = ref<SystemEnvironment | null>(null);
const isLoading = ref(false); const isLoading = ref(false);
// 窗口置顶状态管理
const isWindowOnTop = ref<boolean>(false);
// 计算属性 // 计算属性
const isWindows = computed(() => environment.value?.OS === 'windows'); const isWindows = computed(() => environment.value?.OS === 'windows');
const isMacOS = computed(() => environment.value?.OS === 'darwin'); const isMacOS = computed(() => environment.value?.OS === 'darwin');
const isLinux = computed(() => environment.value?.OS === 'linux'); const isLinux = computed(() => environment.value?.OS === 'linux');
// 获取标题栏高度 // 获取标题栏高度
const titleBarHeight = computed(() => { const titleBarHeight = computed(() => {
if (isWindows.value) return '32px'; if (isWindows.value) return '32px';
@@ -49,10 +50,31 @@ export const useSystemStore = defineStore('system', () => {
} }
}; };
// 设置窗口置顶状态
const setWindowOnTop = async (isPinned: boolean): Promise<void> => {
isWindowOnTop.value = isPinned;
try {
await runtime.Window.SetAlwaysOnTop(isPinned);
} catch (error) {
console.error('Failed to set window always on top:', error);
}
};
// 切换窗口置顶状态
const toggleWindowOnTop = async (): Promise<void> => {
await setWindowOnTop(!isWindowOnTop.value);
};
// 重置临时置顶状态不调用系统API
const resetWindowOnTop = (): void => {
isWindowOnTop.value = false;
};
return { return {
// 状态 // 状态
environment, environment,
isLoading, isLoading,
isWindowOnTop,
// 计算属性 // 计算属性
isWindows, isWindows,
@@ -62,5 +84,14 @@ export const useSystemStore = defineStore('system', () => {
// 方法 // 方法
initializeSystemInfo, initializeSystemInfo,
setWindowOnTop,
toggleWindowOnTop,
resetWindowOnTop,
}; };
}, {
persist: {
key: 'voidraft-system',
storage: localStorage,
pick: ['isWindowOnTop']
}
}); });

View File

@@ -19,6 +19,11 @@ const navItems = [
{ id: 'updates', icon: '🔄', route: '/settings/updates' } { id: 'updates', icon: '🔄', route: '/settings/updates' }
]; ];
// 仅在开发环境添加测试页面导航
if (import.meta.env.DEV) {
navItems.push({ id: 'test', icon: '🧪', route: '/settings/test' });
}
const activeNavItem = ref(route.path.split('/').pop() || 'general'); const activeNavItem = ref(route.path.split('/').pop() || 'general');
// 处理导航点击 // 处理导航点击
@@ -56,7 +61,7 @@ const goBackToEditor = async () => {
@click="handleNavClick(item)" @click="handleNavClick(item)"
> >
<span class="nav-icon">{{ item.icon }}</span> <span class="nav-icon">{{ item.icon }}</span>
<span class="nav-text">{{ t(`settings.${item.id}`) }}</span> <span class="nav-text">{{ item.id === 'test' ? 'Test' : t(`settings.${item.id}`) }}</span>
</div> </div>
</div> </div>
<div class="settings-footer"> <div class="settings-footer">

View File

@@ -159,6 +159,12 @@ const enableSystemTray = computed({
set: (value: boolean) => configStore.setEnableSystemTray(value) set: (value: boolean) => configStore.setEnableSystemTray(value)
}); });
// 计算属性 - 启用窗口吸附
const enableWindowSnap = computed({
get: () => configStore.config.general.enableWindowSnap,
set: (value: boolean) => configStore.setEnableWindowSnap(value)
});
// 计算属性 - 开机启动 // 计算属性 - 开机启动
const startAtLogin = computed({ const startAtLogin = computed({
get: () => configStore.config.general.startAtLogin, get: () => configStore.config.general.startAtLogin,
@@ -334,6 +340,9 @@ onUnmounted(() => {
<SettingItem :title="t('settings.enableSystemTray')"> <SettingItem :title="t('settings.enableSystemTray')">
<ToggleSwitch v-model="enableSystemTray"/> <ToggleSwitch v-model="enableSystemTray"/>
</SettingItem> </SettingItem>
<SettingItem :title="t('settings.enableWindowSnap')">
<ToggleSwitch v-model="enableWindowSnap"/>
</SettingItem>
</SettingSection> </SettingSection>
<SettingSection :title="t('settings.startup')"> <SettingSection :title="t('settings.startup')">

View File

@@ -0,0 +1,274 @@
<template>
<div class="settings-page">
<SettingSection title="Development Test Page">
<div class="dev-description">
This page is only available in development environment for testing notification and badge services.
</div>
</SettingSection>
<!-- Badge测试区域 -->
<SettingSection title="Badge Service Test">
<SettingItem title="Badge Text">
<input
v-model="badgeText"
type="text"
placeholder="Enter badge text (empty to remove)"
class="select-input"
/>
</SettingItem>
<SettingItem title="Actions">
<div class="button-group">
<button @click="testBadge" class="test-button primary">
Set Badge
</button>
<button @click="clearBadge" class="test-button">
Clear Badge
</button>
</div>
</SettingItem>
<div v-if="badgeStatus" class="test-status" :class="badgeStatus.type">
{{ badgeStatus.message }}
</div>
</SettingSection>
<!-- 通知测试区域 -->
<SettingSection title="Notification Service Test">
<SettingItem title="Title">
<input
v-model="notificationTitle"
type="text"
placeholder="Notification title"
class="select-input"
/>
</SettingItem>
<SettingItem title="Subtitle">
<input
v-model="notificationSubtitle"
type="text"
placeholder="Notification subtitle"
class="select-input"
/>
</SettingItem>
<SettingItem title="Body">
<textarea
v-model="notificationBody"
placeholder="Notification body text"
class="select-input textarea-input"
rows="3"
></textarea>
</SettingItem>
<SettingItem title="Actions">
<div class="button-group">
<button @click="testNotification" class="test-button primary">
Send Test Notification
</button>
<button @click="testUpdateNotification" class="test-button">
Test Update Notification
</button>
</div>
</SettingItem>
<div v-if="notificationStatus" class="test-status" :class="notificationStatus.type">
{{ notificationStatus.message }}
</div>
</SettingSection>
<!-- 清除所有测试状态 -->
<SettingSection title="Cleanup">
<SettingItem title="Clear All">
<button @click="clearAll" class="test-button danger">
Clear All Test States
</button>
</SettingItem>
<div v-if="clearStatus" class="test-status" :class="clearStatus.type">
{{ clearStatus.message }}
</div>
</SettingSection>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import * as TestService from '@/../bindings/voidraft/internal/services/testservice'
import SettingSection from '../components/SettingSection.vue'
import SettingItem from '../components/SettingItem.vue'
// Badge测试状态
const badgeText = ref('')
const badgeStatus = ref<{ type: string; message: string } | null>(null)
// 通知测试状态
const notificationTitle = ref('')
const notificationSubtitle = ref('')
const notificationBody = ref('')
const notificationStatus = ref<{ type: string; message: string } | null>(null)
// 清除状态
const clearStatus = ref<{ type: string; message: string } | null>(null)
// 显示状态消息的辅助函数
const showStatus = (statusRef: any, type: 'success' | 'error', message: string) => {
statusRef.value = { type, message }
setTimeout(() => {
statusRef.value = null
}, 5000)
}
// 测试Badge功能
const testBadge = async () => {
try {
await TestService.TestBadge(badgeText.value)
showStatus(badgeStatus, 'success', `Badge ${badgeText.value ? 'set to: ' + badgeText.value : 'cleared'} successfully`)
} catch (error: any) {
showStatus(badgeStatus, 'error', `Failed to set badge: ${error.message || error}`)
}
}
// 清除Badge
const clearBadge = async () => {
try {
await TestService.TestBadge('')
badgeText.value = ''
showStatus(badgeStatus, 'success', 'Badge cleared successfully')
} catch (error: any) {
showStatus(badgeStatus, 'error', `Failed to clear badge: ${error.message || error}`)
}
}
// 测试通知功能
const testNotification = async () => {
try {
await TestService.TestNotification(
notificationTitle.value,
notificationSubtitle.value,
notificationBody.value
)
showStatus(notificationStatus, 'success', 'Notification sent successfully')
} catch (error: any) {
showStatus(notificationStatus, 'error', `Failed to send notification: ${error.message || error}`)
}
}
// 测试更新通知
const testUpdateNotification = async () => {
try {
await TestService.TestUpdateNotification()
showStatus(notificationStatus, 'success', 'Update notification sent successfully (badge + notification)')
} catch (error: any) {
showStatus(notificationStatus, 'error', `Failed to send update notification: ${error.message || error}`)
}
}
// 清除所有测试状态
const clearAll = async () => {
try {
await TestService.ClearAll()
// 清空表单
badgeText.value = ''
notificationTitle.value = ''
notificationSubtitle.value = ''
notificationBody.value = ''
showStatus(clearStatus, 'success', 'All test states cleared successfully')
} catch (error: any) {
showStatus(clearStatus, 'error', `Failed to clear test states: ${error.message || error}`)
}
}
</script>
<style scoped lang="scss">
.settings-page {
padding: 20px 0 20px 0;
}
.dev-description {
color: var(--settings-text-secondary);
font-size: 12px;
line-height: 1.4;
padding: 8px 0;
}
.select-input {
padding: 6px 8px;
border: 1px solid var(--settings-input-border);
border-radius: 4px;
background-color: var(--settings-input-bg);
color: var(--settings-text);
font-size: 12px;
width: 180px;
&:focus {
outline: none;
border-color: #4a9eff;
box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.2);
}
&.textarea-input {
min-height: 60px;
resize: vertical;
font-family: inherit;
line-height: 1.4;
}
}
.button-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.test-button {
padding: 6px 12px;
border: 1px solid var(--settings-border);
border-radius: 4px;
background-color: var(--settings-card-bg);
color: var(--settings-text);
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
&:hover {
background-color: var(--settings-hover);
}
&.primary {
background-color: #4a9eff;
color: white;
border-color: #4a9eff;
&:hover {
background-color: #3a8eef;
border-color: #3a8eef;
}
}
&.danger {
background-color: var(--text-danger);
color: white;
border-color: var(--text-danger);
&:hover {
opacity: 0.9;
}
}
}
.test-status {
margin-top: 12px;
padding: 8px 12px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
border: 1px solid;
&.success {
background-color: rgba(34, 197, 94, 0.1);
color: #16a34a;
border-color: rgba(34, 197, 94, 0.2);
}
&.error {
background-color: rgba(239, 68, 68, 0.1);
color: #dc2626;
border-color: rgba(239, 68, 68, 0.2);
}
}
</style>

9
go.mod
View File

@@ -3,7 +3,6 @@ module voidraft
go 1.24.4 go 1.24.4
require ( require (
github.com/Masterminds/semver/v3 v3.4.0
github.com/creativeprojects/go-selfupdate v1.5.0 github.com/creativeprojects/go-selfupdate v1.5.0
github.com/go-git/go-git/v5 v5.16.2 github.com/go-git/go-git/v5 v5.16.2
github.com/knadh/koanf/parsers/json v1.0.0 github.com/knadh/koanf/parsers/json v1.0.0
@@ -11,6 +10,7 @@ require (
github.com/knadh/koanf/providers/structs v1.0.0 github.com/knadh/koanf/providers/structs v1.0.0
github.com/knadh/koanf/v2 v2.2.2 github.com/knadh/koanf/v2 v2.2.2
github.com/robertkrimen/otto v0.5.1 github.com/robertkrimen/otto v0.5.1
github.com/stretchr/testify v1.10.0
github.com/wailsapp/wails/v3 v3.0.0-alpha.25 github.com/wailsapp/wails/v3 v3.0.0-alpha.25
golang.org/x/net v0.43.0 golang.org/x/net v0.43.0
golang.org/x/sys v0.35.0 golang.org/x/sys v0.35.0
@@ -21,13 +21,16 @@ require (
require ( require (
code.gitea.io/sdk/gitea v0.21.0 // indirect code.gitea.io/sdk/gitea v0.21.0 // 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
github.com/42wim/httpsig v1.2.3 // indirect github.com/42wim/httpsig v1.2.3 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/adrg/xdg v0.5.3 // indirect github.com/adrg/xdg v0.5.3 // indirect
github.com/bep/debounce v1.2.1 // indirect github.com/bep/debounce v1.2.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect github.com/ebitengine/purego v0.8.4 // indirect
@@ -62,18 +65,20 @@ require (
github.com/pjbgf/sha1cd v0.4.0 // indirect github.com/pjbgf/sha1cd v0.4.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/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.51.0 // indirect github.com/samber/lo v1.51.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect github.com/skeema/knownhosts v1.3.1 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect github.com/ulikunitz/xz v0.5.14 // indirect
github.com/wailsapp/go-webview2 v1.0.21 // indirect github.com/wailsapp/go-webview2 v1.0.21 // indirect
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
golang.org/x/crypto v0.41.0 // indirect golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/time v0.12.0 // indirect golang.org/x/time v0.12.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect

8
go.sum
View File

@@ -2,6 +2,8 @@ code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA= code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM= github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
@@ -158,8 +160,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA= github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA=
github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
@@ -178,6 +180,8 @@ golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
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=

View File

@@ -4,6 +4,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
"voidraft/internal/version"
) )
// TabType 定义了制表符类型 // TabType 定义了制表符类型
@@ -68,6 +69,9 @@ type GeneralConfig struct {
EnableSystemTray bool `json:"enableSystemTray"` // 是否启用系统托盘 EnableSystemTray bool `json:"enableSystemTray"` // 是否启用系统托盘
StartAtLogin bool `json:"startAtLogin"` // 开机启动设置 StartAtLogin bool `json:"startAtLogin"` // 开机启动设置
// 窗口吸附设置
EnableWindowSnap bool `json:"enableWindowSnap"` // 是否启用窗口吸附功能(阈值现在是自适应的)
// 全局热键设置 // 全局热键设置
EnableGlobalHotkey bool `json:"enableGlobalHotkey"` // 是否启用全局热键 EnableGlobalHotkey bool `json:"enableGlobalHotkey"` // 是否启用全局热键
GlobalHotkey HotkeyCombo `json:"globalHotkey"` // 全局热键组合 GlobalHotkey HotkeyCombo `json:"globalHotkey"` // 全局热键组合
@@ -145,6 +149,7 @@ func NewDefaultAppConfig() *AppConfig {
DataPath: dataDir, DataPath: dataDir,
EnableSystemTray: true, EnableSystemTray: true,
StartAtLogin: false, StartAtLogin: false,
EnableWindowSnap: true, // 默认启用窗口吸附
EnableGlobalHotkey: false, EnableGlobalHotkey: false,
GlobalHotkey: HotkeyCombo{ GlobalHotkey: HotkeyCombo{
Ctrl: false, Ctrl: false,
@@ -172,7 +177,7 @@ func NewDefaultAppConfig() *AppConfig {
SystemTheme: SystemThemeAuto, SystemTheme: SystemThemeAuto,
}, },
Updates: UpdatesConfig{ Updates: UpdatesConfig{
Version: "1.3.0", Version: version.Version,
AutoUpdate: true, AutoUpdate: true,
PrimarySource: UpdateSourceGitea, PrimarySource: UpdateSourceGitea,
BackupSource: UpdateSourceGithub, BackupSource: UpdateSourceGithub,
@@ -201,7 +206,7 @@ func NewDefaultAppConfig() *AppConfig {
}, },
Metadata: ConfigMetadata{ Metadata: ConfigMetadata{
LastUpdated: time.Now().Format(time.RFC3339), LastUpdated: time.Now().Format(time.RFC3339),
Version: "1.2.0", Version: version.Version,
}, },
} }
} }

41
internal/models/window.go Normal file
View File

@@ -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"` // 上次移动时间,用于判断移动速度
}

View File

@@ -263,7 +263,7 @@ func (s *BackupService) PushToRemote() error {
// 创建提交 // 创建提交
_, err = w.Commit(fmt.Sprintf("Backup %s", time.Now().Format("2006-01-02 15:04:05")), &git.CommitOptions{ _, err = w.Commit(fmt.Sprintf("Backup %s", time.Now().Format("2006-01-02 15:04:05")), &git.CommitOptions{
Author: &object.Signature{ Author: &object.Signature{
Name: "VoidRaft", Name: "voidraft",
Email: "backup@voidraft.app", Email: "backup@voidraft.app",
When: time.Now(), When: time.Now(),
}, },

View File

@@ -1,318 +1,200 @@
package services package services
import ( import (
"encoding/json"
"fmt" "fmt"
"os"
"path/filepath"
"reflect"
"sort"
"time"
"voidraft/internal/models"
"github.com/Masterminds/semver/v3"
jsonparser "github.com/knadh/koanf/parsers/json" jsonparser "github.com/knadh/koanf/parsers/json"
"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/services/log" "github.com/wailsapp/wails/v3/pkg/services/log"
"os"
"path/filepath"
"time"
) )
const ( const (
// CurrentAppConfigVersion 当前应用配置版本 // BackupFilePattern backup file name pattern
CurrentAppConfigVersion = "1.3.0"
// BackupFilePattern 备份文件名模式
BackupFilePattern = "%s.backup.%s.json" BackupFilePattern = "%s.backup.%s.json"
// MaxConfigFileSize maximum config file size
// 资源限制常量
MaxConfigFileSize = 10 * 1024 * 1024 // 10MB MaxConfigFileSize = 10 * 1024 * 1024 // 10MB
MaxRecursionDepth = 50 // 最大递归深度
) )
// Migratable 可迁移的配置接口 // ConfigMigrator elegant configuration migrator with automatic field detection
type Migratable interface { type ConfigMigrator struct {
GetVersion() string // 获取当前版本 logger *log.LogService
SetVersion(string) // 设置版本 configDir string
SetLastUpdated(string) // 设置最后更新时间 configName string
GetDefaultConfig() any // 获取默认配置 configPath string
} }
// ConfigMigrationService 配置迁移服务 // MigrationResult migration operation result
type ConfigMigrationService[T Migratable] struct {
logger *log.LogService
configDir string
configName string
targetVersion string
configPath string
}
// MigrationResult 迁移结果
type MigrationResult struct { type MigrationResult struct {
Migrated, ConfigUpdated bool Migrated bool `json:"migrated"` // Whether migration was performed
FromVersion, ToVersion string MissingFields []string `json:"missingFields"` // Fields that were missing
BackupPath string BackupPath string `json:"backupPath"` // Path to backup file
Description string `json:"description"` // Description of migration
} }
// NewConfigMigrationService 创建配置迁移服务 // NewConfigMigrator creates a new configuration migrator
func NewConfigMigrationService[T Migratable]( func NewConfigMigrator(
logger *log.LogService, logger *log.LogService,
configDir string, configDir string,
configName, targetVersion, configPath string, configName, configPath string,
) *ConfigMigrationService[T] { ) *ConfigMigrator {
return &ConfigMigrationService[T]{ if logger == nil {
logger: orDefault(logger, log.New()), logger = log.New()
configDir: configDir, }
configName: configName, return &ConfigMigrator{
targetVersion: targetVersion, logger: logger,
configPath: configPath, configDir: configDir,
configName: configName,
configPath: configPath,
} }
} }
// MigrateConfig 迁移配置文件 // AutoMigrate automatically detects and migrates missing configuration fields
func (cms *ConfigMigrationService[T]) MigrateConfig(existingConfig *koanf.Koanf) (*MigrationResult, error) { func (cm *ConfigMigrator) AutoMigrate(defaultConfig interface{}, currentConfig *koanf.Koanf) (*MigrationResult, error) {
currentVersion := orDefault(existingConfig.String("metadata.version"), "0.0.0") defaultKoanf := koanf.New(".")
result := &MigrationResult{ if err := defaultKoanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
FromVersion: currentVersion, return nil, fmt.Errorf("failed to load default config: %w", err)
ToVersion: cms.targetVersion,
} }
if needsMigration, err := cms.needsMigration(currentVersion); err != nil { // Detect missing fields
return result, fmt.Errorf("version comparison failed: %w", err) missingFields := cm.detectMissingFields(currentConfig.All(), defaultKoanf.All())
} else if !needsMigration {
// Create result object
result := &MigrationResult{
MissingFields: missingFields,
Migrated: len(missingFields) > 0,
Description: fmt.Sprintf("Found %d missing fields", len(missingFields)),
}
// No migration needed
if !result.Migrated {
return result, nil return result, nil
} }
// 资源检查和备份 // Create backup before migration
if err := cms.checkResourceLimits(); err != nil { backupPath, err := cm.createBackup()
return result, fmt.Errorf("resource limit check failed: %w", err) if err != nil {
}
if backupPath, err := cms.createBackupOptimized(); err != nil {
return result, fmt.Errorf("backup creation failed: %w", err) return result, fmt.Errorf("backup creation failed: %w", err)
} else { }
result.BackupPath = backupPath result.BackupPath = backupPath
// Merge missing fields from default config
if err := cm.mergeDefaultFields(currentConfig, defaultKoanf, missingFields); err != nil {
return result, fmt.Errorf("failed to merge default fields: %w", err)
} }
// 自动恢复检查 // Save updated config
cms.tryQuickRecovery(existingConfig) if err := cm.saveConfig(currentConfig); err != nil {
return result, fmt.Errorf("failed to save updated config: %w", err)
}
// 执行迁移 // Clean up backup on success
if configUpdated, err := cms.performOptimizedMigration(existingConfig); err != nil { if backupPath != "" {
return result, fmt.Errorf("migration failed: %w", err) if err := os.Remove(backupPath); err != nil {
} else { cm.logger.Error("Failed to remove backup", "error", err)
result.Migrated = true }
result.ConfigUpdated = configUpdated
} }
return result, nil return result, nil
} }
// needsMigration 检查是否需要迁移 // detectMissingFields detects missing configuration fields
func (cms *ConfigMigrationService[T]) needsMigration(current string) (bool, error) { func (cm *ConfigMigrator) detectMissingFields(current, defaultConfig map[string]interface{}) []string {
currentVer, err := semver.NewVersion(current) var missing []string
if err != nil { cm.findMissing("", defaultConfig, current, &missing)
return true, nil return missing
}
targetVer, err := semver.NewVersion(cms.targetVersion)
if err != nil {
return false, fmt.Errorf("invalid target version: %s", cms.targetVersion)
}
return currentVer.LessThan(targetVer), nil
} }
// checkResourceLimits 检查资源限制 // findMissing recursively finds missing fields
func (cms *ConfigMigrationService[T]) checkResourceLimits() error { func (cm *ConfigMigrator) findMissing(prefix string, defaultMap, currentMap map[string]interface{}, missing *[]string) {
if info, err := os.Stat(cms.configPath); err == nil && info.Size() > MaxConfigFileSize { for key, defaultVal := range defaultMap {
return fmt.Errorf("config file size (%d bytes) exceeds limit (%d bytes)", info.Size(), MaxConfigFileSize) fullKey := key
if prefix != "" {
fullKey = prefix + "." + key
}
currentVal, exists := currentMap[key]
if !exists {
// Field completely missing - add it
*missing = append(*missing, fullKey)
} else if defaultNestedMap, ok := defaultVal.(map[string]interface{}); ok {
if currentNestedMap, ok := currentVal.(map[string]interface{}); ok {
// Both are maps, recurse into them
cm.findMissing(fullKey, defaultNestedMap, currentNestedMap, missing)
}
// Type mismatch: user has different type, don't recurse
}
// For non-map default values, field exists, preserve user's value
} }
}
// mergeDefaultFields merges default values for missing fields into current config
func (cm *ConfigMigrator) mergeDefaultFields(current, defaultConfig *koanf.Koanf, missingFields []string) error {
actuallyMerged := 0
for _, field := range missingFields {
if defaultConfig.Exists(field) {
if defaultValue := defaultConfig.Get(field); defaultValue != nil {
// Always set the field, even if it causes type conflicts
// This allows configuration structure evolution during upgrades
current.Set(field, defaultValue)
actuallyMerged++
}
}
}
// Update timestamp if we actually merged fields
if actuallyMerged > 0 {
current.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339))
}
return nil return nil
} }
// createBackupOptimized 优化的备份创建 // createBackup creates a backup of the configuration file
func (cms *ConfigMigrationService[T]) createBackupOptimized() (string, error) { func (cm *ConfigMigrator) createBackup() (string, error) {
if _, err := os.Stat(cms.configPath); os.IsNotExist(err) { if _, err := os.Stat(cm.configPath); os.IsNotExist(err) {
return "", nil return "", nil
} }
configDir := cms.configDir
timestamp := time.Now().Format("20060102150405") timestamp := time.Now().Format("20060102150405")
newBackupPath := filepath.Join(configDir, fmt.Sprintf(BackupFilePattern, cms.configName, timestamp)) backupPath := filepath.Join(cm.configDir, fmt.Sprintf(BackupFilePattern, cm.configName, timestamp))
// 单次扫描:删除旧备份并创建新备份 data, err := os.ReadFile(cm.configPath)
pattern := filepath.Join(configDir, fmt.Sprintf("%s.backup.*.json", cms.configName))
if matches, err := filepath.Glob(pattern); err == nil {
for _, oldBackup := range matches {
if oldBackup != newBackupPath {
os.Remove(oldBackup) // 忽略删除错误,继续处理
}
}
}
return newBackupPath, copyFile(cms.configPath, newBackupPath)
}
// tryQuickRecovery 快速恢复检查
func (cms *ConfigMigrationService[T]) tryQuickRecovery(existingConfig *koanf.Koanf) {
var testConfig T
if existingConfig.Unmarshal("", &testConfig) != nil {
cms.logger.Info("Config appears corrupted, attempting quick recovery")
if backupPath := cms.findLatestBackupQuick(); backupPath != "" {
if data, err := os.ReadFile(backupPath); err == nil {
existingConfig.Delete("")
existingConfig.Load(&BytesProvider{data}, jsonparser.Parser())
cms.logger.Info("Quick recovery successful")
}
}
}
}
// findLatestBackupQuick 快速查找最新备份(优化排序)
func (cms *ConfigMigrationService[T]) findLatestBackupQuick() string {
pattern := filepath.Join(cms.configDir, fmt.Sprintf("%s.backup.*.json", cms.configName))
matches, err := filepath.Glob(pattern)
if err != nil || len(matches) == 0 {
return ""
}
sort.Strings(matches) // 字典序排序,时间戳格式确保正确性
return matches[len(matches)-1]
}
// performOptimizedMigration 优化的迁移执行
func (cms *ConfigMigrationService[T]) performOptimizedMigration(existingConfig *koanf.Koanf) (bool, error) {
// 直接从koanf实例获取配置避免额外序列化
var currentConfig T
if err := existingConfig.Unmarshal("", &currentConfig); err != nil {
return false, fmt.Errorf("unmarshal existing config failed: %w", err)
}
defaultConfig, ok := currentConfig.GetDefaultConfig().(T)
if !ok {
return false, fmt.Errorf("default config type mismatch")
}
return cms.mergeInPlace(existingConfig, currentConfig, defaultConfig)
}
// mergeInPlace 就地合并配置
func (cms *ConfigMigrationService[T]) mergeInPlace(existingConfig *koanf.Koanf, currentConfig, defaultConfig T) (bool, error) {
// 创建临时合并实例
mergeKoanf := koanf.New(".")
// 使用快速加载链
if err := chainLoad(mergeKoanf,
func() error { return mergeKoanf.Load(structs.Provider(defaultConfig, "json"), nil) },
func() error {
return mergeKoanf.Load(structs.Provider(currentConfig, "json"), nil,
koanf.WithMergeFunc(cms.fastMerge))
},
); err != nil {
return false, fmt.Errorf("config merge failed: %w", err)
}
// 更新元数据
mergeKoanf.Set("metadata.version", cms.targetVersion)
mergeKoanf.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339))
// 一次性序列化和原子写入
configBytes, err := mergeKoanf.Marshal(jsonparser.Parser())
if err != nil { if err != nil {
return false, fmt.Errorf("marshal config failed: %w", err) return "", fmt.Errorf("failed to read config file: %w", err)
}
if err := os.WriteFile(backupPath, data, 0644); err != nil {
return "", fmt.Errorf("failed to create backup: %w", err)
}
return backupPath, nil
}
// saveConfig saves configuration to file
func (cm *ConfigMigrator) saveConfig(config *koanf.Koanf) error {
configBytes, err := config.Marshal(jsonparser.Parser())
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
} }
if len(configBytes) > MaxConfigFileSize { if len(configBytes) > MaxConfigFileSize {
return false, fmt.Errorf("merged config size exceeds limit") return fmt.Errorf("config size (%d bytes) exceeds limit (%d bytes)", len(configBytes), MaxConfigFileSize)
} }
// 原子写入 // Atomic write
return true, cms.atomicWrite(existingConfig, configBytes) tempPath := cm.configPath + ".tmp"
}
// atomicWrite 原子写入操作
func (cms *ConfigMigrationService[T]) atomicWrite(existingConfig *koanf.Koanf, configBytes []byte) error {
tempPath := cms.configPath + ".tmp"
if err := os.WriteFile(tempPath, configBytes, 0644); err != nil { if err := os.WriteFile(tempPath, configBytes, 0644); err != nil {
return fmt.Errorf("write temp config failed: %w", err) return fmt.Errorf("failed to write temp config: %w", err)
} }
if err := os.Rename(tempPath, cms.configPath); err != nil { if err := os.Rename(tempPath, cm.configPath); err != nil {
os.Remove(tempPath) os.Remove(tempPath)
return fmt.Errorf("atomic rename failed: %w", err) return fmt.Errorf("failed to rename temp config: %w", err)
} }
// 重新加载到原实例
existingConfig.Delete("")
return existingConfig.Load(&BytesProvider{configBytes}, jsonparser.Parser())
}
// fastMerge 快速合并函数
func (cms *ConfigMigrationService[T]) fastMerge(src, dest map[string]interface{}) error {
return cms.fastMergeRecursive(src, dest, 0)
}
// fastMergeRecursive 快速递归合并
func (cms *ConfigMigrationService[T]) fastMergeRecursive(src, dest map[string]interface{}, depth int) error {
if depth > MaxRecursionDepth {
return fmt.Errorf("recursion depth exceeded")
}
for key, srcVal := range src {
if destVal, exists := dest[key]; exists {
// 优先检查map类型最常见情况
if srcMap, srcOK := srcVal.(map[string]interface{}); srcOK {
if destMap, destOK := destVal.(map[string]interface{}); destOK {
if err := cms.fastMergeRecursive(srcMap, destMap, depth+1); err != nil {
return err
}
continue
}
}
// 快速空值检查(避免反射)
if srcVal == nil || srcVal == "" || srcVal == 0 {
continue
}
}
dest[key] = srcVal
}
return nil return nil
} }
// BytesProvider 轻量字节提供器
type BytesProvider struct{ data []byte }
func (bp *BytesProvider) ReadBytes() ([]byte, error) { return bp.data, nil }
func (bp *BytesProvider) Read() (map[string]interface{}, error) {
var result map[string]interface{}
return result, json.Unmarshal(bp.data, &result)
}
// 工具函数
func orDefault[T any](value, defaultValue T) T {
var zero T
if reflect.DeepEqual(value, zero) {
return defaultValue
}
return value
}
func copyFile(src, dst string) error {
data, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dst, data, 0644)
}
func chainLoad(k *koanf.Koanf, loaders ...func() error) error {
for _, loader := range loaders {
if err := loader(); err != nil {
return err
}
}
return nil
}
// 工厂函数
func NewAppConfigMigrationService(logger *log.LogService, configDir, settingsPath string) *ConfigMigrationService[*models.AppConfig] {
return NewConfigMigrationService[*models.AppConfig](
logger, configDir, "settings", CurrentAppConfigVersion, settingsPath)
}

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,8 @@ const (
ConfigChangeTypeDataPath ConfigChangeType = "datapath" ConfigChangeTypeDataPath ConfigChangeType = "datapath"
// ConfigChangeTypeBackup 备份配置变更 // ConfigChangeTypeBackup 备份配置变更
ConfigChangeTypeBackup ConfigChangeType = "backup" ConfigChangeTypeBackup ConfigChangeType = "backup"
// ConfigChangeTypeWindowSnap 窗口吸附配置变更
ConfigChangeTypeWindowSnap ConfigChangeType = "windowsnap"
) )
// ConfigChangeCallback 配置变更回调函数类型 // ConfigChangeCallback 配置变更回调函数类型
@@ -286,25 +288,6 @@ func deepCopyValue(src, dst reflect.Value) {
} }
} }
// deepCopyConfig 保留原有的JSON深拷贝方法作为备用
func deepCopyConfig(src *models.AppConfig) *models.AppConfig {
if src == nil {
return nil
}
jsonBytes, err := json.Marshal(src)
if err != nil {
return src
}
var dst models.AppConfig
if err := json.Unmarshal(jsonBytes, &dst); err != nil {
return src
}
return &dst
}
// debounceNotify 防抖通知 // debounceNotify 防抖通知
func (cns *ConfigNotificationService) debounceNotify(listener *ConfigListener, oldConfig, newConfig *models.AppConfig) { func (cns *ConfigNotificationService) debounceNotify(listener *ConfigListener, oldConfig, newConfig *models.AppConfig) {
listener.mu.Lock() listener.mu.Lock()
@@ -470,6 +453,29 @@ func CreateBackupConfigListener(name string, callback func(config *models.GitBac
} }
} }
// CreateWindowSnapConfigListener 创建窗口吸附配置监听器
func CreateWindowSnapConfigListener(name string, callback func(enabled bool) 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)
}
return callback(newConfig.General.EnableWindowSnap)
},
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 关闭服务 // ServiceShutdown 关闭服务
func (cns *ConfigNotificationService) ServiceShutdown() error { func (cns *ConfigNotificationService) ServiceShutdown() error {
cns.Cleanup() cns.Cleanup()

View File

@@ -27,8 +27,9 @@ type ConfigService struct {
// 配置通知服务 // 配置通知服务
notificationService *ConfigNotificationService notificationService *ConfigNotificationService
// 配置迁移服务
migrationService *ConfigMigrationService[*models.AppConfig] // 配置迁移器
configMigrator *ConfigMigrator
} }
// ConfigError 配置错误 // ConfigError 配置错误
@@ -67,16 +68,18 @@ func NewConfigService(logger *log.LogService) *ConfigService {
settingsPath := filepath.Join(configDir, "settings.json") settingsPath := filepath.Join(configDir, "settings.json")
cs := &ConfigService{ cs := &ConfigService{
logger: logger, logger: logger,
configDir: configDir, configDir: configDir,
settingsPath: settingsPath, settingsPath: settingsPath,
koanf: koanf.New("."), koanf: koanf.New("."),
migrationService: NewAppConfigMigrationService(logger, configDir, settingsPath),
} }
// 初始化配置通知服务 // 初始化配置通知服务
cs.notificationService = NewConfigNotificationService(cs.koanf, logger) cs.notificationService = NewConfigNotificationService(cs.koanf, logger)
// 初始化配置迁移器
cs.configMigrator = NewConfigMigrator(logger, configDir, "settings", settingsPath)
cs.initConfig() cs.initConfig()
// 启动配置文件监听 // 启动配置文件监听
@@ -106,23 +109,33 @@ func (cs *ConfigService) initConfig() error {
return cs.createDefaultConfig() return cs.createDefaultConfig()
} }
// 配置文件存在,加载现有配置 // 配置文件存在,直接加载现有配置
cs.fileProvider = file.Provider(cs.settingsPath) cs.fileProvider = file.Provider(cs.settingsPath)
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil { if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
return &ConfigError{Operation: "load_config_file", Err: err} return &ConfigError{Operation: "load_config_file", Err: err}
} }
// 检查并执行配置迁移 return nil
if cs.migrationService != nil { }
result, err := cs.migrationService.MigrateConfig(cs.koanf)
if err != nil {
return &ConfigError{Operation: "migrate_config", Err: err}
}
if result.Migrated && result.ConfigUpdated { // MigrateConfig 执行配置迁移
// 迁移完成且配置已更新,重新创建文件提供器以监听新文件 func (cs *ConfigService) MigrateConfig() error {
cs.fileProvider = file.Provider(cs.settingsPath) if cs.configMigrator == nil {
} return nil
}
defaultConfig := models.NewDefaultAppConfig()
result, err := cs.configMigrator.AutoMigrate(defaultConfig, cs.koanf)
if err != nil {
cs.logger.Error("Failed to check config migration", "error", err)
return err
}
if result != nil && result.Migrated {
cs.logger.Info("Config migration performed",
"fields", result.MissingFields,
"backup", result.BackupPath)
} }
return nil return nil
@@ -308,6 +321,16 @@ func (cs *ConfigService) SetBackupConfigChangeCallback(callback func(config *mod
return cs.notificationService.RegisterListener(backupListener) return cs.notificationService.RegisterListener(backupListener)
} }
// SetWindowSnapConfigChangeCallback 设置窗口吸附配置变更回调
func (cs *ConfigService) SetWindowSnapConfigChangeCallback(callback func(enabled bool) error) error {
cs.mu.Lock()
defer cs.mu.Unlock()
// 创建窗口吸附配置监听器并注册
windowSnapListener := CreateWindowSnapConfigListener("DefaultWindowSnapConfigListener", callback)
return cs.notificationService.RegisterListener(windowSnapListener)
}
// ServiceShutdown 关闭服务 // ServiceShutdown 关闭服务
func (cs *ConfigService) ServiceShutdown() error { func (cs *ConfigService) ServiceShutdown() error {
cs.stopWatching() cs.stopWatching()
@@ -316,3 +339,13 @@ func (cs *ConfigService) ServiceShutdown() error {
} }
return nil return nil
} }
// GetConfigDir 获取配置目录
func (cs *ConfigService) GetConfigDir() string {
return cs.configDir
}
// GetSettingsPath 获取设置文件路径
func (cs *ConfigService) GetSettingsPath() string {
return cs.settingsPath
}

View File

@@ -6,7 +6,7 @@ package services
#cgo CFLAGS: -I../lib #cgo CFLAGS: -I../lib
#cgo LDFLAGS: -luser32 #cgo LDFLAGS: -luser32
#include "../lib/hotkey_windows.c" #include "../lib/hotkey_windows.c"
#include "hotkey_windows.h" #include "../lib/hotkey_windows.h"
*/ */
import "C" import "C"

View File

@@ -5,7 +5,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/creativeprojects/go-selfupdate" "github.com/creativeprojects/go-selfupdate"
"github.com/wailsapp/wails/v3/pkg/services/badge"
"github.com/wailsapp/wails/v3/pkg/services/log" "github.com/wailsapp/wails/v3/pkg/services/log"
"github.com/wailsapp/wails/v3/pkg/services/notifications"
"os" "os"
"runtime" "runtime"
"time" "time"
@@ -26,16 +28,18 @@ type SelfUpdateResult struct {
// SelfUpdateService 自我更新服务 // SelfUpdateService 自我更新服务
type SelfUpdateService struct { type SelfUpdateService struct {
logger *log.LogService logger *log.LogService
configService *ConfigService configService *ConfigService
config *models.AppConfig badgeService *badge.BadgeService // 直接使用Wails原生badge服务
notificationService *notifications.NotificationService // 通知服务
config *models.AppConfig
// 状态管理 // 状态管理
isUpdating bool isUpdating bool
} }
// NewSelfUpdateService 创建自我更新服务实例 // NewSelfUpdateService 创建自我更新服务实例
func NewSelfUpdateService(configService *ConfigService, logger *log.LogService) (*SelfUpdateService, error) { func NewSelfUpdateService(configService *ConfigService, badgeService *badge.BadgeService, notificationService *notifications.NotificationService, logger *log.LogService) (*SelfUpdateService, error) {
// 获取配置 // 获取配置
appConfig, err := configService.GetConfig() appConfig, err := configService.GetConfig()
if err != nil { if err != nil {
@@ -43,10 +47,12 @@ func NewSelfUpdateService(configService *ConfigService, logger *log.LogService)
} }
service := &SelfUpdateService{ service := &SelfUpdateService{
logger: logger, logger: logger,
configService: configService, configService: configService,
config: appConfig, badgeService: badgeService,
isUpdating: false, notificationService: notificationService,
config: appConfig,
isUpdating: false,
} }
return service, nil return service, nil
@@ -63,6 +69,7 @@ func (s *SelfUpdateService) CheckForUpdates(ctx context.Context) (*SelfUpdateRes
// 首先尝试主要更新源 // 首先尝试主要更新源
primaryResult, err := s.checkSourceForUpdates(ctx, s.config.Updates.PrimarySource) primaryResult, err := s.checkSourceForUpdates(ctx, s.config.Updates.PrimarySource)
if err == nil && primaryResult != nil { if err == nil && primaryResult != nil {
s.handleUpdateBadge(primaryResult)
return primaryResult, nil return primaryResult, nil
} }
@@ -71,9 +78,12 @@ func (s *SelfUpdateService) CheckForUpdates(ctx context.Context) (*SelfUpdateRes
if backupErr != nil { if backupErr != nil {
// 如果备用源也失败,返回主要源的错误信息 // 如果备用源也失败,返回主要源的错误信息
result.Error = fmt.Sprintf("Primary source error: %v; Backup source error: %v", err, backupErr) result.Error = fmt.Sprintf("Primary source error: %v; Backup source error: %v", err, backupErr)
// 确保在检查失败时也调用handleUpdateBadge来清除可能存在的badge
s.handleUpdateBadge(result)
return result, errors.New(result.Error) return result, errors.New(result.Error)
} }
s.handleUpdateBadge(backupResult)
return backupResult, nil return backupResult, nil
} }
@@ -251,7 +261,6 @@ func (s *SelfUpdateService) ApplyUpdate(ctx context.Context) (*SelfUpdateResult,
// 检查是否有可用更新 // 检查是否有可用更新
if !primaryRelease.GreaterThan(s.config.Updates.Version) { if !primaryRelease.GreaterThan(s.config.Updates.Version) {
s.logger.Info("Current version is up to date, no need to apply update")
result.LatestVersion = primaryRelease.Version() result.LatestVersion = primaryRelease.Version()
return result, nil return result, nil
} }
@@ -310,6 +319,18 @@ func (s *SelfUpdateService) ApplyUpdate(ctx context.Context) (*SelfUpdateResult,
s.logger.Error("Failed to update config version", "error", err) s.logger.Error("Failed to update config version", "error", err)
} }
// 执行配置迁移
if err := s.configService.MigrateConfig(); err != nil {
s.logger.Error("Failed to migrate config after update", "error", err)
}
// 更新成功移除badge
if s.badgeService != nil {
if err := s.badgeService.RemoveBadge(); err != nil {
s.logger.Error("failed to remove update badge after successful update", "error", err)
}
}
return result, nil return result, nil
} }
@@ -365,7 +386,6 @@ func (s *SelfUpdateService) updateFromSource(ctx context.Context, sourceType mod
} }
// 尝试下载并应用更新,不设置超时 // 尝试下载并应用更新,不设置超时
s.logger.Info("Downloading update...", "source", sourceType)
err = updater.UpdateTo(ctx, release, exe) err = updater.UpdateTo(ctx, release, exe)
if err != nil { if err != nil {
@@ -392,6 +412,13 @@ func (s *SelfUpdateService) updateFromSource(ctx context.Context, sourceType mod
s.logger.Error("Failed to update config version", "error", err) s.logger.Error("Failed to update config version", "error", err)
} }
// 更新成功移除badge
if s.badgeService != nil {
if err := s.badgeService.RemoveBadge(); err != nil {
s.logger.Error("failed to remove update badge after successful update", "error", err)
}
}
return result, nil return result, nil
} }
@@ -462,3 +489,63 @@ func (s *SelfUpdateService) cleanupBackup(backupPath string) error {
} }
return nil return nil
} }
// handleUpdateBadge 处理更新通知badge和通知
func (s *SelfUpdateService) handleUpdateBadge(result *SelfUpdateResult) {
if result != nil && result.HasUpdate {
// 有更新时显示更新badge
if s.badgeService != nil {
if err := s.badgeService.SetBadge("●"); err != nil {
s.logger.Error("failed to set update badge", "error", err)
}
}
// 发送简单通知
s.sendUpdateNotification(result)
} else {
// 没有更新或出错时移除badge
if s.badgeService != nil {
if err := s.badgeService.RemoveBadge(); err != nil {
s.logger.Error("failed to remove update badge", "error", err)
}
}
}
}
// sendUpdateNotification 发送更新通知
func (s *SelfUpdateService) sendUpdateNotification(result *SelfUpdateResult) {
if s.notificationService == nil {
return
}
// 检查通知授权macOS需要
authorized, err := s.notificationService.CheckNotificationAuthorization()
if err != nil {
s.logger.Error("Failed to check notification authorization", "error", err)
return
}
if !authorized {
authorized, err = s.notificationService.RequestNotificationAuthorization()
if err != nil || !authorized {
s.logger.Error("Failed to get notification authorization", "error", err)
return
}
}
// 构建简单通知内容
title := "Voidraft Update Available"
body := fmt.Sprintf("New version %s available (current: %s)", result.LatestVersion, result.CurrentVersion)
// 发送简单通知
err = s.notificationService.SendNotification(notifications.NotificationOptions{
ID: "update_available",
Title: title,
Subtitle: "New version available",
Body: body,
})
if err != nil {
s.logger.Error("Failed to send notification", "error", err)
return
}
}

View File

@@ -4,28 +4,34 @@ import (
"voidraft/internal/models" "voidraft/internal/models"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/services/badge"
"github.com/wailsapp/wails/v3/pkg/services/log" "github.com/wailsapp/wails/v3/pkg/services/log"
"github.com/wailsapp/wails/v3/pkg/services/notifications"
) )
// ServiceManager 服务管理器,负责协调各个服务 // ServiceManager 服务管理器,负责协调各个服务
type ServiceManager struct { type ServiceManager struct {
configService *ConfigService configService *ConfigService
databaseService *DatabaseService databaseService *DatabaseService
documentService *DocumentService documentService *DocumentService
windowService *WindowService windowService *WindowService
migrationService *MigrationService windowSnapService *WindowSnapService
systemService *SystemService migrationService *MigrationService
hotkeyService *HotkeyService systemService *SystemService
dialogService *DialogService hotkeyService *HotkeyService
trayService *TrayService dialogService *DialogService
keyBindingService *KeyBindingService trayService *TrayService
extensionService *ExtensionService keyBindingService *KeyBindingService
startupService *StartupService extensionService *ExtensionService
selfUpdateService *SelfUpdateService startupService *StartupService
translationService *TranslationService selfUpdateService *SelfUpdateService
themeService *ThemeService translationService *TranslationService
BackupService *BackupService themeService *ThemeService
logger *log.LogService badgeService *badge.BadgeService
notificationService *notifications.NotificationService
testService *TestService // 测试服务(仅开发环境)
BackupService *BackupService
logger *log.LogService
} }
// NewServiceManager 创建新的服务管理器实例 // NewServiceManager 创建新的服务管理器实例
@@ -33,6 +39,12 @@ func NewServiceManager() *ServiceManager {
// 初始化日志服务 // 初始化日志服务
logger := log.New() logger := log.New()
// 初始化badge服务
badgeService := badge.New()
// 初始化通知服务
notificationService := notifications.New()
// 初始化配置服务 // 初始化配置服务
configService := NewConfigService(logger) configService := NewConfigService(logger)
@@ -48,6 +60,12 @@ func NewServiceManager() *ServiceManager {
// 初始化窗口服务 // 初始化窗口服务
windowService := NewWindowService(logger, documentService) windowService := NewWindowService(logger, documentService)
// 初始化窗口吸附服务
windowSnapService := NewWindowSnapService(logger, configService)
// 将吸附服务与窗口服务关联
windowService.SetWindowSnapService(windowSnapService)
// 初始化系统服务 // 初始化系统服务
systemService := NewSystemService(logger) systemService := NewSystemService(logger)
@@ -70,7 +88,7 @@ func NewServiceManager() *ServiceManager {
startupService := NewStartupService(configService, logger) startupService := NewStartupService(configService, logger)
// 初始化自我更新服务 // 初始化自我更新服务
selfUpdateService, err := NewSelfUpdateService(configService, logger) selfUpdateService, err := NewSelfUpdateService(configService, badgeService, notificationService, logger)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -84,6 +102,9 @@ func NewServiceManager() *ServiceManager {
// 初始化备份服务 // 初始化备份服务
backupService := NewBackupService(configService, databaseService, logger) backupService := NewBackupService(configService, databaseService, logger)
// 初始化测试服务(开发环境使用)
testService := NewTestService(badgeService, notificationService, logger)
// 使用新的配置通知系统设置热键配置变更监听 // 使用新的配置通知系统设置热键配置变更监听
err = configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error { err = configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error {
return hotkeyService.UpdateHotkey(enable, hotkey) return hotkeyService.UpdateHotkey(enable, hotkey)
@@ -108,24 +129,36 @@ func NewServiceManager() *ServiceManager {
panic(err) panic(err)
} }
// 设置窗口吸附配置变更回调
err = configService.SetWindowSnapConfigChangeCallback(func(enabled bool) error {
return windowSnapService.OnWindowSnapConfigChanged(enabled)
})
if err != nil {
panic(err)
}
return &ServiceManager{ return &ServiceManager{
configService: configService, configService: configService,
databaseService: databaseService, databaseService: databaseService,
documentService: documentService, documentService: documentService,
windowService: windowService, windowService: windowService,
migrationService: migrationService, windowSnapService: windowSnapService,
systemService: systemService, migrationService: migrationService,
hotkeyService: hotkeyService, systemService: systemService,
dialogService: dialogService, hotkeyService: hotkeyService,
trayService: trayService, dialogService: dialogService,
keyBindingService: keyBindingService, trayService: trayService,
extensionService: extensionService, keyBindingService: keyBindingService,
startupService: startupService, extensionService: extensionService,
selfUpdateService: selfUpdateService, startupService: startupService,
translationService: translationService, selfUpdateService: selfUpdateService,
themeService: themeService, translationService: translationService,
BackupService: backupService, themeService: themeService,
logger: logger, badgeService: badgeService,
notificationService: notificationService,
testService: testService,
BackupService: backupService,
logger: logger,
} }
} }
@@ -136,6 +169,7 @@ func (sm *ServiceManager) GetServices() []application.Service {
application.NewService(sm.databaseService), application.NewService(sm.databaseService),
application.NewService(sm.documentService), application.NewService(sm.documentService),
application.NewService(sm.windowService), application.NewService(sm.windowService),
application.NewService(sm.windowSnapService),
application.NewService(sm.keyBindingService), application.NewService(sm.keyBindingService),
application.NewService(sm.extensionService), application.NewService(sm.extensionService),
application.NewService(sm.migrationService), application.NewService(sm.migrationService),
@@ -147,6 +181,9 @@ func (sm *ServiceManager) GetServices() []application.Service {
application.NewService(sm.selfUpdateService), application.NewService(sm.selfUpdateService),
application.NewService(sm.translationService), application.NewService(sm.translationService),
application.NewService(sm.themeService), application.NewService(sm.themeService),
application.NewService(sm.badgeService),
application.NewService(sm.notificationService),
application.NewService(sm.testService), // 注册测试服务
application.NewService(sm.BackupService), application.NewService(sm.BackupService),
} }
return services return services
@@ -221,3 +258,18 @@ func (sm *ServiceManager) GetDocumentService() *DocumentService {
func (sm *ServiceManager) GetThemeService() *ThemeService { func (sm *ServiceManager) GetThemeService() *ThemeService {
return sm.themeService return sm.themeService
} }
// GetWindowSnapService 获取窗口吸附服务实例
func (sm *ServiceManager) GetWindowSnapService() *WindowSnapService {
return sm.windowSnapService
}
// GetBadgeService 获取badge服务实例
func (sm *ServiceManager) GetBadgeService() *badge.BadgeService {
return sm.badgeService
}
// GetNotificationService 获取通知服务实例
func (sm *ServiceManager) GetNotificationService() *notifications.NotificationService {
return sm.notificationService
}

View File

@@ -0,0 +1,139 @@
package services
import (
"context"
"fmt"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/services/badge"
"github.com/wailsapp/wails/v3/pkg/services/log"
"github.com/wailsapp/wails/v3/pkg/services/notifications"
)
// TestService 测试服务 - 仅在开发环境使用
type TestService struct {
logger *log.LogService
badgeService *badge.BadgeService
notificationService *notifications.NotificationService
}
// NewTestService 创建测试服务实例
func NewTestService(badgeService *badge.BadgeService, notificationService *notifications.NotificationService, logger *log.LogService) *TestService {
if logger == nil {
logger = log.New()
}
return &TestService{
logger: logger,
badgeService: badgeService,
notificationService: notificationService,
}
}
// ServiceStartup 服务启动时调用
func (ts *TestService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
return nil
}
// TestBadge 测试Badge功能
func (ts *TestService) TestBadge(text string) error {
if ts.badgeService == nil {
return fmt.Errorf("badge service not available")
}
if text == "" {
// 如果文本为空则移除badge
err := ts.badgeService.RemoveBadge()
if err != nil {
ts.logger.Error("Failed to remove badge", "error", err)
return err
}
ts.logger.Info("Badge removed successfully")
return nil
}
// 设置badge
err := ts.badgeService.SetBadge(text)
if err != nil {
ts.logger.Error("Failed to set badge", "text", text, "error", err)
return err
}
ts.logger.Info("Badge set successfully", "text", text)
return nil
}
// TestNotification 测试通知功能
func (ts *TestService) TestNotification(title, subtitle, body string) error {
if ts.notificationService == nil {
return fmt.Errorf("notification service not available")
}
// 检查通知授权macOS需要
authorized, err := ts.notificationService.CheckNotificationAuthorization()
if err != nil {
ts.logger.Error("Failed to check notification authorization", "error", err)
return err
}
if !authorized {
authorized, err = ts.notificationService.RequestNotificationAuthorization()
if err != nil || !authorized {
ts.logger.Error("Failed to get notification authorization", "error", err)
return fmt.Errorf("notification authorization denied")
}
}
// 使用默认值如果参数为空
if title == "" {
title = "Test Notification"
}
if subtitle == "" {
subtitle = "Testing notification system"
}
if body == "" {
body = "This is a test notification to verify the system is working correctly."
}
// 发送测试通知
err = ts.notificationService.SendNotification(notifications.NotificationOptions{
ID: "test_notification",
Title: title,
Subtitle: subtitle,
Body: body,
})
if err != nil {
ts.logger.Error("Failed to send test notification", "error", err)
return err
}
ts.logger.Info("Test notification sent successfully", "title", title)
return nil
}
// TestUpdateNotification 测试更新通知
func (ts *TestService) TestUpdateNotification() error {
// 设置badge
if err := ts.TestBadge("●"); err != nil {
return err
}
// 发送更新通知
return ts.TestNotification(
"Voidraft Update Available",
"New version available",
"New version 1.2.3 available (current: 1.2.0)",
)
}
// ClearAll 清除所有测试状态
func (ts *TestService) ClearAll() error {
// 移除badge
if err := ts.TestBadge(""); err != nil {
ts.logger.Error("Failed to clear badge during cleanup", "error", err)
}
ts.logger.Info("Test states cleared successfully")
return nil
}

View File

@@ -9,14 +9,14 @@ import (
"github.com/wailsapp/wails/v3/pkg/services/log" "github.com/wailsapp/wails/v3/pkg/services/log"
) )
// WindowInfo 窗口信息 // WindowInfo 窗口信息(简化版)
type WindowInfo struct { type WindowInfo struct {
Window *application.WebviewWindow Window *application.WebviewWindow
DocumentID int64 DocumentID int64
Title string Title string
} }
// WindowService 窗口管理服务 // WindowService 窗口管理服务(专注于窗口生命周期管理)
type WindowService struct { type WindowService struct {
logger *log.LogService logger *log.LogService
documentService *DocumentService documentService *DocumentService
@@ -24,6 +24,9 @@ type WindowService struct {
mainWindow *application.WebviewWindow mainWindow *application.WebviewWindow
windows map[int64]*WindowInfo // documentID -> WindowInfo windows map[int64]*WindowInfo // documentID -> WindowInfo
mu sync.RWMutex mu sync.RWMutex
// 吸附服务引用
windowSnapService *WindowSnapService
} }
// NewWindowService 创建新的窗口服务实例 // NewWindowService 创建新的窗口服务实例
@@ -39,10 +42,20 @@ func NewWindowService(logger *log.LogService, documentService *DocumentService)
} }
} }
// SetWindowSnapService 设置窗口吸附服务引用
func (ws *WindowService) SetWindowSnapService(snapService *WindowSnapService) {
ws.windowSnapService = snapService
}
// SetAppReferences 设置应用和主窗口引用 // SetAppReferences 设置应用和主窗口引用
func (ws *WindowService) SetAppReferences(app *application.App, mainWindow *application.WebviewWindow) { func (ws *WindowService) SetAppReferences(app *application.App, mainWindow *application.WebviewWindow) {
ws.app = app ws.app = app
ws.mainWindow = mainWindow ws.mainWindow = mainWindow
// 如果吸附服务已设置,也为它设置引用
if ws.windowSnapService != nil {
ws.windowSnapService.SetAppReferences(app, mainWindow)
}
} }
// OpenDocumentWindow 为指定文档ID打开新窗口 // OpenDocumentWindow 为指定文档ID打开新窗口
@@ -101,9 +114,14 @@ func (ws *WindowService) OpenDocumentWindow(documentID int64) error {
} }
ws.windows[documentID] = windowInfo ws.windows[documentID] = windowInfo
// 注册窗口关闭事件 // 注册窗口事件
ws.registerWindowEvents(newWindow, documentID) ws.registerWindowEvents(newWindow, documentID)
// 向吸附服务注册新窗口
if ws.windowSnapService != nil {
ws.windowSnapService.RegisterWindow(documentID, newWindow, doc.Title)
}
return nil return nil
} }
@@ -119,12 +137,17 @@ func (ws *WindowService) registerWindowEvents(window *application.WebviewWindow,
func (ws *WindowService) onWindowClosing(documentID int64) { func (ws *WindowService) onWindowClosing(documentID int64) {
ws.mu.Lock() ws.mu.Lock()
defer ws.mu.Unlock() defer ws.mu.Unlock()
windowInfo, exists := ws.windows[documentID] windowInfo, exists := ws.windows[documentID]
if exists { if exists {
windowInfo.Window.Close() windowInfo.Window.Close()
delete(ws.windows, documentID) delete(ws.windows, documentID)
}
// 从吸附服务中取消注册
if ws.windowSnapService != nil {
ws.windowSnapService.UnregisterWindow(documentID)
}
}
} }
// GetOpenWindows 获取所有打开的窗口信息 // GetOpenWindows 获取所有打开的窗口信息
@@ -147,3 +170,18 @@ func (ws *WindowService) IsDocumentWindowOpen(documentID int64) bool {
_, exists := ws.windows[documentID] _, exists := ws.windows[documentID]
return exists 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
}

View File

@@ -0,0 +1,487 @@
package services
import (
"math"
"sync"
"time"
"voidraft/internal/models"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
"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
// 吸附配置
snapEnabled bool // 是否启用窗口吸附功能
// 自适应阈值参数
baseThresholdRatio float64 // 基础阈值比例
minThreshold int // 最小阈值(像素)
maxThreshold int // 最大阈值(像素)
// 位置缓存
lastMainWindowPos models.WindowPosition // 缓存主窗口位置
lastMainWindowSize [2]int // 缓存主窗口尺寸 [width, height]
// 管理的窗口
managedWindows map[int64]*models.WindowInfo // documentID -> WindowInfo
windowRefs map[int64]*application.WebviewWindow // documentID -> Window引用
}
// NewWindowSnapService 创建新的窗口吸附服务实例
func NewWindowSnapService(logger *log.LogService, configService *ConfigService) *WindowSnapService {
if logger == nil {
logger = log.New()
}
// 从配置获取窗口吸附设置
config, err := configService.GetConfig()
snapEnabled := true // 默认启用
if err == nil {
snapEnabled = config.General.EnableWindowSnap
}
return &WindowSnapService{
logger: logger,
configService: configService,
snapEnabled: snapEnabled,
baseThresholdRatio: 0.025, // 2.5%的主窗口宽度作为基础阈值
minThreshold: 8, // 最小8像素小屏幕保底
maxThreshold: 40, // 最大40像素大屏幕上限
managedWindows: make(map[int64]*models.WindowInfo),
windowRefs: make(map[int64]*application.WebviewWindow),
}
}
// SetAppReferences 设置应用和主窗口引用
func (wss *WindowSnapService) SetAppReferences(app *application.App, mainWindow *application.WebviewWindow) {
wss.app = app
wss.mainWindow = mainWindow
// 初始化主窗口位置缓存
wss.updateMainWindowCache()
// 设置主窗口移动事件监听
if mainWindow != nil {
wss.setupMainWindowEvents()
}
}
// RegisterWindow 注册需要吸附管理的窗口
func (wss *WindowSnapService) RegisterWindow(documentID int64, window *application.WebviewWindow, title string) {
wss.mu.Lock()
defer wss.mu.Unlock()
// 获取初始位置
x, y := window.Position()
windowInfo := &models.WindowInfo{
DocumentID: documentID,
Title: title,
IsSnapped: false,
SnapOffset: models.SnapPosition{X: 0, Y: 0},
SnapEdge: models.SnapEdgeNone,
LastPos: models.WindowPosition{X: x, Y: y},
MoveTime: time.Now(),
}
wss.managedWindows[documentID] = windowInfo
wss.windowRefs[documentID] = window
// 为窗口设置移动事件监听
wss.setupWindowEvents(window, windowInfo)
}
// UnregisterWindow 取消注册窗口
func (wss *WindowSnapService) UnregisterWindow(documentID int64) {
wss.mu.Lock()
defer wss.mu.Unlock()
delete(wss.managedWindows, documentID)
delete(wss.windowRefs, 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
}
}
}
}
// calculateAdaptiveThreshold 计算自适应吸附阈值
func (wss *WindowSnapService) calculateAdaptiveThreshold() int {
// 基于主窗口宽度计算阈值
mainWidth := wss.lastMainWindowSize[0]
if mainWidth == 0 {
return wss.minThreshold // 默认最小值
}
// 计算基础阈值主窗口宽度的2.5%
adaptiveThreshold := int(float64(mainWidth) * wss.baseThresholdRatio)
// 限制在最小和最大值之间
if adaptiveThreshold < wss.minThreshold {
return wss.minThreshold
}
if adaptiveThreshold > wss.maxThreshold {
return wss.maxThreshold
}
return adaptiveThreshold
}
// GetCurrentThreshold 获取当前自适应阈值(用于调试或显示)
func (wss *WindowSnapService) GetCurrentThreshold() int {
wss.mu.RLock()
defer wss.mu.RUnlock()
return wss.calculateAdaptiveThreshold()
}
// OnWindowSnapConfigChanged 处理窗口吸附配置变更
func (wss *WindowSnapService) OnWindowSnapConfigChanged(enabled bool) error {
wss.SetSnapEnabled(enabled)
// 阈值现在是自适应的,无需手动设置
return nil
}
// setupMainWindowEvents 设置主窗口事件监听
func (wss *WindowSnapService) setupMainWindowEvents() {
// 监听主窗口移动事件
wss.mainWindow.RegisterHook(events.Common.WindowDidMove, func(event *application.WindowEvent) {
wss.onMainWindowMoved()
})
}
// setupWindowEvents 为子窗口设置事件监听
func (wss *WindowSnapService) setupWindowEvents(window *application.WebviewWindow, windowInfo *models.WindowInfo) {
// 监听子窗口移动事件
window.RegisterHook(events.Common.WindowDidMove, func(event *application.WindowEvent) {
wss.onChildWindowMoved(window, windowInfo)
})
}
// updateMainWindowCache 更新主窗口缓存
func (wss *WindowSnapService) updateMainWindowCache() {
if wss.mainWindow == nil {
return
}
x, y := wss.mainWindow.Position()
w, h := wss.mainWindow.Size()
wss.lastMainWindowPos = models.WindowPosition{X: x, Y: y}
wss.lastMainWindowSize = [2]int{w, h}
}
// onMainWindowMoved 主窗口移动事件处理
func (wss *WindowSnapService) onMainWindowMoved() {
if !wss.snapEnabled {
return
}
wss.mu.Lock()
defer wss.mu.Unlock()
// 更新主窗口缓存
wss.updateMainWindowCache()
// 只更新已吸附窗口的位置,无需重新检测所有窗口
for _, windowInfo := range wss.managedWindows {
if windowInfo.IsSnapped {
wss.updateSnappedWindowPosition(windowInfo)
}
}
}
// onChildWindowMoved 子窗口移动事件处理
func (wss *WindowSnapService) onChildWindowMoved(window *application.WebviewWindow, windowInfo *models.WindowInfo) {
if !wss.snapEnabled {
return
}
wss.mu.Lock()
defer wss.mu.Unlock()
// 获取当前位置
x, y := window.Position()
currentPos := models.WindowPosition{X: x, Y: y}
// 检查是否真的移动了(避免无效触发)
if currentPos.X == windowInfo.LastPos.X && currentPos.Y == windowInfo.LastPos.Y {
return
}
// 保存上次移动时间用于防抖检测
lastMoveTime := windowInfo.MoveTime
windowInfo.MoveTime = time.Now()
if windowInfo.IsSnapped {
// 已吸附窗口:检查是否被用户拖拽解除吸附
wss.handleSnappedWindow(window, windowInfo, currentPos)
// 对于已吸附窗口,总是更新为当前位置
windowInfo.LastPos = currentPos
} else {
// 未吸附窗口:检查是否应该吸附
isSnapped := wss.handleUnsnappedWindow(window, windowInfo, currentPos, lastMoveTime)
if !isSnapped {
// 如果没有吸附,更新为当前位置
windowInfo.LastPos = currentPos
}
// 如果成功吸附位置已在handleUnsnappedWindow中更新
}
}
// updateSnappedWindowPosition 更新已吸附窗口的位置
func (wss *WindowSnapService) updateSnappedWindowPosition(windowInfo *models.WindowInfo) {
// 计算新的目标位置(基于主窗口新位置)
expectedX := wss.lastMainWindowPos.X + windowInfo.SnapOffset.X
expectedY := wss.lastMainWindowPos.Y + windowInfo.SnapOffset.Y
// 查找对应的window对象并移动
if window, exists := wss.windowRefs[windowInfo.DocumentID]; exists {
window.SetPosition(expectedX, expectedY)
}
windowInfo.LastPos = models.WindowPosition{X: expectedX, Y: expectedY}
}
// handleSnappedWindow 处理已吸附窗口的移动
func (wss *WindowSnapService) handleSnappedWindow(window *application.WebviewWindow, windowInfo *models.WindowInfo, currentPos models.WindowPosition) {
// 计算预期位置
expectedX := wss.lastMainWindowPos.X + windowInfo.SnapOffset.X
expectedY := wss.lastMainWindowPos.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.calculateAdaptiveThreshold())
isUserDrag := maxDistance > userDragThreshold && time.Since(windowInfo.MoveTime) < 50*time.Millisecond
if isUserDrag {
// 用户主动拖拽,解除吸附
windowInfo.IsSnapped = false
windowInfo.SnapEdge = models.SnapEdgeNone
}
}
// handleUnsnappedWindow 处理未吸附窗口的移动,返回是否成功吸附
func (wss *WindowSnapService) handleUnsnappedWindow(window *application.WebviewWindow, windowInfo *models.WindowInfo, currentPos models.WindowPosition, lastMoveTime time.Time) bool {
// 检查是否应该吸附
should, snapEdge := wss.shouldSnapToMainWindow(window, windowInfo, currentPos, lastMoveTime)
if should {
// 设置吸附状态
windowInfo.IsSnapped = true
windowInfo.SnapEdge = snapEdge
// 执行吸附移动
targetPos := wss.calculateSnapPosition(snapEdge, currentPos, window)
window.SetPosition(targetPos.X, targetPos.Y)
// 计算并保存偏移量
windowInfo.SnapOffset.X = targetPos.X - wss.lastMainWindowPos.X
windowInfo.SnapOffset.Y = targetPos.Y - wss.lastMainWindowPos.Y
// 更新位置为吸附后的位置
windowInfo.LastPos = targetPos
return true
}
return false
}
// 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(window *application.WebviewWindow, windowInfo *models.WindowInfo, currentPos models.WindowPosition, lastMoveTime time.Time) (bool, models.SnapEdge) {
// 防抖:移动太快时不检测,
timeSinceLastMove := time.Since(lastMoveTime)
if timeSinceLastMove < 30*time.Millisecond && timeSinceLastMove > 0 {
return false, models.SnapEdgeNone
}
// 使用缓存的主窗口位置和尺寸
if wss.lastMainWindowSize[0] == 0 || wss.lastMainWindowSize[1] == 0 {
// 主窗口缓存未初始化,立即更新
wss.updateMainWindowCache()
}
mainPos := wss.lastMainWindowPos
mainWidth := wss.lastMainWindowSize[0]
mainHeight := wss.lastMainWindowSize[1]
// 获取子窗口尺寸
windowWidth, windowHeight := window.Size()
// 自适应阈值计算
threshold := float64(wss.calculateAdaptiveThreshold())
cornerThreshold := threshold * 1.5
// 计算边界
mainLeft, mainTop := mainPos.X, mainPos.Y
mainRight, mainBottom := mainPos.X+mainWidth, mainPos.Y+mainHeight
windowLeft, windowTop := currentPos.X, currentPos.Y
windowRight, windowBottom := currentPos.X+windowWidth, currentPos.Y+windowHeight
// 简化的距离计算结构
type snapCheck struct {
edge models.SnapEdge
distance float64
priority int // 1=角落, 2=边缘
}
var bestSnap *snapCheck
// 检查角落吸附优先级1
cornerChecks := []struct {
edge models.SnapEdge
dx int
dy int
}{
{models.SnapEdgeTopRight, mainRight - windowLeft, mainTop - windowBottom},
{models.SnapEdgeBottomRight, mainRight - windowLeft, mainBottom - windowTop},
{models.SnapEdgeBottomLeft, mainLeft - windowRight, mainBottom - windowTop},
{models.SnapEdgeTopLeft, mainLeft - windowRight, mainTop - windowBottom},
}
for _, check := range cornerChecks {
dist := math.Sqrt(float64(check.dx*check.dx + check.dy*check.dy))
if dist <= cornerThreshold {
if bestSnap == nil || dist < bestSnap.distance {
bestSnap = &snapCheck{check.edge, dist, 1}
}
}
}
// 如果没有角落吸附检查边缘吸附优先级2
if bestSnap == nil {
edgeChecks := []struct {
edge models.SnapEdge
distance float64
}{
{models.SnapEdgeRight, math.Abs(float64(mainRight - windowLeft))},
{models.SnapEdgeLeft, math.Abs(float64(mainLeft - windowRight))},
{models.SnapEdgeBottom, math.Abs(float64(mainBottom - windowTop))},
{models.SnapEdgeTop, math.Abs(float64(mainTop - windowBottom))},
}
for _, check := range edgeChecks {
if check.distance <= threshold {
if bestSnap == nil || check.distance < bestSnap.distance {
bestSnap = &snapCheck{check.edge, check.distance, 2}
}
}
}
}
if bestSnap == nil {
return false, models.SnapEdgeNone
}
return true, bestSnap.edge
}
// calculateSnapPosition 计算吸附目标位置
func (wss *WindowSnapService) calculateSnapPosition(snapEdge models.SnapEdge, currentPos models.WindowPosition, window *application.WebviewWindow) models.WindowPosition {
// 使用缓存的主窗口信息
mainPos := wss.lastMainWindowPos
mainWidth := wss.lastMainWindowSize[0]
mainHeight := wss.lastMainWindowSize[1]
// 获取子窗口尺寸
windowWidth, windowHeight := window.Size()
switch snapEdge {
case models.SnapEdgeRight:
return models.WindowPosition{
X: mainPos.X + mainWidth,
Y: currentPos.Y, // 保持当前Y位置
}
case models.SnapEdgeLeft:
return models.WindowPosition{
X: mainPos.X - windowWidth,
Y: currentPos.Y,
}
case models.SnapEdgeBottom:
return models.WindowPosition{
X: currentPos.X,
Y: mainPos.Y + mainHeight,
}
case models.SnapEdgeTop:
return models.WindowPosition{
X: currentPos.X,
Y: mainPos.Y - windowHeight,
}
case models.SnapEdgeTopRight:
return models.WindowPosition{
X: mainPos.X + mainWidth,
Y: mainPos.Y - windowHeight,
}
case models.SnapEdgeBottomRight:
return models.WindowPosition{
X: mainPos.X + mainWidth,
Y: mainPos.Y + mainHeight,
}
case models.SnapEdgeBottomLeft:
return models.WindowPosition{
X: mainPos.X - windowWidth,
Y: mainPos.Y + mainHeight,
}
case models.SnapEdgeTopLeft:
return models.WindowPosition{
X: mainPos.X - windowWidth,
Y: mainPos.Y - windowHeight,
}
}
return currentPos
}
// Cleanup 清理资源
func (wss *WindowSnapService) Cleanup() {
wss.mu.Lock()
defer wss.mu.Unlock()
// 清空管理的窗口
wss.managedWindows = make(map[int64]*models.WindowInfo)
wss.windowRefs = make(map[int64]*application.WebviewWindow)
}
// ServiceShutdown 实现服务关闭接口
func (wss *WindowSnapService) ServiceShutdown() error {
wss.Cleanup()
return nil
}

View File

@@ -12,13 +12,13 @@ func SetupSystemTray(app *application.App, mainWindow *application.WebviewWindow
// 创建系统托盘 // 创建系统托盘
systray := app.SystemTray.New() systray := app.SystemTray.New()
// 设置标签
systray.SetLabel("voidraft")
systray.Label()
// 设置图标 // 设置图标
iconBytes, _ := assets.ReadFile("appicon.png") iconBytes, _ := assets.ReadFile("appicon.png")
systray.SetIcon(iconBytes) systray.SetIcon(iconBytes)
// 设置标签
systray.SetLabel("VoidRaft")
// 创建托盘菜单 // 创建托盘菜单
menu := app.NewMenu() menu := app.NewMenu()

View File

@@ -0,0 +1,4 @@
package version
// Version 版本注入 Ldflags
var Version = "0.0.0"

86
scripts/version.bat Normal file
View File

@@ -0,0 +1,86 @@
@echo off
setlocal enabledelayedexpansion
REM Simplified version management script - Windows version
REM Auto-increment patch version from git tags or use custom version
REM Configuration section - Set custom version here if needed
set "CUSTOM_VERSION="
REM Example: set "CUSTOM_VERSION=2.0.0"
set "VERSION_FILE=version.txt"
REM Check if custom version is set
if not "%CUSTOM_VERSION%"=="" (
echo [INFO] Using custom version: %CUSTOM_VERSION%
set "VERSION=%CUSTOM_VERSION%"
goto :save_version
)
REM Check if git is available
git --version >nul 2>&1
if errorlevel 1 (
echo [ERROR] Git is not installed or not in PATH
exit /b 1
)
REM Check if in git repository
git rev-parse --git-dir >nul 2>&1
if errorlevel 1 (
echo [ERROR] Not in a git repository
exit /b 1
)
REM Sync remote tags
echo [INFO] Syncing remote tags...
echo [INFO] Deleting local tags...
for /f "delims=" %%i in ('git tag -l') do git tag -d %%i >nul 2>&1
echo [INFO] Fetching remote tags...
git fetch origin --prune >nul 2>&1
if errorlevel 1 (
echo [WARNING] Failed to fetch from remote, using local tags
) else (
echo [INFO] Remote tags synced successfully
)
REM Get latest git tag
git describe --abbrev=0 --tags > temp_tag.txt 2>nul
if errorlevel 1 (
echo [ERROR] No git tags found in repository
if exist temp_tag.txt del temp_tag.txt
exit /b 1
)
set /p LATEST_TAG=<temp_tag.txt
del temp_tag.txt
if not defined LATEST_TAG (
echo [ERROR] Failed to read git tag
exit /b 1
)
echo [INFO] Latest git tag: %LATEST_TAG%
REM Remove v prefix
set "CLEAN_VERSION=%LATEST_TAG:v=%"
REM Split version number and increment patch
for /f "tokens=1,2,3 delims=." %%a in ("%CLEAN_VERSION%") do (
set "MAJOR=%%a"
set "MINOR=%%b"
set /a "PATCH=%%c+1"
)
set "VERSION=%MAJOR%.%MINOR%.%PATCH%"
echo [INFO] Auto-incremented patch version: %VERSION%
:save_version
REM Output version information
echo [SUCCESS] Version resolved: %VERSION%
echo VERSION=%VERSION%
REM Save to file
echo VERSION=%VERSION% > %VERSION_FILE%
echo [INFO] Version information saved to %VERSION_FILE%

62
scripts/version.sh Normal file
View File

@@ -0,0 +1,62 @@
#!/bin/bash
# 配置区域 - 如需自定义版本,请在此处设置
CUSTOM_VERSION=""
# 示例: CUSTOM_VERSION="2.0.0"
VERSION_FILE="version.txt"
# 检查是否设置了自定义版本
if [ -n "$CUSTOM_VERSION" ]; then
echo "[INFO] Using custom version: $CUSTOM_VERSION"
VERSION="$CUSTOM_VERSION"
else
# 检查git是否可用
if ! command -v git &> /dev/null; then
echo "[ERROR] Git is not installed or not in PATH"
exit 1
elif ! git rev-parse --git-dir &> /dev/null; then
echo "[ERROR] Not in a git repository"
exit 1
else
# 同步远程标签
echo "[INFO] Syncing remote tags..."
echo "[INFO] Deleting local tags..."
git tag -l | xargs git tag -d &> /dev/null
echo "[INFO] Fetching remote tags..."
if git fetch origin --prune &> /dev/null; then
echo "[INFO] Remote tags synced successfully"
else
echo "[WARNING] Failed to fetch from remote, using local tags"
fi
# 获取最新的git标签
LATEST_TAG=$(git describe --abbrev=0 --tags 2>/dev/null)
if [ -z "$LATEST_TAG" ]; then
echo "[ERROR] No git tags found in repository"
exit 1
else
echo "[INFO] Latest git tag: $LATEST_TAG"
# 移除v前缀
CLEAN_VERSION=${LATEST_TAG#v}
# 分割版本号并递增patch版本
IFS='.' read -r MAJOR MINOR PATCH <<< "$CLEAN_VERSION"
PATCH=$((PATCH + 1))
VERSION="$MAJOR.$MINOR.$PATCH"
echo "[INFO] Auto-incremented patch version: $VERSION"
fi
fi
fi
# 输出版本信息
echo "VERSION=$VERSION"
# 保存到文件供其他脚本使用
echo "VERSION=$VERSION" > "$VERSION_FILE"
echo "[INFO] Version information saved to $VERSION_FILE"

1
version.txt Normal file
View File

@@ -0,0 +1 @@
VERSION=1.3.6