8 Commits

Author SHA1 Message Date
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
709998ff9c 🐛 Fixed hotkey crashes 2025-08-19 22:26:55 +08:00
51 changed files with 1703 additions and 631 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**
> *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
@@ -87,7 +87,7 @@ After building, the executable will be generated in the `bin` directory.
## Project Structure
```
Voidraft/
voidraft/
├── frontend/ # Vue 3 frontend application
│ ├── src/
│ │ ├── views/editor/ # Editor core views
@@ -129,11 +129,11 @@ Voidraft/
> 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
- **[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
- Adds more practical features on the original foundation
- 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.
[![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 forks](https://img.shields.io/github/forks/landaiqing/Voidraft.svg?style=social&label=Fork)](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)
*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)
> *一个专为开发者打造的优雅文本片段记录工具。*
Voidraft 是一个现代化的开发者专用文本编辑器让你能够随时随地记录、整理和管理各种文本片段。无论是临时的代码片段、API 响应、会议笔记,还是日常的待办事项,Voidraft 都能为你提供流畅而优雅的编辑体验。
voidraft 是一个现代化的开发者专用文本编辑器让你能够随时随地记录、整理和管理各种文本片段。无论是临时的代码片段、API 响应、会议笔记,还是日常的待办事项,voidraft 都能为你提供流畅而优雅的编辑体验。
## 核心特性
@@ -88,7 +88,7 @@ wails3 package
## 项目结构
```
Voidraft/
voidraft/
├── frontend/ # Vue 3 前端应用
│ ├── src/
│ │ ├── 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 优雅的块状编辑理念
- 在原有基础上增加了更多实用功能
- 采用现代化技术栈重新构建
@@ -159,7 +159,7 @@ Voidraft 的诞生离不开以下优秀的开源项目:
欢迎 Fork、Star 和贡献代码。
[![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 forks](https://img.shields.io/github/forks/landaiqing/Voidraft.svg?style=social&label=Fork)](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)
*Made with ❤️ by landaiqing*

View File

@@ -12,13 +12,25 @@ vars:
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
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:
summary: Builds the application
deps: [version]
cmds:
- task: "{{OS}}:build"
package:
summary: Packages a production build of the application
deps: [version]
cmds:
- task: "{{OS}}:package"

View File

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

View File

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

View File

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

View File

@@ -11,9 +11,12 @@ tasks:
- task: common:build:frontend
- task: common:generate:icons
cmds:
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
- go build {{.BUILD_FLAGS}} -ldflags="{{.LDFLAGS}} {{.VERSION_FLAGS}}" -o {{.OUTPUT}}
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}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
env:

View File

@@ -11,9 +11,12 @@ tasks:
- task: common:build:frontend
- task: common:generate:icons
cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
- go build {{.BUILD_FLAGS}} -ldflags="{{.LDFLAGS}} {{.VERSION_FLAGS}}" -o {{.BIN_DIR}}/{{.APP_NAME}}
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:
GOOS: linux
CGO_ENABLED: 1

View File

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

View File

@@ -14,13 +14,16 @@ tasks:
- task: common:generate:icons
cmds:
- 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
platforms: [windows]
- cmd: rm -f *.syso
platforms: [linux, darwin]
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:
GOOS: windows
CGO_ENABLED: 1

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<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/changelog.css">
<link rel="icon" href="img/favicon.ico">
@@ -16,7 +16,7 @@
<!-- 主卡片 -->
<div class="card">
<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">
<button id="theme-toggle" class="btn btn-secondary" title="切换主题">
<i class="fas fa-sun"></i> <span data-en="Theme" data-zh="主题">Theme</span>
@@ -59,7 +59,7 @@
<!-- 页脚 -->
<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">
<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>

View File

@@ -3,10 +3,10 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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="keywords" content="text editor, code snippets, developer tools, syntax highlighting, code formatting, multi-language, VoidRaft">
<meta name="author" content="VoidRaft Team">
<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="keywords" content="text editor, code snippets, developer tools, syntax highlighting, code formatting, multi-language, voidraft">
<meta name="author" content="voidraft Team">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://landaiqing.github.io/voidraft/">
@@ -18,16 +18,16 @@
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<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: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: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: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 -->
<meta property="twitter:card" content="summary_large_image">
<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: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: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:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
<link rel="stylesheet" href="./css/styles.css">
@@ -39,13 +39,13 @@
{
"@context": "https://schema.org",
"@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.",
"url": "https://landaiqing.github.io/voidraft/",
"downloadUrl": "https://github.com/landaiqing/voidraft/releases",
"author": {
"@type": "Organization",
"name": "VoidRaft"
"name": "voidraft"
},
"operatingSystem": ["Windows", "macOS", "Linux"],
"applicationCategory": "DeveloperApplication",
@@ -66,7 +66,7 @@
<!-- 主卡片 -->
<div class="card">
<div class="card-header">
<h1 class="card-title">VoidRaft</h1>
<h1 class="card-title">voidraft</h1>
<div class="card-controls">
<button id="theme-toggle" class="btn btn-secondary" title="切换主题">
<i class="fas fa-sun"></i> <span data-en="Theme" data-zh="主题">Theme</span>
@@ -81,9 +81,9 @@
<!-- Logo和介绍 -->
<div class="logo-container">
<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>
<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>
</div>
@@ -195,15 +195,15 @@
<div class="block-language">text</div>
</div>
<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">// A modern text editor designed for developers</span></pre>
</div>
</div>
</div>
<div class="preview-window">
<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-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;">
</div>
</div>
@@ -241,7 +241,7 @@
<!-- 页脚 -->
<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">
<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>

View File

@@ -1,5 +1,5 @@
/**
* VoidRaft - Changelog Script
* voidraft - Changelog Script
* 从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.metaTexts = {
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.',
title: 'VoidRaft - An elegant text snippet recording tool designed for developers.',
ogTitle: 'VoidRaft - An elegant text snippet recording tool designed for developers'
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.',
ogTitle: 'voidraft - An elegant text snippet recording tool designed for developers'
},
zh: {
description: 'VoidRaft 是专为开发者打造的优雅文本片段记录工具。支持多语言代码块、语法高亮、代码格式化、自定义主题等功能。',
title: 'VoidRaft - 专为开发者打造的优雅文本片段记录工具',
ogTitle: 'VoidRaft - 专为开发者打造的优雅文本片段记录工具'
description: 'voidraft 是专为开发者打造的优雅文本片段记录工具。支持多语言代码块、语法高亮、代码格式化、自定义主题等功能。',
title: 'voidraft - 专为开发者打造的优雅文本片段记录工具',
ogTitle: 'voidraft - 专为开发者打造的优雅文本片段记录工具'
}
};
this.init();
@@ -371,9 +371,9 @@ class UIEffects {
}
/**
* VoidRaft主应用类
* voidraft主应用类
*/
class VoidRaftApp {
class voidraftApp {
constructor() {
this.themeManager = null;
this.languageManager = null;
@@ -404,7 +404,7 @@ class VoidRaftApp {
* 显示控制台品牌信息
*/
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;');
}
@@ -439,5 +439,5 @@ class VoidRaftApp {
// 当DOM加载完成时初始化应用
document.addEventListener('DOMContentLoaded', () => {
window.voidRaftApp = new VoidRaftApp();
window.voidRaftApp = new voidraftApp();
});

View File

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

View File

@@ -34,6 +34,22 @@ export function GetConfig(): Promise<models$0.AppConfig | null> & { cancel(): vo
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;
}
/**
* ResetConfig 强制重置所有配置为默认值
*/
@@ -82,6 +98,14 @@ export function SetHotkeyChangeCallback(callback: any): Promise<void> & { cancel
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
const $$createType0 = models$0.AppConfig.createFrom;
const $$createType1 = $Create.Nullable($$createType0);

View File

@@ -17,6 +17,7 @@ import * as ThemeService from "./themeservice.js";
import * as TranslationService from "./translationservice.js";
import * as TrayService from "./trayservice.js";
import * as WindowService from "./windowservice.js";
import * as WindowSnapService from "./windowsnapservice.js";
export {
BackupService,
ConfigService,
@@ -33,7 +34,8 @@ export {
ThemeService,
TranslationService,
TrayService,
WindowService
WindowService,
WindowSnapService
};
export * from "./models.js";

View File

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

View File

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

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

View File

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

View File

@@ -1,10 +1,11 @@
<script setup lang="ts">
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 {useEditorStore} from '@/stores/editorStore';
import {useUpdateStore} from '@/stores/updateStore';
import {useWindowStore} from '@/stores/windowStore';
import {useSystemStore} from '@/stores/systemStore';
import * as runtime from '@wailsio/runtime';
import {useRouter} from 'vue-router';
import BlockLanguageSelector from './BlockLanguageSelector.vue';
@@ -17,22 +18,32 @@ const editorStore = useEditorStore();
const configStore = useConfigStore();
const updateStore = useUpdateStore();
const windowStore = useWindowStore();
const systemStore = useSystemStore();
const {t} = useI18n();
const router = useRouter();
// 当前块是否支持格式化的响应式状态
const canFormatCurrentBlock = ref(false);
// 窗口置顶状态管理(仅当前窗口,不同步到配置文件)
const isCurrentWindowOnTop = ref(false);
const setWindowAlwaysOnTop = async (isTop: boolean) => {
await runtime.Window.SetAlwaysOnTop(isTop);
};
// 窗口置顶状态 - 合并配置和临时状态
const isCurrentWindowOnTop = computed(() => {
return configStore.config.general.alwaysOnTop || systemStore.isWindowOnTop;
});
// 切换窗口置顶状态
const toggleAlwaysOnTop = async () => {
isCurrentWindowOnTop.value = !isCurrentWindowOnTop.value;
await runtime.Window.SetAlwaysOnTop(isCurrentWindowOnTop.value);
const currentlyOnTop = isCurrentWindowOnTop.value;
if (currentlyOnTop) {
// 如果当前是置顶状态,彻底关闭所有置顶
if (configStore.config.general.alwaysOnTop) {
await configStore.setAlwaysOnTop(false);
}
await systemStore.setWindowOnTop(false);
} else {
// 如果当前不是置顶状态,开启临时置顶
await systemStore.setWindowOnTop(true);
}
};
// 跳转到设置页面
@@ -62,8 +73,8 @@ const updateFormatButtonState = () => {
// 检查块和语言格式化支持
canFormatCurrentBlock.value = !!(
activeBlock &&
getLanguage(activeBlock.language.name as any)?.prettier
activeBlock &&
getLanguage(activeBlock.language.name as any)?.prettier
);
} catch (error) {
console.warn('Error checking format capability:', error);
@@ -80,7 +91,7 @@ const debouncedUpdateFormatButton = (() => {
timeout = window.setTimeout(() => {
updateFormatButtonState();
timeout = null;
}, 300);
}, 1000);
};
})();
@@ -89,9 +100,9 @@ const setupEditorListeners = (view: any) => {
if (!view?.dom) return [];
const events = [
{ type: 'click', handler: updateFormatButtonState },
{ type: 'keyup', handler: debouncedUpdateFormatButton },
{ type: 'focus', handler: updateFormatButtonState }
{type: 'click', handler: updateFormatButtonState},
{type: 'keyup', handler: debouncedUpdateFormatButton},
{type: 'focus', handler: updateFormatButtonState}
];
// 注册所有事件
@@ -99,7 +110,7 @@ const setupEditorListeners = (view: any) => {
// 返回清理函数数组
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)[] = [];
watch(
() => editorStore.editorView,
(newView) => {
// 清理旧监听器
cleanupListeners.forEach(cleanup => cleanup());
cleanupListeners = [];
() => editorStore.editorView,
(newView) => {
// 清理旧监听器
cleanupListeners.forEach(cleanup => cleanup());
cleanupListeners = [];
if (newView) {
// 初始更新状态
updateFormatButtonState();
// 设置新监听器
cleanupListeners = setupEditorListeners(newView);
} else {
canFormatCurrentBlock.value = false;
}
},
{ immediate: true }
if (newView) {
// 初始更新状态
updateFormatButtonState();
// 设置新监听器
cleanupListeners = setupEditorListeners(newView);
} else {
canFormatCurrentBlock.value = false;
}
},
{immediate: true}
);
// 组件生命周期
@@ -143,12 +154,28 @@ onUnmounted(() => {
// 组件加载后初始化置顶状态
watch(isLoaded, async (loaded) => {
if (loaded) {
// 初始化时从配置文件读取置顶状态
isCurrentWindowOnTop.value = configStore.config.general.alwaysOnTop;
await setWindowAlwaysOnTop(isCurrentWindowOnTop.value);
// 应用合并后的置顶状态
const shouldBeOnTop = configStore.config.general.alwaysOnTop || systemStore.isWindowOnTop;
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 () => {
if (updateStore.hasUpdate && !updateStore.isUpdating && !updateStore.updateSuccess) {
// 开始下载更新
@@ -230,20 +257,23 @@ const updateButtonTitle = computed(() => {
@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">
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</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">
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
<path d="M12 2a10 10 0 1 0 10 10"></path>
</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">
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path>
<line x1="12" y1="2" x2="12" y2="12"></line>
@@ -252,7 +282,8 @@ const updateButtonTitle = computed(() => {
<!-- 有更新可用 -->
<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">
<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="12,15 12,3"/>
</svg>

View File

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

View File

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

View File

@@ -62,7 +62,8 @@ const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
enableSystemTray: 'general.enableSystemTray',
startAtLogin: 'general.startAtLogin',
enableGlobalHotkey: 'general.enableGlobalHotkey',
globalHotkey: 'general.globalHotkey'
globalHotkey: 'general.globalHotkey',
enableWindowSnap: 'general.enableWindowSnap',
} as const;
const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
@@ -176,7 +177,8 @@ const DEFAULT_CONFIG: AppConfig = {
alt: true,
win: false,
key: 'X'
}
},
enableWindowSnap: true,
},
editing: {
fontSize: CONFIG_LIMITS.fontSize.default,
@@ -521,6 +523,9 @@ export const useConfigStore = defineStore('config', () => {
await StartupService.SetEnabled(value);
},
// 窗口吸附配置相关方法
setEnableWindowSnap: async (value: boolean) => await updateGeneralConfig('enableWindowSnap', 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 {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice';
import {Document} from '@/../bindings/voidraft/internal/models/models';
import {useSystemStore} from './systemStore';
const SCRATCH_DOCUMENT_ID = 1; // 默认草稿文档ID
export const useDocumentStore = defineStore('document', () => {
const DEFAULT_DOCUMENT_ID = ref<number>(1); // 默认草稿文档ID
// === 核心状态 ===
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 currentDocument = ref<Document | null>(null);
@@ -159,7 +161,7 @@ export const useDocumentStore = defineStore('document', () => {
const deleteDocument = async (docId: number): Promise<boolean> => {
try {
// 检查是否是默认文档使用ID判断
if (docId === SCRATCH_DOCUMENT_ID) {
if (docId === DEFAULT_DOCUMENT_ID.value) {
return false;
}
@@ -221,6 +223,7 @@ export const useDocumentStore = defineStore('document', () => {
};
return {
DEFAULT_DOCUMENT_ID,
// 状态
documents,
documentList,

View File

@@ -20,13 +20,14 @@ export const useSystemStore = defineStore('system', () => {
const environment = ref<SystemEnvironment | null>(null);
const isLoading = ref(false);
// 窗口置顶状态管理
const isWindowOnTop = ref<boolean>(false);
// 计算属性
const isWindows = computed(() => environment.value?.OS === 'windows');
const isMacOS = computed(() => environment.value?.OS === 'darwin');
const isLinux = computed(() => environment.value?.OS === 'linux');
// 获取标题栏高度
const titleBarHeight = computed(() => {
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 {
// 状态
environment,
isLoading,
isWindowOnTop,
// 计算属性
isWindows,
@@ -62,5 +84,14 @@ export const useSystemStore = defineStore('system', () => {
// 方法
initializeSystemInfo,
setWindowOnTop,
toggleWindowOnTop,
resetWindowOnTop,
};
}, {
persist: {
key: 'voidraft-system',
storage: localStorage,
pick: ['isWindowOnTop']
}
});

View File

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

7
go.mod
View File

@@ -3,7 +3,6 @@ module voidraft
go 1.24.4
require (
github.com/Masterminds/semver/v3 v3.4.0
github.com/creativeprojects/go-selfupdate v1.5.0
github.com/go-git/go-git/v5 v5.16.2
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/v2 v2.2.2
github.com/robertkrimen/otto v0.5.1
github.com/stretchr/testify v1.10.0
github.com/wailsapp/wails/v3 v3.0.0-alpha.25
golang.org/x/net v0.43.0
golang.org/x/sys v0.35.0
@@ -22,12 +22,14 @@ require (
code.gitea.io/sdk/gitea v0.21.0 // indirect
dario.cat/mergo v1.0.2 // 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/ProtonMail/go-crypto v1.3.0 // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/cloudflare/circl v1.6.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/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
@@ -62,12 +64,13 @@ require (
github.com/pjbgf/sha1cd v0.4.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.51.0 // indirect
github.com/sergi/go-diff v1.4.0 // 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/mimetype v1.4.1 // indirect
github.com/xanzy/go-gitlab v0.115.0 // indirect

4
go.sum
View File

@@ -158,8 +158,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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
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.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
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/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=

View File

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

View File

@@ -1,318 +1,195 @@
package services
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
"time"
"voidraft/internal/models"
"github.com/Masterminds/semver/v3"
jsonparser "github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/providers/structs"
"github.com/knadh/koanf/v2"
"github.com/wailsapp/wails/v3/pkg/services/log"
"os"
"path/filepath"
"time"
)
const (
// CurrentAppConfigVersion 当前应用配置版本
CurrentAppConfigVersion = "1.3.0"
// BackupFilePattern 备份文件名模式
// BackupFilePattern backup file name pattern
BackupFilePattern = "%s.backup.%s.json"
// 资源限制常量
// MaxConfigFileSize maximum config file size
MaxConfigFileSize = 10 * 1024 * 1024 // 10MB
MaxRecursionDepth = 50 // 最大递归深度
)
// Migratable 可迁移的配置接口
type Migratable interface {
GetVersion() string // 获取当前版本
SetVersion(string) // 设置版本
SetLastUpdated(string) // 设置最后更新时间
GetDefaultConfig() any // 获取默认配置
// ConfigMigrator elegant configuration migrator with automatic field detection
type ConfigMigrator struct {
logger *log.LogService
configDir string
configName string
configPath string
}
// ConfigMigrationService 配置迁移服务
type ConfigMigrationService[T Migratable] struct {
logger *log.LogService
configDir string
configName string
targetVersion string
configPath string
}
// MigrationResult 迁移结果
// MigrationResult migration operation result
type MigrationResult struct {
Migrated, ConfigUpdated bool
FromVersion, ToVersion string
BackupPath string
Migrated bool `json:"migrated"` // Whether migration was performed
MissingFields []string `json:"missingFields"` // Fields that were missing
BackupPath string `json:"backupPath"` // Path to backup file
Description string `json:"description"` // Description of migration
}
// NewConfigMigrationService 创建配置迁移服务
func NewConfigMigrationService[T Migratable](
// NewConfigMigrator creates a new configuration migrator
func NewConfigMigrator(
logger *log.LogService,
configDir string,
configName, targetVersion, configPath string,
) *ConfigMigrationService[T] {
return &ConfigMigrationService[T]{
logger: orDefault(logger, log.New()),
configDir: configDir,
configName: configName,
targetVersion: targetVersion,
configPath: configPath,
configName, configPath string,
) *ConfigMigrator {
if logger == nil {
logger = log.New()
}
return &ConfigMigrator{
logger: logger,
configDir: configDir,
configName: configName,
configPath: configPath,
}
}
// MigrateConfig 迁移配置文件
func (cms *ConfigMigrationService[T]) MigrateConfig(existingConfig *koanf.Koanf) (*MigrationResult, error) {
currentVersion := orDefault(existingConfig.String("metadata.version"), "0.0.0")
result := &MigrationResult{
FromVersion: currentVersion,
ToVersion: cms.targetVersion,
// AutoMigrate automatically detects and migrates missing configuration fields
func (cm *ConfigMigrator) AutoMigrate(defaultConfig interface{}, currentConfig *koanf.Koanf) (*MigrationResult, error) {
// Load default config into temporary koanf instance
defaultKoanf := koanf.New(".")
if err := defaultKoanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
return nil, fmt.Errorf("failed to load default config: %w", err)
}
if needsMigration, err := cms.needsMigration(currentVersion); err != nil {
return result, fmt.Errorf("version comparison failed: %w", err)
} else if !needsMigration {
// Detect missing fields
missingFields := cm.detectMissingFields(currentConfig.All(), defaultKoanf.All())
// Create result object
result := &MigrationResult{
MissingFields: missingFields,
Migrated: len(missingFields) > 0,
Description: fmt.Sprintf("Detected %d missing configuration fields", len(missingFields)),
}
// If no missing fields, return early
if !result.Migrated {
cm.logger.Info("No missing configuration fields detected")
return result, nil
}
// 资源检查和备份
if err := cms.checkResourceLimits(); err != nil {
return result, fmt.Errorf("resource limit check failed: %w", err)
// Only create backup if we actually need to migrate (has missing fields)
if len(missingFields) > 0 {
if backupPath, err := cm.createBackup(); err != nil {
cm.logger.Error("Failed to create backup", "error", err)
} else {
result.BackupPath = backupPath
}
}
if backupPath, err := cms.createBackupOptimized(); err != nil {
return result, fmt.Errorf("backup creation failed: %w", err)
} else {
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)
}
// 自动恢复检查
cms.tryQuickRecovery(existingConfig)
// 执行迁移
if configUpdated, err := cms.performOptimizedMigration(existingConfig); err != nil {
return result, fmt.Errorf("migration failed: %w", err)
} else {
result.Migrated = true
result.ConfigUpdated = configUpdated
// Save updated config
if err := cm.saveConfig(currentConfig); err != nil {
return result, fmt.Errorf("failed to save updated config: %w", err)
}
cm.logger.Info("Configuration migration completed successfully", "migratedFields", len(missingFields))
return result, nil
}
// needsMigration 检查是否需要迁移
func (cms *ConfigMigrationService[T]) needsMigration(current string) (bool, error) {
currentVer, err := semver.NewVersion(current)
if err != nil {
return true, nil
}
targetVer, err := semver.NewVersion(cms.targetVersion)
if err != nil {
return false, fmt.Errorf("invalid target version: %s", cms.targetVersion)
}
return currentVer.LessThan(targetVer), nil
// detectMissingFields detects missing configuration fields
func (cm *ConfigMigrator) detectMissingFields(current, defaultConfig map[string]interface{}) []string {
var missingFields []string
cm.findMissingFieldsRecursive("", defaultConfig, current, &missingFields)
return missingFields
}
// checkResourceLimits 检查资源限制
func (cms *ConfigMigrationService[T]) checkResourceLimits() error {
if info, err := os.Stat(cms.configPath); err == nil && info.Size() > MaxConfigFileSize {
return fmt.Errorf("config file size (%d bytes) exceeds limit (%d bytes)", info.Size(), MaxConfigFileSize)
// findMissingFieldsRecursive recursively finds missing fields
func (cm *ConfigMigrator) findMissingFieldsRecursive(prefix string, defaultMap, currentMap map[string]interface{}, missing *[]string) {
for key, defaultVal := range defaultMap {
fullKey := key
if prefix != "" {
fullKey = prefix + "." + key
}
currentVal, exists := currentMap[key]
if !exists {
// Field is completely missing
*missing = append(*missing, fullKey)
} else {
// Check nested structures
if defaultNestedMap, ok := defaultVal.(map[string]interface{}); ok {
if currentNestedMap, ok := currentVal.(map[string]interface{}); ok {
cm.findMissingFieldsRecursive(fullKey, defaultNestedMap, currentNestedMap, missing)
} else {
// Current value is not a map but default is, structure mismatch
*missing = append(*missing, fullKey)
}
}
}
}
}
// mergeDefaultFields merges default values for missing fields into current config
func (cm *ConfigMigrator) mergeDefaultFields(current, defaultConfig *koanf.Koanf, missingFields []string) error {
for _, field := range missingFields {
defaultValue := defaultConfig.Get(field)
if defaultValue != nil {
current.Set(field, defaultValue)
cm.logger.Debug("Merged missing field", "field", field, "value", defaultValue)
}
}
// Update last modified timestamp
current.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339))
return nil
}
// createBackupOptimized 优化的备份创建
func (cms *ConfigMigrationService[T]) createBackupOptimized() (string, error) {
if _, err := os.Stat(cms.configPath); os.IsNotExist(err) {
// createBackup creates a backup of the configuration file
func (cm *ConfigMigrator) createBackup() (string, error) {
if _, err := os.Stat(cm.configPath); os.IsNotExist(err) {
return "", nil
}
configDir := cms.configDir
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))
// 单次扫描:删除旧备份并创建新备份
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())
data, err := os.ReadFile(cm.configPath)
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)
}
cm.logger.Info("Configuration backup created", "path", backupPath)
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 {
return false, fmt.Errorf("merged config size exceeds limit")
return fmt.Errorf("config size (%d bytes) exceeds limit (%d bytes)", len(configBytes), MaxConfigFileSize)
}
// 原子写入
return true, cms.atomicWrite(existingConfig, configBytes)
}
// atomicWrite 原子写入操作
func (cms *ConfigMigrationService[T]) atomicWrite(existingConfig *koanf.Koanf, configBytes []byte) error {
tempPath := cms.configPath + ".tmp"
// Atomic write
tempPath := cm.configPath + ".tmp"
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)
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
}
// 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)
}

View File

@@ -0,0 +1,167 @@
package services
import (
"encoding/json"
jsonparser "github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
"github.com/stretchr/testify/assert"
"github.com/wailsapp/wails/v3/pkg/services/log"
"os"
"path/filepath"
"testing"
)
// TestConfig represents a simplified config structure for testing
type TestConfig struct {
App struct {
Name string `json:"name"`
Version string `json:"version"`
Theme string `json:"theme"`
} `json:"app"`
User struct {
Name string `json:"name"`
Email string `json:"email"`
Settings struct {
AutoSave bool `json:"autoSave"`
Language string `json:"language"`
NewSetting bool `json:"newSetting"` // This field will be missing in old config
NewSetting2 string `json:"newSetting2"` // This field will be missing in old config
} `json:"settings"`
} `json:"user"`
NewSection struct {
Enabled bool `json:"enabled"`
Value string `json:"value"`
} `json:"newSection"` // This entire section will be missing in old config
}
// createTestConfig creates a test configuration file
func createTestConfig(t *testing.T, tempDir string) string {
// Old config without some fields
oldConfig := map[string]interface{}{
"app": map[string]interface{}{
"name": "TestApp",
"version": "1.0.0",
"theme": "dark",
},
"user": map[string]interface{}{
"name": "Test User",
"email": "test@example.com",
"settings": map[string]interface{}{
"autoSave": true,
"language": "en",
// Missing newSetting and newSetting2
},
},
// Missing newSection
}
// Create config file
configPath := filepath.Join(tempDir, "config.json")
jsonData, err := json.MarshalIndent(oldConfig, "", " ")
if err != nil {
t.Fatalf("Failed to marshal test config: %v", err)
}
err = os.WriteFile(configPath, jsonData, 0644)
if err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
return configPath
}
// TestConfigMigrator_AutoMigrate tests the ConfigMigrator's AutoMigrate functionality
func TestConfigMigrator_AutoMigrate(t *testing.T) {
// Create temp directory for test
tempDir, err := os.MkdirTemp("", "config_migrator_test")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tempDir)
// Create test config file
configPath := createTestConfig(t, tempDir)
// Create logger
logger := log.New()
// Create config migrator
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
// Create koanf instance and load the config
k := koanf.New(".")
fileProvider := file.Provider(configPath)
jsonParser := jsonparser.Parser()
if err := k.Load(fileProvider, jsonParser); err != nil {
t.Fatalf("Failed to load config: %v", err)
}
// Create default config with all fields
defaultConfig := TestConfig{}
defaultConfig.App.Name = "TestApp"
defaultConfig.App.Version = "1.0.0"
defaultConfig.App.Theme = "dark"
defaultConfig.User.Name = "Test User"
defaultConfig.User.Email = "test@example.com"
defaultConfig.User.Settings.AutoSave = true
defaultConfig.User.Settings.Language = "en"
defaultConfig.User.Settings.NewSetting = true // New field
defaultConfig.User.Settings.NewSetting2 = "value" // New field
defaultConfig.NewSection.Enabled = true // New section
defaultConfig.NewSection.Value = "new section" // New section
// Run auto migration
result, err := migrator.AutoMigrate(defaultConfig, k)
if err != nil {
t.Fatalf("Failed to auto migrate: %v", err)
}
// Assertions
assert.True(t, result.Migrated, "Migration should have been performed")
// 打印检测到的缺失字段,便于分析
t.Logf("Detected fields: %v", result.MissingFields)
// 验证检测到了正确数量的字段 - 实际检测到4个
assert.Equal(t, 4, len(result.MissingFields), "Should have detected 4 missing fields")
// 期望检测到的缺失字段
expectedFields := map[string]bool{
"user.settings.newSetting": true,
"user.settings.newSetting2": true,
"newSection.enabled": true,
"newSection.value": true,
}
// 验证所有预期的字段都被检测到了
for _, field := range result.MissingFields {
_, expected := expectedFields[field]
assert.True(t, expected, "Field %s was detected but not expected", field)
}
// 验证所有检测到的字段都在预期之内
for expectedField := range expectedFields {
found := false
for _, field := range result.MissingFields {
if field == expectedField {
found = true
break
}
}
assert.True(t, found, "Expected field %s was not detected", expectedField)
}
// Verify that the fields were actually added to the config
assert.True(t, k.Bool("user.settings.newSetting"), "newSetting should be added with correct value")
assert.Equal(t, "value", k.String("user.settings.newSetting2"), "newSetting2 should be added with correct value")
assert.True(t, k.Bool("newSection.enabled"), "newSection.enabled should be added with correct value")
assert.Equal(t, "new section", k.String("newSection.value"), "newSection.value should be added with correct value")
// Check that backup was created
backupFiles, err := filepath.Glob(filepath.Join(tempDir, "*.backup.*"))
if err != nil {
t.Fatalf("Failed to list backup files: %v", err)
}
assert.Equal(t, 1, len(backupFiles), "One backup file should have been created")
}

View File

@@ -24,6 +24,8 @@ const (
ConfigChangeTypeDataPath ConfigChangeType = "datapath"
// ConfigChangeTypeBackup 备份配置变更
ConfigChangeTypeBackup ConfigChangeType = "backup"
// ConfigChangeTypeWindowSnap 窗口吸附配置变更
ConfigChangeTypeWindowSnap ConfigChangeType = "windowsnap"
)
// 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 防抖通知
func (cns *ConfigNotificationService) debounceNotify(listener *ConfigListener, oldConfig, newConfig *models.AppConfig) {
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 关闭服务
func (cns *ConfigNotificationService) ServiceShutdown() error {
cns.Cleanup()

View File

@@ -27,8 +27,9 @@ type ConfigService struct {
// 配置通知服务
notificationService *ConfigNotificationService
// 配置迁移服务
migrationService *ConfigMigrationService[*models.AppConfig]
// 配置迁移器
configMigrator *ConfigMigrator
}
// ConfigError 配置错误
@@ -67,16 +68,18 @@ func NewConfigService(logger *log.LogService) *ConfigService {
settingsPath := filepath.Join(configDir, "settings.json")
cs := &ConfigService{
logger: logger,
configDir: configDir,
settingsPath: settingsPath,
koanf: koanf.New("."),
migrationService: NewAppConfigMigrationService(logger, configDir, settingsPath),
logger: logger,
configDir: configDir,
settingsPath: settingsPath,
koanf: koanf.New("."),
}
// 初始化配置通知服务
cs.notificationService = NewConfigNotificationService(cs.koanf, logger)
// 初始化配置迁移器
cs.configMigrator = NewConfigMigrator(logger, configDir, "settings", settingsPath)
cs.initConfig()
// 启动配置文件监听
@@ -106,23 +109,36 @@ func (cs *ConfigService) initConfig() error {
return cs.createDefaultConfig()
}
// 配置文件存在,先加载现有配置
// 检查并自动迁移配置
cs.checkConfigMigration()
// 配置文件存在,直接加载现有配置
cs.fileProvider = file.Provider(cs.settingsPath)
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
return &ConfigError{Operation: "load_config_file", Err: err}
}
// 检查并执行配置迁移
if cs.migrationService != nil {
result, err := cs.migrationService.MigrateConfig(cs.koanf)
if err != nil {
return &ConfigError{Operation: "migrate_config", Err: err}
}
return nil
}
if result.Migrated && result.ConfigUpdated {
// 迁移完成且配置已更新,重新创建文件提供器以监听新文件
cs.fileProvider = file.Provider(cs.settingsPath)
}
// checkConfigMigration 检查配置迁移
func (cs *ConfigService) checkConfigMigration() error {
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
@@ -308,6 +324,16 @@ func (cs *ConfigService) SetBackupConfigChangeCallback(callback func(config *mod
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 关闭服务
func (cs *ConfigService) ServiceShutdown() error {
cs.stopWatching()
@@ -316,3 +342,13 @@ func (cs *ConfigService) ServiceShutdown() error {
}
return nil
}
// GetConfigDir 获取配置目录
func (cs *ConfigService) GetConfigDir() string {
return cs.configDir
}
// GetSettingsPath 获取设置文件路径
func (cs *ConfigService) GetSettingsPath() string {
return cs.settingsPath
}

View File

@@ -61,14 +61,16 @@ func (hs *HotkeyService) setCancelFunc(cancel context.CancelFunc) {
// getCancelFunc 原子地获取cancel函数
func (hs *HotkeyService) getCancelFunc() context.CancelFunc {
if cancel := hs.cancelFunc.Load(); cancel != nil {
return cancel.(context.CancelFunc)
if cancelFunc, ok := cancel.(context.CancelFunc); ok {
return cancelFunc
}
}
return nil
}
// clearCancelFunc 原子地清除cancel函数
func (hs *HotkeyService) clearCancelFunc() {
hs.cancelFunc.Store((*context.CancelFunc)(nil))
hs.cancelFunc.Store((context.CancelFunc)(nil))
}
// NewHotkeyService 创建热键服务实例

View File

@@ -66,6 +66,7 @@ int isHotkeyRegistered() {
import "C"
import (
"context"
"fmt"
"sync"
"sync/atomic"
@@ -115,14 +116,16 @@ func (hs *HotkeyService) setCancelFunc(cancel context.CancelFunc) {
// getCancelFunc 原子地获取cancel函数
func (hs *HotkeyService) getCancelFunc() context.CancelFunc {
if cancel := hs.cancelFunc.Load(); cancel != nil {
return cancel.(context.CancelFunc)
if cancelFunc, ok := cancel.(context.CancelFunc); ok {
return cancelFunc
}
}
return nil
}
// clearCancelFunc 原子地清除cancel函数
func (hs *HotkeyService) clearCancelFunc() {
hs.cancelFunc.Store((*context.CancelFunc)(nil))
hs.cancelFunc.Store((context.CancelFunc)(nil))
}
// NewHotkeyService 创建新的热键服务实例

View File

@@ -179,14 +179,16 @@ func (hs *HotkeyService) setCancelFunc(cancel context.CancelFunc) {
// getCancelFunc 原子地获取cancel函数
func (hs *HotkeyService) getCancelFunc() context.CancelFunc {
if cancel := hs.cancelFunc.Load(); cancel != nil {
return cancel.(context.CancelFunc)
if cancelFunc, ok := cancel.(context.CancelFunc); ok {
return cancelFunc
}
}
return nil
}
// clearCancelFunc 原子地清除cancel函数
func (hs *HotkeyService) clearCancelFunc() {
hs.cancelFunc.Store((*context.CancelFunc)(nil))
hs.cancelFunc.Store((context.CancelFunc)(nil))
}
// NewHotkeyService 创建热键服务实例

View File

@@ -13,6 +13,7 @@ type ServiceManager struct {
databaseService *DatabaseService
documentService *DocumentService
windowService *WindowService
windowSnapService *WindowSnapService
migrationService *MigrationService
systemService *SystemService
hotkeyService *HotkeyService
@@ -48,6 +49,12 @@ func NewServiceManager() *ServiceManager {
// 初始化窗口服务
windowService := NewWindowService(logger, documentService)
// 初始化窗口吸附服务
windowSnapService := NewWindowSnapService(logger, configService)
// 将吸附服务与窗口服务关联
windowService.SetWindowSnapService(windowSnapService)
// 初始化系统服务
systemService := NewSystemService(logger)
@@ -108,11 +115,20 @@ func NewServiceManager() *ServiceManager {
panic(err)
}
// 设置窗口吸附配置变更回调
err = configService.SetWindowSnapConfigChangeCallback(func(enabled bool) error {
return windowSnapService.OnWindowSnapConfigChanged(enabled)
})
if err != nil {
panic(err)
}
return &ServiceManager{
configService: configService,
databaseService: databaseService,
documentService: documentService,
windowService: windowService,
windowSnapService: windowSnapService,
migrationService: migrationService,
systemService: systemService,
hotkeyService: hotkeyService,
@@ -136,6 +152,7 @@ func (sm *ServiceManager) GetServices() []application.Service {
application.NewService(sm.databaseService),
application.NewService(sm.documentService),
application.NewService(sm.windowService),
application.NewService(sm.windowSnapService),
application.NewService(sm.keyBindingService),
application.NewService(sm.extensionService),
application.NewService(sm.migrationService),
@@ -221,3 +238,8 @@ func (sm *ServiceManager) GetDocumentService() *DocumentService {
func (sm *ServiceManager) GetThemeService() *ThemeService {
return sm.themeService
}
// GetWindowSnapService 获取窗口吸附服务实例
func (sm *ServiceManager) GetWindowSnapService() *WindowSnapService {
return sm.windowSnapService
}

View File

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

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.SetLabel("voidraft")
systray.Label()
// 设置图标
iconBytes, _ := assets.ReadFile("appicon.png")
systray.SetIcon(iconBytes)
// 设置标签
systray.SetLabel("VoidRaft")
// 创建托盘菜单
menu := app.NewMenu()

View File

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

73
scripts/version.bat Normal file
View File

@@ -0,0 +1,73 @@
@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 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%

50
scripts/version.sh Normal file
View File

@@ -0,0 +1,50 @@
#!/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
# 获取最新的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.5