12 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
6adeadeed4 🐛 Fixed document deadlock issue 2025-08-19 00:24:51 +08:00
7b70a39b23 🐛 Fixed hotkey service issues 2025-08-19 00:08:50 +08:00
873a3c0e60 Modify the theme storage schema 2025-08-17 19:34:35 +08:00
5b88efcfbe 🐛 Fixed the window toggle maximise issue 2025-08-17 14:51:39 +08:00
78 changed files with 3026 additions and 1548 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -114,11 +114,6 @@ export class AppearanceConfig {
*/ */
"systemTheme": SystemThemeType; "systemTheme": SystemThemeType;
/**
* 自定义主题配置
*/
"customTheme": CustomThemeConfig;
/** Creates a new AppearanceConfig instance. */ /** Creates a new AppearanceConfig instance. */
constructor($$source: Partial<AppearanceConfig> = {}) { constructor($$source: Partial<AppearanceConfig> = {}) {
if (!("language" in $$source)) { if (!("language" in $$source)) {
@@ -127,9 +122,6 @@ export class AppearanceConfig {
if (!("systemTheme" in $$source)) { if (!("systemTheme" in $$source)) {
this["systemTheme"] = ("" as SystemThemeType); this["systemTheme"] = ("" as SystemThemeType);
} }
if (!("customTheme" in $$source)) {
this["customTheme"] = (new CustomThemeConfig());
}
Object.assign(this, $$source); Object.assign(this, $$source);
} }
@@ -138,11 +130,7 @@ export class AppearanceConfig {
* Creates a new AppearanceConfig instance from a string or object. * Creates a new AppearanceConfig instance from a string or object.
*/ */
static createFrom($$source: any = {}): AppearanceConfig { static createFrom($$source: any = {}): AppearanceConfig {
const $$createField2_0 = $$createType6;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("customTheme" in $$parsedSource) {
$$parsedSource["customTheme"] = $$createField2_0($$parsedSource["customTheme"]);
}
return new AppearanceConfig($$parsedSource as Partial<AppearanceConfig>); return new AppearanceConfig($$parsedSource as Partial<AppearanceConfig>);
} }
} }
@@ -201,49 +189,6 @@ export class ConfigMetadata {
} }
} }
/**
* CustomThemeConfig 自定义主题配置
*/
export class CustomThemeConfig {
/**
* 深色主题配置
*/
"darkTheme": ThemeColorConfig;
/**
* 浅色主题配置
*/
"lightTheme": ThemeColorConfig;
/** Creates a new CustomThemeConfig instance. */
constructor($$source: Partial<CustomThemeConfig> = {}) {
if (!("darkTheme" in $$source)) {
this["darkTheme"] = (new ThemeColorConfig());
}
if (!("lightTheme" in $$source)) {
this["lightTheme"] = (new ThemeColorConfig());
}
Object.assign(this, $$source);
}
/**
* Creates a new CustomThemeConfig instance from a string or object.
*/
static createFrom($$source: any = {}): CustomThemeConfig {
const $$createField0_0 = $$createType7;
const $$createField1_0 = $$createType7;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("darkTheme" in $$parsedSource) {
$$parsedSource["darkTheme"] = $$createField0_0($$parsedSource["darkTheme"]);
}
if ("lightTheme" in $$parsedSource) {
$$parsedSource["lightTheme"] = $$createField1_0($$parsedSource["lightTheme"]);
}
return new CustomThemeConfig($$parsedSource as Partial<CustomThemeConfig>);
}
}
/** /**
* Document represents a document in the system * Document represents a document in the system
*/ */
@@ -428,7 +373,7 @@ export class Extension {
* Creates a new Extension instance from a string or object. * Creates a new Extension instance from a string or object.
*/ */
static createFrom($$source: any = {}): Extension { static createFrom($$source: any = {}): Extension {
const $$createField3_0 = $$createType8; const $$createField3_0 = $$createType6;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("config" in $$parsedSource) { if ("config" in $$parsedSource) {
$$parsedSource["config"] = $$createField3_0($$parsedSource["config"]); $$parsedSource["config"] = $$createField3_0($$parsedSource["config"]);
@@ -522,6 +467,12 @@ export class GeneralConfig {
*/ */
"startAtLogin": boolean; "startAtLogin": boolean;
/**
* 窗口吸附设置
* 是否启用窗口吸附功能(阈值现在是自适应的)
*/
"enableWindowSnap": boolean;
/** /**
* 全局热键设置 * 全局热键设置
* 是否启用全局热键 * 是否启用全局热键
@@ -547,6 +498,9 @@ export class GeneralConfig {
if (!("startAtLogin" in $$source)) { if (!("startAtLogin" in $$source)) {
this["startAtLogin"] = false; this["startAtLogin"] = false;
} }
if (!("enableWindowSnap" in $$source)) {
this["enableWindowSnap"] = false;
}
if (!("enableGlobalHotkey" in $$source)) { if (!("enableGlobalHotkey" in $$source)) {
this["enableGlobalHotkey"] = false; this["enableGlobalHotkey"] = false;
} }
@@ -561,10 +515,10 @@ export class GeneralConfig {
* Creates a new GeneralConfig instance from a string or object. * Creates a new GeneralConfig instance from a string or object.
*/ */
static createFrom($$source: any = {}): GeneralConfig { static createFrom($$source: any = {}): GeneralConfig {
const $$createField5_0 = $$createType10; const $$createField6_0 = $$createType8;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("globalHotkey" in $$parsedSource) { if ("globalHotkey" in $$parsedSource) {
$$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]); $$parsedSource["globalHotkey"] = $$createField6_0($$parsedSource["globalHotkey"]);
} }
return new GeneralConfig($$parsedSource as Partial<GeneralConfig>); return new GeneralConfig($$parsedSource as Partial<GeneralConfig>);
} }
@@ -1171,6 +1125,58 @@ export enum TabType {
TabTypeTab = "tab", TabTypeTab = "tab",
}; };
/**
* Theme 主题数据库模型
*/
export class Theme {
"id": number;
"name": string;
"type": ThemeType;
"colors": ThemeColorConfig;
"isDefault": boolean;
"createdAt": time$0.Time;
"updatedAt": time$0.Time;
/** Creates a new Theme instance. */
constructor($$source: Partial<Theme> = {}) {
if (!("id" in $$source)) {
this["id"] = 0;
}
if (!("name" in $$source)) {
this["name"] = "";
}
if (!("type" in $$source)) {
this["type"] = ("" as ThemeType);
}
if (!("colors" in $$source)) {
this["colors"] = (new ThemeColorConfig());
}
if (!("isDefault" in $$source)) {
this["isDefault"] = false;
}
if (!("createdAt" in $$source)) {
this["createdAt"] = null;
}
if (!("updatedAt" in $$source)) {
this["updatedAt"] = null;
}
Object.assign(this, $$source);
}
/**
* Creates a new Theme instance from a string or object.
*/
static createFrom($$source: any = {}): Theme {
const $$createField3_0 = $$createType9;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("colors" in $$parsedSource) {
$$parsedSource["colors"] = $$createField3_0($$parsedSource["colors"]);
}
return new Theme($$parsedSource as Partial<Theme>);
}
}
/** /**
* ThemeColorConfig 主题颜色配置 * ThemeColorConfig 主题颜色配置
*/ */
@@ -1379,6 +1385,19 @@ export class ThemeColorConfig {
} }
} }
/**
* ThemeType 主题类型枚举
*/
export enum ThemeType {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
ThemeTypeDark = "dark",
ThemeTypeLight = "light",
};
/** /**
* UpdateSourceType 更新源类型 * UpdateSourceType 更新源类型
*/ */
@@ -1477,8 +1496,8 @@ export class UpdatesConfig {
* Creates a new UpdatesConfig instance from a string or object. * Creates a new UpdatesConfig instance from a string or object.
*/ */
static createFrom($$source: any = {}): UpdatesConfig { static createFrom($$source: any = {}): UpdatesConfig {
const $$createField6_0 = $$createType11; const $$createField6_0 = $$createType10;
const $$createField7_0 = $$createType12; const $$createField7_0 = $$createType11;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("github" in $$parsedSource) { if ("github" in $$parsedSource) {
$$parsedSource["github"] = $$createField6_0($$parsedSource["github"]); $$parsedSource["github"] = $$createField6_0($$parsedSource["github"]);
@@ -1497,15 +1516,14 @@ const $$createType2 = AppearanceConfig.createFrom;
const $$createType3 = UpdatesConfig.createFrom; const $$createType3 = UpdatesConfig.createFrom;
const $$createType4 = GitBackupConfig.createFrom; const $$createType4 = GitBackupConfig.createFrom;
const $$createType5 = ConfigMetadata.createFrom; const $$createType5 = ConfigMetadata.createFrom;
const $$createType6 = CustomThemeConfig.createFrom; var $$createType6 = (function $$initCreateType6(...args): any {
const $$createType7 = ThemeColorConfig.createFrom; if ($$createType6 === $$initCreateType6) {
var $$createType8 = (function $$initCreateType8(...args): any { $$createType6 = $$createType7;
if ($$createType8 === $$initCreateType8) {
$$createType8 = $$createType9;
} }
return $$createType8(...args); return $$createType6(...args);
}); });
const $$createType9 = $Create.Map($Create.Any, $Create.Any); const $$createType7 = $Create.Map($Create.Any, $Create.Any);
const $$createType10 = HotkeyCombo.createFrom; const $$createType8 = HotkeyCombo.createFrom;
const $$createType11 = GithubConfig.createFrom; const $$createType9 = ThemeColorConfig.createFrom;
const $$createType12 = GiteaConfig.createFrom; const $$createType10 = GithubConfig.createFrom;
const $$createType11 = GiteaConfig.createFrom;

View File

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

View File

@@ -32,8 +32,8 @@ export function GetCurrentHotkey(): Promise<models$0.HotkeyCombo | null> & { can
/** /**
* Initialize 初始化热键服务 * Initialize 初始化热键服务
*/ */
export function Initialize(app: application$0.App | null): Promise<void> & { cancel(): void } { export function Initialize(app: application$0.App | null, mainWindow: application$0.WebviewWindow | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3671360458, app) as any; let $resultPromise = $Call.ByID(3671360458, app, mainWindow) as any;
return $resultPromise; return $resultPromise;
} }

View File

@@ -13,9 +13,11 @@ import * as MigrationService from "./migrationservice.js";
import * as SelfUpdateService from "./selfupdateservice.js"; import * as SelfUpdateService from "./selfupdateservice.js";
import * as StartupService from "./startupservice.js"; import * as StartupService from "./startupservice.js";
import * as SystemService from "./systemservice.js"; import * as SystemService from "./systemservice.js";
import * as ThemeService from "./themeservice.js";
import * as TranslationService from "./translationservice.js"; import * as TranslationService from "./translationservice.js";
import * as TrayService from "./trayservice.js"; import * as TrayService from "./trayservice.js";
import * as WindowService from "./windowservice.js"; import * as WindowService from "./windowservice.js";
import * as WindowSnapService from "./windowsnapservice.js";
export { export {
BackupService, BackupService,
ConfigService, ConfigService,
@@ -29,9 +31,11 @@ export {
SelfUpdateService, SelfUpdateService,
StartupService, StartupService,
SystemService, SystemService,
ThemeService,
TranslationService, TranslationService,
TrayService, TrayService,
WindowService WindowService,
WindowSnapService
}; };
export * from "./models.js"; export * from "./models.js";

View File

@@ -203,7 +203,7 @@ export class SelfUpdateResult {
} }
/** /**
* WindowInfo 窗口信息 * WindowInfo 窗口信息(简化版)
*/ */
export class WindowInfo { export class WindowInfo {
"Window": application$0.WebviewWindow | null; "Window": application$0.WebviewWindow | null;
@@ -238,6 +238,26 @@ export class WindowInfo {
} }
} }
/**
* WindowSnapService 窗口吸附服务
*/
export class WindowSnapService {
/** Creates a new WindowSnapService instance. */
constructor($$source: Partial<WindowSnapService> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new WindowSnapService instance from a string or object.
*/
static createFrom($$source: any = {}): WindowSnapService {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new WindowSnapService($$parsedSource as Partial<WindowSnapService>);
}
}
// Private type creation functions // Private type creation functions
const $$createType0 = application$0.WebviewWindow.createFrom; const $$createType0 = application$0.WebviewWindow.createFrom;
const $$createType1 = $Create.Nullable($$createType0); const $$createType1 = $Create.Nullable($$createType0);

View File

@@ -0,0 +1,104 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* ThemeService 主题服务
* @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";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as models$0 from "../models/models.js";
/**
* CreateTheme 创建新主题
*/
export function CreateTheme(theme: models$0.Theme | null): Promise<models$0.Theme | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3274757686, theme) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetAllThemes 获取所有主题
*/
export function GetAllThemes(): Promise<(models$0.Theme | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(2425053076) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetDefaultThemes 获取默认主题
*/
export function GetDefaultThemes(): Promise<{ [_: string]: models$0.Theme | null }> & { cancel(): void } {
let $resultPromise = $Call.ByID(3801788118) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType3($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetThemeByType 根据类型获取默认主题
*/
export function GetThemeByType(themeType: models$0.ThemeType): Promise<models$0.Theme | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(1680465265, themeType) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* ResetThemeColors 重置主题颜色为默认值
*/
export function ResetThemeColors(themeType: models$0.ThemeType): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(342461245, themeType) as any;
return $resultPromise;
}
/**
* ServiceShutdown 服务关闭
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1676749034) as any;
return $resultPromise;
}
/**
* ServiceStartup 服务启动时初始化
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2915959937, options) as any;
return $resultPromise;
}
/**
* UpdateThemeColors 更新主题颜色
*/
export function UpdateThemeColors(themeType: models$0.ThemeType, colors: models$0.ThemeColorConfig): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2750902529, themeType, colors) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.Theme.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1);
const $$createType3 = $Create.Map($Create.Any, $$createType1);

View File

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

View File

@@ -0,0 +1,79 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* WindowSnapService 窗口吸附服务
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
/**
* Cleanup 清理资源
*/
export function Cleanup(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2155505498) as any;
return $resultPromise;
}
/**
* GetCurrentThreshold 获取当前自适应阈值(用于调试或显示)
*/
export function GetCurrentThreshold(): Promise<number> & { cancel(): void } {
let $resultPromise = $Call.ByID(3176419026) as any;
return $resultPromise;
}
/**
* OnWindowSnapConfigChanged 处理窗口吸附配置变更
*/
export function OnWindowSnapConfigChanged(enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3794787039, enabled) as any;
return $resultPromise;
}
/**
* RegisterWindow 注册需要吸附管理的窗口
*/
export function RegisterWindow(documentID: number, window: application$0.WebviewWindow | null, title: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1000222723, documentID, window, title) as any;
return $resultPromise;
}
/**
* ServiceShutdown 实现服务关闭接口
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1172710495) as any;
return $resultPromise;
}
/**
* SetAppReferences 设置应用和主窗口引用
*/
export function SetAppReferences(app: application$0.App | null, mainWindow: application$0.WebviewWindow | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1782093351, app, mainWindow) as any;
return $resultPromise;
}
/**
* SetSnapEnabled 设置是否启用窗口吸附
*/
export function SetSnapEnabled(enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2280126835, enabled) as any;
return $resultPromise;
}
/**
* UnregisterWindow 取消注册窗口
*/
export function UnregisterWindow(documentID: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2844230768, documentID) as any;
return $resultPromise;
}

File diff suppressed because it is too large Load Diff

View File

@@ -24,18 +24,18 @@
"@codemirror/lang-json": "^6.0.2", "@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-less": "^6.0.2", "@codemirror/lang-less": "^6.0.2",
"@codemirror/lang-lezer": "^6.0.2", "@codemirror/lang-lezer": "^6.0.2",
"@codemirror/lang-liquid": "^6.2.3", "@codemirror/lang-liquid": "^6.3.0",
"@codemirror/lang-markdown": "^6.3.3", "@codemirror/lang-markdown": "^6.3.4",
"@codemirror/lang-php": "^6.0.2", "@codemirror/lang-php": "^6.0.2",
"@codemirror/lang-python": "^6.2.1", "@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-rust": "^6.0.2", "@codemirror/lang-rust": "^6.0.2",
"@codemirror/lang-sass": "^6.0.2", "@codemirror/lang-sass": "^6.0.2",
"@codemirror/lang-sql": "^6.9.0", "@codemirror/lang-sql": "^6.9.1",
"@codemirror/lang-vue": "^0.1.3", "@codemirror/lang-vue": "^0.1.3",
"@codemirror/lang-wast": "^6.0.2", "@codemirror/lang-wast": "^6.0.2",
"@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.2", "@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.11.2", "@codemirror/language": "^6.11.3",
"@codemirror/language-data": "^6.5.1", "@codemirror/language-data": "^6.5.1",
"@codemirror/legacy-modes": "^6.5.1", "@codemirror/legacy-modes": "^6.5.1",
"@codemirror/lint": "^6.8.5", "@codemirror/lint": "^6.8.5",
@@ -52,30 +52,30 @@
"hsl-matcher": "^1.2.4", "hsl-matcher": "^1.2.4",
"lezer": "^0.13.5", "lezer": "^0.13.5",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.4.1", "pinia-plugin-persistedstate": "^4.5.0",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"remarkable": "^2.0.1", "remarkable": "^2.0.1",
"sass": "^1.89.2", "sass": "^1.90.0",
"vue": "^3.5.17", "vue": "^3.5.19",
"vue-i18n": "^11.1.10", "vue-i18n": "^11.1.11",
"vue-pick-colors": "^1.8.0", "vue-pick-colors": "^1.8.0",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.31.0", "@eslint/js": "^9.34.0",
"@lezer/generator": "^1.8.0", "@lezer/generator": "^1.8.0",
"@types/node": "^24.0.14", "@types/node": "^24.3.0",
"@types/remarkable": "^2.0.8", "@types/remarkable": "^2.0.8",
"@vitejs/plugin-vue": "^6.0.0", "@vitejs/plugin-vue": "^6.0.1",
"@wailsio/runtime": "latest", "@wailsio/runtime": "latest",
"eslint": "^9.31.0", "eslint": "^9.34.0",
"eslint-plugin-vue": "^10.3.0", "eslint-plugin-vue": "^10.4.0",
"globals": "^16.3.0", "globals": "^16.3.0",
"typescript": "^5.8.3", "typescript": "^5.9.2",
"typescript-eslint": "^8.37.0", "typescript-eslint": "^8.40.0",
"unplugin-vue-components": "^28.8.0", "unplugin-vue-components": "^29.0.0",
"vite": "^7.0.4", "vite": "^7.1.3",
"vue-eslint-parser": "^10.2.0", "vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.0.1" "vue-tsc": "^3.0.6"
} }
} }

View File

@@ -2,16 +2,16 @@
<div class="linux-titlebar" style="--wails-draggable:drag" @contextmenu.prevent> <div class="linux-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent> <div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
<div class="titlebar-icon"> <div class="titlebar-icon">
<img src="/appicon.png" alt="voidraft" /> <img src="/appicon.png" alt="voidraft"/>
</div> </div>
<div class="titlebar-title">{{ titleText }}</div> <div class="titlebar-title">{{ titleText }}</div>
</div> </div>
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent> <div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
<button <button
class="titlebar-button minimize-button" class="titlebar-button minimize-button"
@click="minimizeWindow" @click="minimizeWindow"
:title="t('titlebar.minimize')" :title="t('titlebar.minimize')"
> >
<svg width="16" height="16" viewBox="0 0 16 16"> <svg width="16" height="16" viewBox="0 0 16 16">
<path d="M4 8h8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> <path d="M4 8h8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
@@ -19,9 +19,9 @@
</button> </button>
<button <button
class="titlebar-button maximize-button" class="titlebar-button maximize-button"
@click="toggleMaximize" @click="toggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')" :title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
> >
<svg width="16" height="16" viewBox="0 0 16 16" v-if="!isMaximized"> <svg width="16" height="16" viewBox="0 0 16 16" v-if="!isMaximized">
<rect x="4" y="4" width="8" height="8" fill="none" stroke="currentColor" stroke-width="2"/> <rect x="4" y="4" width="8" height="8" fill="none" stroke="currentColor" stroke-width="2"/>
@@ -33,9 +33,9 @@
</button> </button>
<button <button
class="titlebar-button close-button" class="titlebar-button close-button"
@click="closeWindow" @click="closeWindow"
:title="t('titlebar.close')" :title="t('titlebar.close')"
> >
<svg width="16" height="16" viewBox="0 0 16 16"> <svg width="16" height="16" viewBox="0 0 16 16">
<path d="M4 4l8 8m0-8L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> <path d="M4 4l8 8m0-8L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
@@ -46,85 +46,56 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'; import {computed, onMounted, ref} from 'vue';
import { useI18n } from 'vue-i18n'; import {useI18n} from 'vue-i18n';
import * as runtime from '@wailsio/runtime'; import * as runtime from '@wailsio/runtime';
import { useWindowStore } from '@/stores/windowStore'; import {useDocumentStore} from '@/stores/documentStore';
import { useDocumentStore } from '@/stores/documentStore';
const { t } = useI18n(); const {t} = useI18n();
const isMaximized = ref(false); const isMaximized = ref(false);
const documentStore = useDocumentStore(); const documentStore = useDocumentStore();
const minimizeWindow = async () => {
try {
await runtime.Window.Minimise();
} catch (error) {
// Error handling
}
};
const toggleMaximize = async () => {
try {
const newState = !isMaximized.value;
isMaximized.value = newState;
if (newState) {
await runtime.Window.Maximise();
} else {
await runtime.Window.UnMaximise();
}
setTimeout(async () => {
await checkMaximizedState();
}, 100);
} catch (error) {
isMaximized.value = !isMaximized.value;
}
};
const closeWindow = async () => {
try {
await runtime.Window.Close();
} catch (error) {
// Error handling
}
};
const checkMaximizedState = async () => {
try {
isMaximized.value = await runtime.Window.IsMaximised();
} catch (error) {
// Error handling
}
};
// 计算标题文本 // 计算标题文本
const titleText = computed(() => { const titleText = computed(() => {
const currentDoc = documentStore.currentDocument; const currentDoc = documentStore.currentDocument;
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft'; return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
}); });
const minimizeWindow = async () => {
try {
await runtime.Window.Minimise();
} catch (error) {
console.error(error);
}
};
const toggleMaximize = async () => {
try {
await runtime.Window.ToggleMaximise();
await checkMaximizedState();
} catch (error) {
console.error(error);
}
};
const closeWindow = async () => {
try {
await runtime.Window.Close();
} catch (error) {
console.error(error);
}
};
const checkMaximizedState = async () => {
try {
isMaximized.value = await runtime.Window.IsMaximised();
} catch (error) {
console.error(error);
}
};
onMounted(async () => { onMounted(async () => {
await checkMaximizedState(); await checkMaximizedState();
runtime.Events.On('window:maximised', () => {
isMaximized.value = true;
});
runtime.Events.On('window:unmaximised', () => {
isMaximized.value = false;
});
runtime.Events.On('window:focus', async () => {
await checkMaximizedState();
});
});
onUnmounted(() => {
runtime.Events.Off('window:maximised');
runtime.Events.Off('window:unmaximised');
runtime.Events.Off('window:focus');
}); });
</script> </script>

View File

@@ -53,7 +53,6 @@
import { ref, onMounted, onUnmounted, computed } from 'vue'; import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import * as runtime from '@wailsio/runtime'; import * as runtime from '@wailsio/runtime';
import { useWindowStore } from '@/stores/windowStore';
import { useDocumentStore } from '@/stores/documentStore'; import { useDocumentStore } from '@/stores/documentStore';
const { t } = useI18n(); const { t } = useI18n();
@@ -65,26 +64,16 @@ const minimizeWindow = async () => {
try { try {
await runtime.Window.Minimise(); await runtime.Window.Minimise();
} catch (error) { } catch (error) {
// Error handling console.error(error)
} }
}; };
const toggleMaximize = async () => { const toggleMaximize = async () => {
try { try {
const newState = !isMaximized.value; await runtime.Window.ToggleMaximise();
isMaximized.value = newState; await checkMaximizedState();
if (newState) {
await runtime.Window.Maximise();
} else {
await runtime.Window.UnMaximise();
}
setTimeout(async () => {
await checkMaximizedState();
}, 100);
} catch (error) { } catch (error) {
isMaximized.value = !isMaximized.value; console.error(error)
} }
}; };
@@ -92,7 +81,7 @@ const closeWindow = async () => {
try { try {
await runtime.Window.Close(); await runtime.Window.Close();
} catch (error) { } catch (error) {
// Error handling console.error(error)
} }
}; };
@@ -100,7 +89,7 @@ const checkMaximizedState = async () => {
try { try {
isMaximized.value = await runtime.Window.IsMaximised(); isMaximized.value = await runtime.Window.IsMaximised();
} catch (error) { } catch (error) {
// Error handling console.error(error)
} }
}; };
@@ -112,24 +101,6 @@ const titleText = computed(() => {
onMounted(async () => { onMounted(async () => {
await checkMaximizedState(); await checkMaximizedState();
runtime.Events.On('window:maximised', () => {
isMaximized.value = true;
});
runtime.Events.On('window:unmaximised', () => {
isMaximized.value = false;
});
runtime.Events.On('window:focus', async () => {
await checkMaximizedState();
});
});
onUnmounted(() => {
runtime.Events.Off('window:maximised');
runtime.Events.Off('window:unmaximised');
runtime.Events.Off('window:focus');
}); });
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="windows-titlebar" style="--wails-draggable:drag" @contextmenu.prevent> <div class="windows-titlebar" style="--wails-draggable:drag">
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent> <div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
<div class="titlebar-icon"> <div class="titlebar-icon">
<img src="/appicon.png" alt="voidraft"/> <img src="/appicon.png" alt="voidraft"/>
@@ -36,11 +36,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {computed, onMounted, onUnmounted, ref} from 'vue'; import {computed, onMounted, ref} from 'vue';
import {useI18n} from 'vue-i18n'; import {useI18n} from 'vue-i18n';
import * as runtime from '@wailsio/runtime'; import * as runtime from '@wailsio/runtime';
import { useWindowStore } from '@/stores/windowStore'; import {useDocumentStore} from '@/stores/documentStore';
import { useDocumentStore } from '@/stores/documentStore';
const {t} = useI18n(); const {t} = useI18n();
const isMaximized = ref(false); const isMaximized = ref(false);
@@ -59,30 +58,16 @@ const minimizeWindow = async () => {
try { try {
await runtime.Window.Minimise(); await runtime.Window.Minimise();
} catch (error) { } catch (error) {
// Error handling console.error(error);
} }
}; };
const toggleMaximize = async () => { const toggleMaximize = async () => {
try { try {
// 立即更新UI状态提供即时反馈 await runtime.Window.ToggleMaximise();
const newState = !isMaximized.value; await checkMaximizedState();
isMaximized.value = newState;
// 然后执行实际操作
if (newState) {
await runtime.Window.Maximise();
} else {
await runtime.Window.UnMaximise();
}
// 操作完成后再次确认状态(防止操作失败时状态不一致)
setTimeout(async () => {
await checkMaximizedState();
}, 100);
} catch (error) { } catch (error) {
// 如果操作失败,恢复原状态 console.error(error);
isMaximized.value = !isMaximized.value;
} }
}; };
@@ -90,7 +75,7 @@ const closeWindow = async () => {
try { try {
await runtime.Window.Close(); await runtime.Window.Close();
} catch (error) { } catch (error) {
// Error handling console.error(error);
} }
}; };
@@ -98,31 +83,12 @@ const checkMaximizedState = async () => {
try { try {
isMaximized.value = await runtime.Window.IsMaximised(); isMaximized.value = await runtime.Window.IsMaximised();
} catch (error) { } catch (error) {
// Error handling console.error(error);
} }
}; };
onMounted(async () => { onMounted(async () => {
await checkMaximizedState(); await checkMaximizedState();
runtime.Events.On('window:maximised', () => {
isMaximized.value = true;
});
runtime.Events.On('window:unmaximised', () => {
isMaximized.value = false;
});
runtime.Events.On('window:focus', async () => {
await checkMaximizedState();
});
});
onUnmounted(() => {
runtime.Events.Off('window:maximised');
runtime.Events.Off('window:unmaximised');
runtime.Events.Off('window:focus');
}); });
</script> </script>

View File

@@ -107,6 +107,12 @@ const selectItem = async (item: any) => {
// 选择文档 // 选择文档
const selectDoc = async (doc: Document) => { const selectDoc = async (doc: Document) => {
try { try {
// 如果选择的就是当前文档,直接关闭菜单
if (documentStore.currentDocument?.id === doc.id) {
closeMenu();
return;
}
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id); const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
if (hasOpen) { if (hasOpen) {
// 设置错误状态并启动定时器 // 设置错误状态并启动定时器
@@ -208,7 +214,7 @@ const openInNewWindow = async (doc: Document, event: Event) => {
} }
}; };
// 处理删除 - 简化确认机制 // 处理删除
const handleDelete = async (doc: Document, event: Event) => { const handleDelete = async (doc: Document, event: Event) => {
event.stopPropagation(); event.stopPropagation();
@@ -236,9 +242,13 @@ const handleDelete = async (doc: Document, event: Event) => {
return; return;
} }
await documentStore.deleteDocument(doc.id); const deleteSuccess = await documentStore.deleteDocument(doc.id);
await documentStore.updateDocuments();
if (!deleteSuccess) {
return;
}
await documentStore.updateDocuments();
// 如果删除的是当前文档,切换到第一个文档 // 如果删除的是当前文档,切换到第一个文档
if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) { if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) {
const firstDoc = documentStore.documentList[0]; const firstDoc = documentStore.documentList[0];

View File

@@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import {useI18n} from 'vue-i18n'; import {useI18n} from 'vue-i18n';
import {onMounted, onUnmounted, ref, watch, computed} from 'vue'; import {computed, onMounted, onUnmounted, ref, watch} from 'vue';
import {useConfigStore} from '@/stores/configStore'; import {useConfigStore} from '@/stores/configStore';
import {useEditorStore} from '@/stores/editorStore'; import {useEditorStore} from '@/stores/editorStore';
import {useUpdateStore} from '@/stores/updateStore'; import {useUpdateStore} from '@/stores/updateStore';
import {useWindowStore} from '@/stores/windowStore'; import {useWindowStore} from '@/stores/windowStore';
import {useSystemStore} from '@/stores/systemStore';
import * as runtime from '@wailsio/runtime'; import * as runtime from '@wailsio/runtime';
import {useRouter} from 'vue-router'; import {useRouter} from 'vue-router';
import BlockLanguageSelector from './BlockLanguageSelector.vue'; import BlockLanguageSelector from './BlockLanguageSelector.vue';
@@ -17,22 +18,32 @@ const editorStore = useEditorStore();
const configStore = useConfigStore(); const configStore = useConfigStore();
const updateStore = useUpdateStore(); const updateStore = useUpdateStore();
const windowStore = useWindowStore(); const windowStore = useWindowStore();
const systemStore = useSystemStore();
const {t} = useI18n(); const {t} = useI18n();
const router = useRouter(); const router = useRouter();
// 当前块是否支持格式化的响应式状态 // 当前块是否支持格式化的响应式状态
const canFormatCurrentBlock = ref(false); const canFormatCurrentBlock = ref(false);
// 窗口置顶状态管理(仅当前窗口,不同步到配置文件) // 窗口置顶状态 - 合并配置和临时状态
const isCurrentWindowOnTop = ref(false); const isCurrentWindowOnTop = computed(() => {
return configStore.config.general.alwaysOnTop || systemStore.isWindowOnTop;
const setWindowAlwaysOnTop = async (isTop: boolean) => { });
await runtime.Window.SetAlwaysOnTop(isTop);
};
// 切换窗口置顶状态
const toggleAlwaysOnTop = async () => { const toggleAlwaysOnTop = async () => {
isCurrentWindowOnTop.value = !isCurrentWindowOnTop.value; const currentlyOnTop = isCurrentWindowOnTop.value;
await runtime.Window.SetAlwaysOnTop(isCurrentWindowOnTop.value);
if (currentlyOnTop) {
// 如果当前是置顶状态,彻底关闭所有置顶
if (configStore.config.general.alwaysOnTop) {
await configStore.setAlwaysOnTop(false);
}
await systemStore.setWindowOnTop(false);
} else {
// 如果当前不是置顶状态,开启临时置顶
await systemStore.setWindowOnTop(true);
}
}; };
// 跳转到设置页面 // 跳转到设置页面
@@ -62,8 +73,8 @@ const updateFormatButtonState = () => {
// 检查块和语言格式化支持 // 检查块和语言格式化支持
canFormatCurrentBlock.value = !!( canFormatCurrentBlock.value = !!(
activeBlock && activeBlock &&
getLanguage(activeBlock.language.name as any)?.prettier getLanguage(activeBlock.language.name as any)?.prettier
); );
} catch (error) { } catch (error) {
console.warn('Error checking format capability:', error); console.warn('Error checking format capability:', error);
@@ -80,7 +91,7 @@ const debouncedUpdateFormatButton = (() => {
timeout = window.setTimeout(() => { timeout = window.setTimeout(() => {
updateFormatButtonState(); updateFormatButtonState();
timeout = null; timeout = null;
}, 300); }, 1000);
}; };
})(); })();
@@ -89,9 +100,9 @@ const setupEditorListeners = (view: any) => {
if (!view?.dom) return []; if (!view?.dom) return [];
const events = [ const events = [
{ type: 'click', handler: updateFormatButtonState }, {type: 'click', handler: updateFormatButtonState},
{ type: 'keyup', handler: debouncedUpdateFormatButton }, {type: 'keyup', handler: debouncedUpdateFormatButton},
{ type: 'focus', handler: updateFormatButtonState } {type: 'focus', handler: updateFormatButtonState}
]; ];
// 注册所有事件 // 注册所有事件
@@ -99,7 +110,7 @@ const setupEditorListeners = (view: any) => {
// 返回清理函数数组 // 返回清理函数数组
return events.map(event => return events.map(event =>
() => view.dom.removeEventListener(event.type, event.handler) () => view.dom.removeEventListener(event.type, event.handler)
); );
}; };
@@ -107,22 +118,22 @@ const setupEditorListeners = (view: any) => {
let cleanupListeners: (() => void)[] = []; let cleanupListeners: (() => void)[] = [];
watch( watch(
() => editorStore.editorView, () => editorStore.editorView,
(newView) => { (newView) => {
// 清理旧监听器 // 清理旧监听器
cleanupListeners.forEach(cleanup => cleanup()); cleanupListeners.forEach(cleanup => cleanup());
cleanupListeners = []; cleanupListeners = [];
if (newView) { if (newView) {
// 初始更新状态 // 初始更新状态
updateFormatButtonState(); updateFormatButtonState();
// 设置新监听器 // 设置新监听器
cleanupListeners = setupEditorListeners(newView); cleanupListeners = setupEditorListeners(newView);
} else { } else {
canFormatCurrentBlock.value = false; canFormatCurrentBlock.value = false;
} }
}, },
{ immediate: true } {immediate: true}
); );
// 组件生命周期 // 组件生命周期
@@ -143,12 +154,28 @@ onUnmounted(() => {
// 组件加载后初始化置顶状态 // 组件加载后初始化置顶状态
watch(isLoaded, async (loaded) => { watch(isLoaded, async (loaded) => {
if (loaded) { if (loaded) {
// 初始化时从配置文件读取置顶状态 // 应用合并后的置顶状态
isCurrentWindowOnTop.value = configStore.config.general.alwaysOnTop; const shouldBeOnTop = configStore.config.general.alwaysOnTop || systemStore.isWindowOnTop;
await setWindowAlwaysOnTop(isCurrentWindowOnTop.value); try {
await runtime.Window.SetAlwaysOnTop(shouldBeOnTop);
} catch (error) {
console.error('Failed to apply window pin state:', error);
}
} }
}); });
// 监听配置变化,同步窗口状态
watch(
() => isCurrentWindowOnTop.value,
async (shouldBeOnTop) => {
try {
await runtime.Window.SetAlwaysOnTop(shouldBeOnTop);
} catch (error) {
console.error('Failed to sync window pin state:', error);
}
}
);
const handleUpdateButtonClick = async () => { const handleUpdateButtonClick = async () => {
if (updateStore.hasUpdate && !updateStore.isUpdating && !updateStore.updateSuccess) { if (updateStore.hasUpdate && !updateStore.isUpdating && !updateStore.updateSuccess) {
// 开始下载更新 // 开始下载更新
@@ -230,20 +257,23 @@ const updateButtonTitle = computed(() => {
@click="handleUpdateButtonClick" @click="handleUpdateButtonClick"
> >
<!-- 检查更新中 --> <!-- 检查更新中 -->
<svg v-if="updateStore.isChecking" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" <svg v-if="updateStore.isChecking" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotating"> stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotating">
<path d="M21 12a9 9 0 1 1-6.219-8.56"/> <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg> </svg>
<!-- 下载更新中 --> <!-- 下载更新中 -->
<svg v-else-if="updateStore.isUpdating" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" <svg v-else-if="updateStore.isUpdating" xmlns="http://www.w3.org/2000/svg" width="14" height="14"
viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotating"> stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotating">
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path> <path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
<path d="M12 2a10 10 0 1 0 10 10"></path> <path d="M12 2a10 10 0 1 0 10 10"></path>
</svg> </svg>
<!-- 更新成功等待重启 --> <!-- 更新成功等待重启 -->
<svg v-else-if="updateStore.updateSuccess" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" <svg v-else-if="updateStore.updateSuccess" xmlns="http://www.w3.org/2000/svg" width="14" height="14"
viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pulsing"> stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pulsing">
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path> <path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path>
<line x1="12" y1="2" x2="12" y2="12"></line> <line x1="12" y1="2" x2="12" y2="12"></line>
@@ -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" <svg v-else xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/> <path
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="7.5,10.5 12,15 16.5,10.5"/> <polyline points="7.5,10.5 12,15 16.5,10.5"/>
<polyline points="12,15 12,3"/> <polyline points="12,15 12,3"/>
</svg> </svg>

View File

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

View File

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

View File

@@ -16,9 +16,7 @@ import {
} from '@/../bindings/voidraft/internal/models/models'; } from '@/../bindings/voidraft/internal/models/models';
import {useI18n} from 'vue-i18n'; import {useI18n} from 'vue-i18n';
import {ConfigUtils} from '@/utils/configUtils'; import {ConfigUtils} from '@/utils/configUtils';
import {WindowController} from '@/utils/windowController';
import * as runtime from '@wailsio/runtime'; import * as runtime from '@wailsio/runtime';
import {useBackupStore} from '@/stores/backupStore';
// 国际化相关导入 // 国际化相关导入
export type SupportedLocaleType = 'zh-CN' | 'en-US'; export type SupportedLocaleType = 'zh-CN' | 'en-US';
@@ -64,7 +62,8 @@ const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
enableSystemTray: 'general.enableSystemTray', enableSystemTray: 'general.enableSystemTray',
startAtLogin: 'general.startAtLogin', startAtLogin: 'general.startAtLogin',
enableGlobalHotkey: 'general.enableGlobalHotkey', enableGlobalHotkey: 'general.enableGlobalHotkey',
globalHotkey: 'general.globalHotkey' globalHotkey: 'general.globalHotkey',
enableWindowSnap: 'general.enableWindowSnap',
} as const; } as const;
const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = { const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
@@ -80,8 +79,7 @@ const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = { const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
language: 'appearance.language', language: 'appearance.language',
systemTheme: 'appearance.systemTheme', systemTheme: 'appearance.systemTheme'
customTheme: 'appearance.customTheme'
} as const; } as const;
const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = { const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
@@ -179,7 +177,8 @@ const DEFAULT_CONFIG: AppConfig = {
alt: true, alt: true,
win: false, win: false,
key: 'X' key: 'X'
} },
enableWindowSnap: true,
}, },
editing: { editing: {
fontSize: CONFIG_LIMITS.fontSize.default, fontSize: CONFIG_LIMITS.fontSize.default,
@@ -191,80 +190,10 @@ const DEFAULT_CONFIG: AppConfig = {
tabType: CONFIG_LIMITS.tabType.default, tabType: CONFIG_LIMITS.tabType.default,
autoSaveDelay: 5000 autoSaveDelay: 5000
}, },
appearance: { appearance: {
language: LanguageType.LangZhCN, language: LanguageType.LangZhCN,
systemTheme: SystemThemeType.SystemThemeAuto, systemTheme: SystemThemeType.SystemThemeAuto
customTheme: { },
darkTheme: {
// 基础色调
background: '#252B37',
backgroundSecondary: '#213644',
surface: '#474747',
foreground: '#9BB586',
foregroundSecondary: '#9c9c9c',
// 语法高亮
comment: '#6272a4',
keyword: '#ff79c6',
string: '#f1fa8c',
function: '#50fa7b',
number: '#bd93f9',
operator: '#ff79c6',
variable: '#8fbcbb',
type: '#8be9fd',
// 界面元素
cursor: '#fff',
selection: '#0865a9aa',
selectionBlur: '#225377aa',
activeLine: 'rgba(255,255,255,0.04)',
lineNumber: 'rgba(255,255,255, 0.15)',
activeLineNumber: 'rgba(255,255,255, 0.6)',
// 边框分割线
borderColor: '#1e222a',
borderLight: 'rgba(255,255,255, 0.1)',
// 搜索匹配
searchMatch: '#8fbcbb',
matchingBracket: 'rgba(255,255,255,0.1)'
},
lightTheme: {
// 基础色调
background: '#ffffff',
backgroundSecondary: '#f1faf1',
surface: '#f5f5f5',
foreground: '#444d56',
foregroundSecondary: '#6a737d',
// 语法高亮
comment: '#6a737d',
keyword: '#d73a49',
string: '#032f62',
function: '#005cc5',
number: '#005cc5',
operator: '#d73a49',
variable: '#24292e',
type: '#6f42c1',
// 界面元素
cursor: '#000',
selection: '#77baff8c',
selectionBlur: '#b2c2ca85',
activeLine: '#000000',
lineNumber: '#000000',
activeLineNumber: '#000000',
// 边框分割线
borderColor: '#dfdfdf',
borderLight: '#0000000C',
// 搜索匹配
searchMatch: '#005cc5',
matchingBracket: 'rgba(0,0,0,0.1)'
}
}
},
updates: { updates: {
version: "1.0.0", version: "1.0.0",
autoUpdate: true, autoUpdate: true,
@@ -416,9 +345,6 @@ export const useConfigStore = defineStore('config', () => {
state.configLoaded = true; state.configLoaded = true;
// 初始化热键监听器
const windowController = WindowController.getInstance();
await windowController.initializeHotkeyListener();
} finally { } finally {
state.isLoading = false; state.isLoading = false;
} }
@@ -483,50 +409,7 @@ export const useConfigStore = defineStore('config', () => {
await updateAppearanceConfig('systemTheme', systemTheme); await updateAppearanceConfig('systemTheme', systemTheme);
}; };
// 更新自定义主题方法
const updateCustomTheme = async (themeType: 'darkTheme' | 'lightTheme', colorKey: string, colorValue: string): Promise<void> => {
// 确保配置已加载
if (!state.configLoaded && !state.isLoading) {
await initConfig();
}
try {
// 深拷贝当前配置
const customTheme = JSON.parse(JSON.stringify(state.config.appearance.customTheme));
// 更新对应主题的颜色值
customTheme[themeType][colorKey] = colorValue;
// 更新整个自定义主题配置到后端
await ConfigService.Set(APPEARANCE_CONFIG_KEY_MAP.customTheme, customTheme);
// 更新前端状态
state.config.appearance.customTheme = customTheme;
} catch (error) {
throw error;
}
};
// 设置整个自定义主题配置
const setCustomTheme = async (customTheme: any): Promise<void> => {
// 确保配置已加载
if (!state.configLoaded && !state.isLoading) {
await initConfig();
}
try {
// 更新整个自定义主题配置到后端
await ConfigService.Set(APPEARANCE_CONFIG_KEY_MAP.customTheme, customTheme);
// 更新前端状态
state.config.appearance.customTheme = customTheme;
// 确保Vue能检测到变化
state.config.appearance = { ...state.config.appearance };
} catch (error) {
throw error;
}
};
// 初始化语言设置 // 初始化语言设置
const initializeLanguage = async (): Promise<void> => { const initializeLanguage = async (): Promise<void> => {
@@ -591,8 +474,6 @@ export const useConfigStore = defineStore('config', () => {
// 主题相关方法 // 主题相关方法
setSystemTheme, setSystemTheme,
updateCustomTheme,
setCustomTheme,
// 字体大小操作 // 字体大小操作
...adjusters.fontSize, ...adjusters.fontSize,
@@ -642,6 +523,9 @@ export const useConfigStore = defineStore('config', () => {
await StartupService.SetEnabled(value); await StartupService.SetEnabled(value);
}, },
// 窗口吸附配置相关方法
setEnableWindowSnap: async (value: boolean) => await updateGeneralConfig('enableWindowSnap', value),
// 更新配置相关方法 // 更新配置相关方法
setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value), setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value),

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import {defineStore} from 'pinia'; import {defineStore} from 'pinia';
import {computed, reactive} from 'vue'; import {computed, reactive} from 'vue';
import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models'; import {SystemThemeType, ThemeType, ThemeColorConfig} from '@/../bindings/voidraft/internal/models/models';
import {ThemeService} from '@/../bindings/voidraft/internal/services';
import {useConfigStore} from './configStore'; import {useConfigStore} from './configStore';
import {useEditorStore} from './editorStore'; import {useEditorStore} from './editorStore';
import {defaultDarkColors} from '@/views/editor/theme/dark'; import {defaultDarkColors} from '@/views/editor/theme/dark';
@@ -24,16 +25,21 @@ export const useThemeStore = defineStore('theme', () => {
configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
); );
// 初始化主题颜色 - 从配置加载 // 初始化主题颜色 - 从数据库加载
const initializeThemeColors = () => { const initializeThemeColors = async () => {
const customTheme = configStore.config?.appearance?.customTheme; try {
if (customTheme) { const themes = await ThemeService.GetDefaultThemes();
if (customTheme.darkTheme) { if (themes.dark) {
Object.assign(themeColors.darkTheme, customTheme.darkTheme); Object.assign(themeColors.darkTheme, themes.dark.colors);
} }
if (customTheme.lightTheme) { if (themes.light) {
Object.assign(themeColors.lightTheme, customTheme.lightTheme); Object.assign(themeColors.lightTheme, themes.light.colors);
} }
} catch (error) {
console.warn('Failed to load themes from database, using defaults:', error);
// 如果数据库加载失败,使用默认主题
Object.assign(themeColors.darkTheme, defaultDarkColors);
Object.assign(themeColors.lightTheme, defaultLightColors);
} }
}; };
@@ -46,10 +52,10 @@ export const useThemeStore = defineStore('theme', () => {
}; };
// 初始化主题 // 初始化主题
const initializeTheme = () => { const initializeTheme = async () => {
const theme = configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto; const theme = configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto;
applyThemeToDOM(theme); applyThemeToDOM(theme);
initializeThemeColors(); await initializeThemeColors();
}; };
// 设置主题 // 设置主题
@@ -84,31 +90,35 @@ export const useThemeStore = defineStore('theme', () => {
return hasChanges; return hasChanges;
}; };
// 保存主题颜色到配置 // 保存主题颜色到数据库
const saveThemeColors = async () => { const saveThemeColors = async () => {
const customTheme = { try {
darkTheme: { ...themeColors.darkTheme }, const darkColors = ThemeColorConfig.createFrom(themeColors.darkTheme);
lightTheme: { ...themeColors.lightTheme } const lightColors = ThemeColorConfig.createFrom(themeColors.lightTheme);
};
await configStore.setCustomTheme(customTheme); await ThemeService.UpdateThemeColors(ThemeType.ThemeTypeDark, darkColors);
await ThemeService.UpdateThemeColors(ThemeType.ThemeTypeLight, lightColors);
} catch (error) {
console.error('Failed to save theme colors:', error);
throw error;
}
}; };
// 重置主题颜色 // 重置主题颜色
const resetThemeColors = async (themeType: 'darkTheme' | 'lightTheme') => { const resetThemeColors = async (themeType: 'darkTheme' | 'lightTheme') => {
try { try {
// 1. 更新内存中的颜色状态 const dbThemeType = themeType === 'darkTheme' ? ThemeType.ThemeTypeDark : ThemeType.ThemeTypeLight;
// 1. 调用后端重置服务
await ThemeService.ResetThemeColors(dbThemeType);
// 2. 更新内存中的颜色状态
if (themeType === 'darkTheme') { if (themeType === 'darkTheme') {
Object.assign(themeColors.darkTheme, defaultDarkColors); Object.assign(themeColors.darkTheme, defaultDarkColors);
} } else {
if (themeType === 'lightTheme') {
Object.assign(themeColors.lightTheme, defaultLightColors); Object.assign(themeColors.lightTheme, defaultLightColors);
} }
// 2. 保存到配置
await saveThemeColors();
// 3. 刷新编辑器主题 // 3. 刷新编辑器主题
refreshEditorTheme(); refreshEditorTheme();

View File

@@ -1,105 +0,0 @@
import * as wails from '@wailsio/runtime'
// 窗口控制工具类
export class WindowController {
private static instance: WindowController;
private currentWindow = wails.Window;
private isWindowVisible: boolean = true; // 跟踪窗口可见状态
private isInitialized: boolean = false; // 跟踪是否已初始化
private isToggling: boolean = false; // 防止重复切换
private lastToggleTime: number = 0; // 上次切换时间
private readonly TOGGLE_COOLDOWN = 500; // 切换冷却时间(毫秒)
static getInstance(): WindowController {
if (!WindowController.instance) {
WindowController.instance = new WindowController();
}
return WindowController.instance;
}
async toggleWindow(): Promise<void> {
const now = Date.now();
// 防抖检查
if (this.isToggling || (now - this.lastToggleTime) < this.TOGGLE_COOLDOWN) {
return;
}
this.isToggling = true;
this.lastToggleTime = now;
try {
// 如果还没初始化,先初始化状态
if (!this.isInitialized) {
await this.syncWindowState();
}
if (!this.isWindowVisible) {
// 窗口当前隐藏,显示它
await this.currentWindow.Show();
await this.currentWindow.UnMinimise(); // 修正API名称
await this.currentWindow.Focus();
this.isWindowVisible = true;
} else {
// 窗口当前可见,隐藏它
await this.currentWindow.Hide();
this.isWindowVisible = false;
}
} catch (error) {
console.error(error);
} finally {
// 延迟重置切换状态,确保操作完成
setTimeout(() => {
this.isToggling = false;
}, 100);
}
}
// 同步窗口状态
private async syncWindowState(): Promise<void> {
try {
// 检查窗口是否最小化
const isMinimised = await this.currentWindow.IsMinimised();
// 简化状态判断:只要不是最小化状态就认为是可见的
this.isWindowVisible = !isMinimised;
this.isInitialized = true;
} catch (error) {
// 如果检查失败,保持默认状态
this.isWindowVisible = true;
this.isInitialized = true;
}
}
// 当窗口被系统事件隐藏时调用(比如点击关闭/最小化按钮)
onWindowHidden(): void {
this.isWindowVisible = false;
}
// 当窗口被系统事件显示时调用(比如点击托盘图标)
onWindowShown(): void {
this.isWindowVisible = true;
}
async initializeHotkeyListener(): Promise<void> {
// 初始化时同步窗口状态
await this.syncWindowState();
// 监听后端发送的热键事件
wails.Events.On('hotkey:toggle-window', () => {
this.toggleWindow();
});
// 监听窗口显示/隐藏事件以同步状态
wails.Events.On('window:shown', () => {
this.onWindowShown();
});
wails.Events.On('window:hidden', () => {
this.onWindowHidden();
});
}
}

View File

@@ -16,8 +16,8 @@ const themeStore = useThemeStore();
// 添加临时颜色状态 // 添加临时颜色状态
const tempColors = ref({ const tempColors = ref({
darkTheme: { ...configStore.config.appearance.customTheme?.darkTheme || defaultDarkColors }, darkTheme: { ...defaultDarkColors },
lightTheme: { ...configStore.config.appearance.customTheme?.lightTheme || defaultLightColors } lightTheme: { ...defaultLightColors }
}); });
// 标记是否有未保存的更改 // 标记是否有未保存的更改
@@ -71,9 +71,9 @@ const currentThemeMode = computed(() => {
return isDark ? 'dark' : 'light'; return isDark ? 'dark' : 'light';
}); });
// 监听配置变更,更新临时颜色 // 监听主题颜色变更,更新临时颜色
watch( watch(
() => configStore.config.appearance.customTheme, () => themeStore.themeColors,
(newValue) => { (newValue) => {
if (!hasUnsavedChanges.value) { if (!hasUnsavedChanges.value) {
tempColors.value = { tempColors.value = {

View File

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

23
go.mod
View File

@@ -3,7 +3,6 @@ module voidraft
go 1.24.4 go 1.24.4
require ( require (
github.com/Masterminds/semver/v3 v3.4.0
github.com/creativeprojects/go-selfupdate v1.5.0 github.com/creativeprojects/go-selfupdate v1.5.0
github.com/go-git/go-git/v5 v5.16.2 github.com/go-git/go-git/v5 v5.16.2
github.com/knadh/koanf/parsers/json v1.0.0 github.com/knadh/koanf/parsers/json v1.0.0
@@ -11,23 +10,26 @@ require (
github.com/knadh/koanf/providers/structs v1.0.0 github.com/knadh/koanf/providers/structs v1.0.0
github.com/knadh/koanf/v2 v2.2.2 github.com/knadh/koanf/v2 v2.2.2
github.com/robertkrimen/otto v0.5.1 github.com/robertkrimen/otto v0.5.1
github.com/wailsapp/wails/v3 v3.0.0-alpha.12 github.com/stretchr/testify v1.10.0
golang.org/x/net v0.42.0 github.com/wailsapp/wails/v3 v3.0.0-alpha.25
golang.org/x/sys v0.34.0 golang.org/x/net v0.43.0
golang.org/x/text v0.27.0 golang.org/x/sys v0.35.0
modernc.org/sqlite v1.38.0 golang.org/x/text v0.28.0
modernc.org/sqlite v1.38.2
) )
require ( require (
code.gitea.io/sdk/gitea v0.21.0 // indirect code.gitea.io/sdk/gitea v0.21.0 // indirect
dario.cat/mergo v1.0.2 // indirect dario.cat/mergo v1.0.2 // indirect
github.com/42wim/httpsig v1.2.3 // indirect github.com/42wim/httpsig v1.2.3 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/adrg/xdg v0.5.3 // indirect github.com/adrg/xdg v0.5.3 // indirect
github.com/bep/debounce v1.2.1 // indirect github.com/bep/debounce v1.2.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect github.com/ebitengine/purego v0.8.4 // indirect
@@ -62,25 +64,26 @@ require (
github.com/pjbgf/sha1cd v0.4.0 // indirect github.com/pjbgf/sha1cd v0.4.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.51.0 // indirect github.com/samber/lo v1.51.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect github.com/skeema/knownhosts v1.3.1 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect github.com/ulikunitz/xz v0.5.14 // indirect
github.com/wailsapp/go-webview2 v1.0.21 // indirect github.com/wailsapp/go-webview2 v1.0.21 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/go-gitlab v0.115.0 // indirect github.com/xanzy/go-gitlab v0.115.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.40.0 // indirect golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/time v0.12.0 // indirect golang.org/x/time v0.12.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.66.3 // indirect modernc.org/libc v1.66.7 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
) )

56
go.sum
View File

@@ -158,14 +158,14 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA= github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA=
github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v3 v3.0.0-alpha.12 h1:z4wYfujk5tSuZXh+59S2DdEncQHG63CW51i2mZFKLS8= github.com/wailsapp/wails/v3 v3.0.0-alpha.25 h1:o05zUiPEvmrq2lqqCs4wqnrnAjGmhryYHRhjQmtkvk8=
github.com/wailsapp/wails/v3 v3.0.0-alpha.12/go.mod h1:4LCCW7s9e4PuSmu7l9OTvfWIGMO8TaSiftSeR5NpBIc= github.com/wailsapp/wails/v3 v3.0.0-alpha.25/go.mod h1:UZpnhYuju4saspCJrIHAvC0H5XjtKnqd26FRxJLrQ0M=
github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8= github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M= github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@@ -174,19 +174,19 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
@@ -203,21 +203,21 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -234,18 +234,18 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -254,8 +254,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI= modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE= modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -4,6 +4,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
"voidraft/internal/version"
) )
// TabType 定义了制表符类型 // TabType 定义了制表符类型
@@ -68,6 +69,9 @@ type GeneralConfig struct {
EnableSystemTray bool `json:"enableSystemTray"` // 是否启用系统托盘 EnableSystemTray bool `json:"enableSystemTray"` // 是否启用系统托盘
StartAtLogin bool `json:"startAtLogin"` // 开机启动设置 StartAtLogin bool `json:"startAtLogin"` // 开机启动设置
// 窗口吸附设置
EnableWindowSnap bool `json:"enableWindowSnap"` // 是否启用窗口吸附功能(阈值现在是自适应的)
// 全局热键设置 // 全局热键设置
EnableGlobalHotkey bool `json:"enableGlobalHotkey"` // 是否启用全局热键 EnableGlobalHotkey bool `json:"enableGlobalHotkey"` // 是否启用全局热键
GlobalHotkey HotkeyCombo `json:"globalHotkey"` // 全局热键组合 GlobalHotkey HotkeyCombo `json:"globalHotkey"` // 全局热键组合
@@ -101,9 +105,8 @@ type EditingConfig struct {
// AppearanceConfig 外观设置配置 // AppearanceConfig 外观设置配置
type AppearanceConfig struct { type AppearanceConfig struct {
Language LanguageType `json:"language"` // 界面语言 Language LanguageType `json:"language"` // 界面语言
SystemTheme SystemThemeType `json:"systemTheme"` // 系统界面主题 SystemTheme SystemThemeType `json:"systemTheme"` // 系统界面主题
CustomTheme CustomThemeConfig `json:"customTheme"` // 自定义主题配置
} }
// UpdatesConfig 更新设置配置 // UpdatesConfig 更新设置配置
@@ -146,6 +149,7 @@ func NewDefaultAppConfig() *AppConfig {
DataPath: dataDir, DataPath: dataDir,
EnableSystemTray: true, EnableSystemTray: true,
StartAtLogin: false, StartAtLogin: false,
EnableWindowSnap: true, // 默认启用窗口吸附
EnableGlobalHotkey: false, EnableGlobalHotkey: false,
GlobalHotkey: HotkeyCombo{ GlobalHotkey: HotkeyCombo{
Ctrl: false, Ctrl: false,
@@ -171,10 +175,9 @@ func NewDefaultAppConfig() *AppConfig {
Appearance: AppearanceConfig{ Appearance: AppearanceConfig{
Language: LangEnUS, Language: LangEnUS,
SystemTheme: SystemThemeAuto, SystemTheme: SystemThemeAuto,
CustomTheme: *NewDefaultCustomThemeConfig(),
}, },
Updates: UpdatesConfig{ Updates: UpdatesConfig{
Version: "1.3.0", Version: version.Version,
AutoUpdate: true, AutoUpdate: true,
PrimarySource: UpdateSourceGitea, PrimarySource: UpdateSourceGitea,
BackupSource: UpdateSourceGithub, BackupSource: UpdateSourceGithub,
@@ -203,7 +206,7 @@ func NewDefaultAppConfig() *AppConfig {
}, },
Metadata: ConfigMetadata{ Metadata: ConfigMetadata{
LastUpdated: time.Now().Format(time.RFC3339), LastUpdated: time.Now().Format(time.RFC3339),
Version: "1.2.0", Version: version.Version,
}, },
} }
} }

View File

@@ -1,5 +1,20 @@
package models package models
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
)
// ThemeType 主题类型枚举
type ThemeType string
const (
ThemeTypeDark ThemeType = "dark"
ThemeTypeLight ThemeType = "light"
)
// ThemeColorConfig 主题颜色配置 // ThemeColorConfig 主题颜色配置
type ThemeColorConfig struct { type ThemeColorConfig struct {
// 基础色调 // 基础色调
@@ -36,15 +51,44 @@ type ThemeColorConfig struct {
MatchingBracket string `json:"matchingBracket"` // 匹配括号 MatchingBracket string `json:"matchingBracket"` // 匹配括号
} }
// CustomThemeConfig 自定义主题配置 // Theme 主题数据库模型
type CustomThemeConfig struct { type Theme struct {
DarkTheme ThemeColorConfig `json:"darkTheme"` // 深色主题配置 ID int `db:"id" json:"id"`
LightTheme ThemeColorConfig `json:"lightTheme"` // 浅色主题配置 Name string `db:"name" json:"name"`
Type ThemeType `db:"type" json:"type"`
Colors ThemeColorConfig `db:"colors" json:"colors"`
IsDefault bool `db:"is_default" json:"isDefault"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
}
// Value 实现 driver.Valuer 接口,用于将 ThemeColorConfig 存储到数据库
func (tc ThemeColorConfig) Value() (driver.Value, error) {
return json.Marshal(tc)
}
// Scan 实现 sql.Scanner 接口,用于从数据库读取 ThemeColorConfig
func (tc *ThemeColorConfig) Scan(value interface{}) error {
if value == nil {
return nil
}
var bytes []byte
switch v := value.(type) {
case []byte:
bytes = v
case string:
bytes = []byte(v)
default:
return fmt.Errorf("cannot scan %T into ThemeColorConfig", value)
}
return json.Unmarshal(bytes, tc)
} }
// NewDefaultDarkTheme 创建默认深色主题配置 // NewDefaultDarkTheme 创建默认深色主题配置
func NewDefaultDarkTheme() ThemeColorConfig { func NewDefaultDarkTheme() *ThemeColorConfig {
return ThemeColorConfig{ return &ThemeColorConfig{
// 基础色调 // 基础色调
Background: "#252B37", Background: "#252B37",
BackgroundSecondary: "#213644", BackgroundSecondary: "#213644",
@@ -81,8 +125,8 @@ func NewDefaultDarkTheme() ThemeColorConfig {
} }
// NewDefaultLightTheme 创建默认浅色主题配置 // NewDefaultLightTheme 创建默认浅色主题配置
func NewDefaultLightTheme() ThemeColorConfig { func NewDefaultLightTheme() *ThemeColorConfig {
return ThemeColorConfig{ return &ThemeColorConfig{
// 基础色调 // 基础色调
Background: "#ffffff", Background: "#ffffff",
BackgroundSecondary: "#f1faf1", BackgroundSecondary: "#f1faf1",
@@ -117,11 +161,3 @@ func NewDefaultLightTheme() ThemeColorConfig {
MatchingBracket: "#00000019", MatchingBracket: "#00000019",
} }
} }
// NewDefaultCustomThemeConfig 创建默认自定义主题配置
func NewDefaultCustomThemeConfig() *CustomThemeConfig {
return &CustomThemeConfig{
DarkTheme: NewDefaultDarkTheme(),
LightTheme: NewDefaultLightTheme(),
}
}

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

@@ -30,14 +30,14 @@ type BackupService struct {
configService *ConfigService configService *ConfigService
dbService *DatabaseService dbService *DatabaseService
repository *git.Repository repository *git.Repository
logger *log.Service logger *log.LogService
isInitialized bool isInitialized bool
autoBackupTicker *time.Ticker autoBackupTicker *time.Ticker
autoBackupStop chan bool autoBackupStop chan bool
} }
// NewBackupService 创建新的备份服务实例 // NewBackupService 创建新的备份服务实例
func NewBackupService(configService *ConfigService, dbService *DatabaseService, logger *log.Service) *BackupService { func NewBackupService(configService *ConfigService, dbService *DatabaseService, logger *log.LogService) *BackupService {
return &BackupService{ return &BackupService{
configService: configService, configService: configService,
dbService: dbService, dbService: dbService,
@@ -263,7 +263,7 @@ func (s *BackupService) PushToRemote() error {
// 创建提交 // 创建提交
_, err = w.Commit(fmt.Sprintf("Backup %s", time.Now().Format("2006-01-02 15:04:05")), &git.CommitOptions{ _, err = w.Commit(fmt.Sprintf("Backup %s", time.Now().Format("2006-01-02 15:04:05")), &git.CommitOptions{
Author: &object.Signature{ Author: &object.Signature{
Name: "VoidRaft", Name: "voidraft",
Email: "backup@voidraft.app", Email: "backup@voidraft.app",
When: time.Now(), When: time.Now(),
}, },

View File

@@ -1,318 +1,195 @@
package services package services
import ( import (
"encoding/json"
"fmt" "fmt"
"os"
"path/filepath"
"reflect"
"sort"
"time"
"voidraft/internal/models"
"github.com/Masterminds/semver/v3"
jsonparser "github.com/knadh/koanf/parsers/json" jsonparser "github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/providers/structs" "github.com/knadh/koanf/providers/structs"
"github.com/knadh/koanf/v2" "github.com/knadh/koanf/v2"
"github.com/wailsapp/wails/v3/pkg/services/log" "github.com/wailsapp/wails/v3/pkg/services/log"
"os"
"path/filepath"
"time"
) )
const ( const (
// CurrentAppConfigVersion 当前应用配置版本 // BackupFilePattern backup file name pattern
CurrentAppConfigVersion = "1.3.0"
// BackupFilePattern 备份文件名模式
BackupFilePattern = "%s.backup.%s.json" BackupFilePattern = "%s.backup.%s.json"
// MaxConfigFileSize maximum config file size
// 资源限制常量
MaxConfigFileSize = 10 * 1024 * 1024 // 10MB MaxConfigFileSize = 10 * 1024 * 1024 // 10MB
MaxRecursionDepth = 50 // 最大递归深度
) )
// Migratable 可迁移的配置接口 // ConfigMigrator elegant configuration migrator with automatic field detection
type Migratable interface { type ConfigMigrator struct {
GetVersion() string // 获取当前版本 logger *log.LogService
SetVersion(string) // 设置版本 configDir string
SetLastUpdated(string) // 设置最后更新时间 configName string
GetDefaultConfig() any // 获取默认配置 configPath string
} }
// ConfigMigrationService 配置迁移服务 // MigrationResult migration operation result
type ConfigMigrationService[T Migratable] struct {
logger *log.Service
configDir string
configName string
targetVersion string
configPath string
}
// MigrationResult 迁移结果
type MigrationResult struct { type MigrationResult struct {
Migrated, ConfigUpdated bool Migrated bool `json:"migrated"` // Whether migration was performed
FromVersion, ToVersion string MissingFields []string `json:"missingFields"` // Fields that were missing
BackupPath string BackupPath string `json:"backupPath"` // Path to backup file
Description string `json:"description"` // Description of migration
} }
// NewConfigMigrationService 创建配置迁移服务 // NewConfigMigrator creates a new configuration migrator
func NewConfigMigrationService[T Migratable]( func NewConfigMigrator(
logger *log.Service, logger *log.LogService,
configDir string, configDir string,
configName, targetVersion, configPath string, configName, configPath string,
) *ConfigMigrationService[T] { ) *ConfigMigrator {
return &ConfigMigrationService[T]{ if logger == nil {
logger: orDefault(logger, log.New()), logger = log.New()
configDir: configDir, }
configName: configName, return &ConfigMigrator{
targetVersion: targetVersion, logger: logger,
configPath: configPath, configDir: configDir,
configName: configName,
configPath: configPath,
} }
} }
// MigrateConfig 迁移配置文件 // AutoMigrate automatically detects and migrates missing configuration fields
func (cms *ConfigMigrationService[T]) MigrateConfig(existingConfig *koanf.Koanf) (*MigrationResult, error) { func (cm *ConfigMigrator) AutoMigrate(defaultConfig interface{}, currentConfig *koanf.Koanf) (*MigrationResult, error) {
currentVersion := orDefault(existingConfig.String("metadata.version"), "0.0.0") // Load default config into temporary koanf instance
result := &MigrationResult{ defaultKoanf := koanf.New(".")
FromVersion: currentVersion, if err := defaultKoanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
ToVersion: cms.targetVersion, return nil, fmt.Errorf("failed to load default config: %w", err)
} }
if needsMigration, err := cms.needsMigration(currentVersion); err != nil { // Detect missing fields
return result, fmt.Errorf("version comparison failed: %w", err) missingFields := cm.detectMissingFields(currentConfig.All(), defaultKoanf.All())
} else if !needsMigration {
// Create result object
result := &MigrationResult{
MissingFields: missingFields,
Migrated: len(missingFields) > 0,
Description: fmt.Sprintf("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 return result, nil
} }
// 资源检查和备份 // Only create backup if we actually need to migrate (has missing fields)
if err := cms.checkResourceLimits(); err != nil { if len(missingFields) > 0 {
return result, fmt.Errorf("resource limit check failed: %w", err) 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 { // Merge missing fields from default config
return result, fmt.Errorf("backup creation failed: %w", err) if err := cm.mergeDefaultFields(currentConfig, defaultKoanf, missingFields); err != nil {
} else { return result, fmt.Errorf("failed to merge default fields: %w", err)
result.BackupPath = backupPath
} }
// 自动恢复检查 // Save updated config
cms.tryQuickRecovery(existingConfig) if err := cm.saveConfig(currentConfig); err != nil {
return result, fmt.Errorf("failed to save updated config: %w", err)
// 执行迁移
if configUpdated, err := cms.performOptimizedMigration(existingConfig); err != nil {
return result, fmt.Errorf("migration failed: %w", err)
} else {
result.Migrated = true
result.ConfigUpdated = configUpdated
} }
cm.logger.Info("Configuration migration completed successfully", "migratedFields", len(missingFields))
return result, nil return result, nil
} }
// needsMigration 检查是否需要迁移 // detectMissingFields detects missing configuration fields
func (cms *ConfigMigrationService[T]) needsMigration(current string) (bool, error) { func (cm *ConfigMigrator) detectMissingFields(current, defaultConfig map[string]interface{}) []string {
currentVer, err := semver.NewVersion(current) var missingFields []string
if err != nil { cm.findMissingFieldsRecursive("", defaultConfig, current, &missingFields)
return true, nil return missingFields
}
targetVer, err := semver.NewVersion(cms.targetVersion)
if err != nil {
return false, fmt.Errorf("invalid target version: %s", cms.targetVersion)
}
return currentVer.LessThan(targetVer), nil
} }
// checkResourceLimits 检查资源限制 // findMissingFieldsRecursive recursively finds missing fields
func (cms *ConfigMigrationService[T]) checkResourceLimits() error { func (cm *ConfigMigrator) findMissingFieldsRecursive(prefix string, defaultMap, currentMap map[string]interface{}, missing *[]string) {
if info, err := os.Stat(cms.configPath); err == nil && info.Size() > MaxConfigFileSize { for key, defaultVal := range defaultMap {
return fmt.Errorf("config file size (%d bytes) exceeds limit (%d bytes)", info.Size(), MaxConfigFileSize) fullKey := key
if prefix != "" {
fullKey = prefix + "." + key
}
currentVal, exists := currentMap[key]
if !exists {
// Field 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 return nil
} }
// createBackupOptimized 优化的备份创建 // createBackup creates a backup of the configuration file
func (cms *ConfigMigrationService[T]) createBackupOptimized() (string, error) { func (cm *ConfigMigrator) createBackup() (string, error) {
if _, err := os.Stat(cms.configPath); os.IsNotExist(err) { if _, err := os.Stat(cm.configPath); os.IsNotExist(err) {
return "", nil return "", nil
} }
configDir := cms.configDir
timestamp := time.Now().Format("20060102150405") timestamp := time.Now().Format("20060102150405")
newBackupPath := filepath.Join(configDir, fmt.Sprintf(BackupFilePattern, cms.configName, timestamp)) backupPath := filepath.Join(cm.configDir, fmt.Sprintf(BackupFilePattern, cm.configName, timestamp))
// 单次扫描:删除旧备份并创建新备份 data, err := os.ReadFile(cm.configPath)
pattern := filepath.Join(configDir, fmt.Sprintf("%s.backup.*.json", cms.configName))
if matches, err := filepath.Glob(pattern); err == nil {
for _, oldBackup := range matches {
if oldBackup != newBackupPath {
os.Remove(oldBackup) // 忽略删除错误,继续处理
}
}
}
return newBackupPath, copyFile(cms.configPath, newBackupPath)
}
// tryQuickRecovery 快速恢复检查
func (cms *ConfigMigrationService[T]) tryQuickRecovery(existingConfig *koanf.Koanf) {
var testConfig T
if existingConfig.Unmarshal("", &testConfig) != nil {
cms.logger.Info("Config appears corrupted, attempting quick recovery")
if backupPath := cms.findLatestBackupQuick(); backupPath != "" {
if data, err := os.ReadFile(backupPath); err == nil {
existingConfig.Delete("")
existingConfig.Load(&BytesProvider{data}, jsonparser.Parser())
cms.logger.Info("Quick recovery successful")
}
}
}
}
// findLatestBackupQuick 快速查找最新备份(优化排序)
func (cms *ConfigMigrationService[T]) findLatestBackupQuick() string {
pattern := filepath.Join(cms.configDir, fmt.Sprintf("%s.backup.*.json", cms.configName))
matches, err := filepath.Glob(pattern)
if err != nil || len(matches) == 0 {
return ""
}
sort.Strings(matches) // 字典序排序,时间戳格式确保正确性
return matches[len(matches)-1]
}
// performOptimizedMigration 优化的迁移执行
func (cms *ConfigMigrationService[T]) performOptimizedMigration(existingConfig *koanf.Koanf) (bool, error) {
// 直接从koanf实例获取配置避免额外序列化
var currentConfig T
if err := existingConfig.Unmarshal("", &currentConfig); err != nil {
return false, fmt.Errorf("unmarshal existing config failed: %w", err)
}
defaultConfig, ok := currentConfig.GetDefaultConfig().(T)
if !ok {
return false, fmt.Errorf("default config type mismatch")
}
return cms.mergeInPlace(existingConfig, currentConfig, defaultConfig)
}
// mergeInPlace 就地合并配置
func (cms *ConfigMigrationService[T]) mergeInPlace(existingConfig *koanf.Koanf, currentConfig, defaultConfig T) (bool, error) {
// 创建临时合并实例
mergeKoanf := koanf.New(".")
// 使用快速加载链
if err := chainLoad(mergeKoanf,
func() error { return mergeKoanf.Load(structs.Provider(defaultConfig, "json"), nil) },
func() error {
return mergeKoanf.Load(structs.Provider(currentConfig, "json"), nil,
koanf.WithMergeFunc(cms.fastMerge))
},
); err != nil {
return false, fmt.Errorf("config merge failed: %w", err)
}
// 更新元数据
mergeKoanf.Set("metadata.version", cms.targetVersion)
mergeKoanf.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339))
// 一次性序列化和原子写入
configBytes, err := mergeKoanf.Marshal(jsonparser.Parser())
if err != nil { if err != nil {
return false, fmt.Errorf("marshal config failed: %w", err) return "", fmt.Errorf("failed to read config file: %w", err)
}
if err := os.WriteFile(backupPath, data, 0644); err != nil {
return "", fmt.Errorf("failed to create backup: %w", err)
}
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 { if len(configBytes) > MaxConfigFileSize {
return false, fmt.Errorf("merged config size exceeds limit") return fmt.Errorf("config size (%d bytes) exceeds limit (%d bytes)", len(configBytes), MaxConfigFileSize)
} }
// 原子写入 // Atomic write
return true, cms.atomicWrite(existingConfig, configBytes) tempPath := cm.configPath + ".tmp"
}
// atomicWrite 原子写入操作
func (cms *ConfigMigrationService[T]) atomicWrite(existingConfig *koanf.Koanf, configBytes []byte) error {
tempPath := cms.configPath + ".tmp"
if err := os.WriteFile(tempPath, configBytes, 0644); err != nil { if err := os.WriteFile(tempPath, configBytes, 0644); err != nil {
return fmt.Errorf("write temp config failed: %w", err) return fmt.Errorf("failed to write temp config: %w", err)
} }
if err := os.Rename(tempPath, cms.configPath); err != nil { if err := os.Rename(tempPath, cm.configPath); err != nil {
os.Remove(tempPath) os.Remove(tempPath)
return fmt.Errorf("atomic rename failed: %w", err) return fmt.Errorf("failed to rename temp config: %w", err)
} }
// 重新加载到原实例
existingConfig.Delete("")
return existingConfig.Load(&BytesProvider{configBytes}, jsonparser.Parser())
}
// fastMerge 快速合并函数
func (cms *ConfigMigrationService[T]) fastMerge(src, dest map[string]interface{}) error {
return cms.fastMergeRecursive(src, dest, 0)
}
// fastMergeRecursive 快速递归合并
func (cms *ConfigMigrationService[T]) fastMergeRecursive(src, dest map[string]interface{}, depth int) error {
if depth > MaxRecursionDepth {
return fmt.Errorf("recursion depth exceeded")
}
for key, srcVal := range src {
if destVal, exists := dest[key]; exists {
// 优先检查map类型最常见情况
if srcMap, srcOK := srcVal.(map[string]interface{}); srcOK {
if destMap, destOK := destVal.(map[string]interface{}); destOK {
if err := cms.fastMergeRecursive(srcMap, destMap, depth+1); err != nil {
return err
}
continue
}
}
// 快速空值检查(避免反射)
if srcVal == nil || srcVal == "" || srcVal == 0 {
continue
}
}
dest[key] = srcVal
}
return nil return nil
} }
// BytesProvider 轻量字节提供器
type BytesProvider struct{ data []byte }
func (bp *BytesProvider) ReadBytes() ([]byte, error) { return bp.data, nil }
func (bp *BytesProvider) Read() (map[string]interface{}, error) {
var result map[string]interface{}
return result, json.Unmarshal(bp.data, &result)
}
// 工具函数
func orDefault[T any](value, defaultValue T) T {
var zero T
if reflect.DeepEqual(value, zero) {
return defaultValue
}
return value
}
func copyFile(src, dst string) error {
data, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dst, data, 0644)
}
func chainLoad(k *koanf.Koanf, loaders ...func() error) error {
for _, loader := range loaders {
if err := loader(); err != nil {
return err
}
}
return nil
}
// 工厂函数
func NewAppConfigMigrationService(logger *log.Service, 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" ConfigChangeTypeDataPath ConfigChangeType = "datapath"
// ConfigChangeTypeBackup 备份配置变更 // ConfigChangeTypeBackup 备份配置变更
ConfigChangeTypeBackup ConfigChangeType = "backup" ConfigChangeTypeBackup ConfigChangeType = "backup"
// ConfigChangeTypeWindowSnap 窗口吸附配置变更
ConfigChangeTypeWindowSnap ConfigChangeType = "windowsnap"
) )
// ConfigChangeCallback 配置变更回调函数类型 // ConfigChangeCallback 配置变更回调函数类型
@@ -51,7 +53,7 @@ type ConfigListener struct {
type ConfigNotificationService struct { type ConfigNotificationService struct {
listeners map[ConfigChangeType][]*ConfigListener // 支持多监听器的map listeners map[ConfigChangeType][]*ConfigListener // 支持多监听器的map
mu sync.RWMutex // 监听器map的读写锁 mu sync.RWMutex // 监听器map的读写锁
logger *log.Service // 日志服务 logger *log.LogService // 日志服务
koanf *koanf.Koanf // koanf实例 koanf *koanf.Koanf // koanf实例
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
@@ -59,7 +61,7 @@ type ConfigNotificationService struct {
} }
// NewConfigNotificationService 创建配置通知服务 // NewConfigNotificationService 创建配置通知服务
func NewConfigNotificationService(k *koanf.Koanf, logger *log.Service) *ConfigNotificationService { func NewConfigNotificationService(k *koanf.Koanf, logger *log.LogService) *ConfigNotificationService {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
return &ConfigNotificationService{ return &ConfigNotificationService{
listeners: make(map[ConfigChangeType][]*ConfigListener), listeners: make(map[ConfigChangeType][]*ConfigListener),
@@ -286,25 +288,6 @@ func deepCopyValue(src, dst reflect.Value) {
} }
} }
// deepCopyConfig 保留原有的JSON深拷贝方法作为备用
func deepCopyConfig(src *models.AppConfig) *models.AppConfig {
if src == nil {
return nil
}
jsonBytes, err := json.Marshal(src)
if err != nil {
return src
}
var dst models.AppConfig
if err := json.Unmarshal(jsonBytes, &dst); err != nil {
return src
}
return &dst
}
// debounceNotify 防抖通知 // debounceNotify 防抖通知
func (cns *ConfigNotificationService) debounceNotify(listener *ConfigListener, oldConfig, newConfig *models.AppConfig) { func (cns *ConfigNotificationService) debounceNotify(listener *ConfigListener, oldConfig, newConfig *models.AppConfig) {
listener.mu.Lock() listener.mu.Lock()
@@ -470,6 +453,29 @@ func CreateBackupConfigListener(name string, callback func(config *models.GitBac
} }
} }
// CreateWindowSnapConfigListener 创建窗口吸附配置监听器
func CreateWindowSnapConfigListener(name string, callback func(enabled bool) error) *ConfigListener {
return &ConfigListener{
Name: name,
ChangeType: ConfigChangeTypeWindowSnap,
Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
if newConfig == nil {
defaultConfig := models.NewDefaultAppConfig()
return callback(defaultConfig.General.EnableWindowSnap)
}
return callback(newConfig.General.EnableWindowSnap)
},
DebounceDelay: 200 * time.Millisecond,
GetConfigFunc: func(k *koanf.Koanf) *models.AppConfig {
var config models.AppConfig
if err := k.Unmarshal("", &config); err != nil {
return nil
}
return &config
},
}
}
// ServiceShutdown 关闭服务 // ServiceShutdown 关闭服务
func (cns *ConfigNotificationService) ServiceShutdown() error { func (cns *ConfigNotificationService) ServiceShutdown() error {
cns.Cleanup() cns.Cleanup()

View File

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

View File

@@ -63,6 +63,19 @@ CREATE TABLE IF NOT EXISTS key_bindings (
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(command, extension) UNIQUE(command, extension)
)` )`
// Themes table
sqlCreateThemesTable = `
CREATE TABLE IF NOT EXISTS themes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL,
colors TEXT NOT NULL,
is_default INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(type, is_default)
)`
) )
// ColumnInfo 存储列的信息 // ColumnInfo 存储列的信息
@@ -80,7 +93,7 @@ type TableModel struct {
// DatabaseService provides shared database functionality // DatabaseService provides shared database functionality
type DatabaseService struct { type DatabaseService struct {
configService *ConfigService configService *ConfigService
logger *log.Service logger *log.LogService
db *sql.DB db *sql.DB
mu sync.RWMutex mu sync.RWMutex
ctx context.Context ctx context.Context
@@ -88,7 +101,7 @@ type DatabaseService struct {
} }
// NewDatabaseService creates a new database service // NewDatabaseService creates a new database service
func NewDatabaseService(configService *ConfigService, logger *log.Service) *DatabaseService { func NewDatabaseService(configService *ConfigService, logger *log.LogService) *DatabaseService {
if logger == nil { if logger == nil {
logger = log.New() logger = log.New()
} }
@@ -112,6 +125,8 @@ func (ds *DatabaseService) registerAllModels() {
ds.RegisterModel("extensions", &models.Extension{}) ds.RegisterModel("extensions", &models.Extension{})
// 快捷键表 // 快捷键表
ds.RegisterModel("key_bindings", &models.KeyBinding{}) ds.RegisterModel("key_bindings", &models.KeyBinding{})
// 主题表
ds.RegisterModel("themes", &models.Theme{})
} }
// ServiceStartup initializes the service when the application starts // ServiceStartup initializes the service when the application starts
@@ -181,6 +196,7 @@ func (ds *DatabaseService) createTables() error {
sqlCreateDocumentsTable, sqlCreateDocumentsTable,
sqlCreateExtensionsTable, sqlCreateExtensionsTable,
sqlCreateKeyBindingsTable, sqlCreateKeyBindingsTable,
sqlCreateThemesTable,
} }
for _, table := range tables { for _, table := range tables {
@@ -204,6 +220,9 @@ func (ds *DatabaseService) createIndexes() error {
`CREATE INDEX IF NOT EXISTS idx_key_bindings_command ON key_bindings(command)`, `CREATE INDEX IF NOT EXISTS idx_key_bindings_command ON key_bindings(command)`,
`CREATE INDEX IF NOT EXISTS idx_key_bindings_extension ON key_bindings(extension)`, `CREATE INDEX IF NOT EXISTS idx_key_bindings_extension ON key_bindings(extension)`,
`CREATE INDEX IF NOT EXISTS idx_key_bindings_enabled ON key_bindings(enabled)`, `CREATE INDEX IF NOT EXISTS idx_key_bindings_enabled ON key_bindings(enabled)`,
// Themes indexes
`CREATE INDEX IF NOT EXISTS idx_themes_type ON themes(type)`,
`CREATE INDEX IF NOT EXISTS idx_themes_is_default ON themes(is_default)`,
} }
for _, index := range indexes { for _, index := range indexes {

View File

@@ -7,12 +7,12 @@ import (
// DialogService 对话框服务,处理文件选择等对话框操作 // DialogService 对话框服务,处理文件选择等对话框操作
type DialogService struct { type DialogService struct {
logger *log.Service logger *log.LogService
window *application.WebviewWindow // 绑定的窗口 window *application.WebviewWindow // 绑定的窗口
} }
// NewDialogService 创建新的对话框服务实例 // NewDialogService 创建新的对话框服务实例
func NewDialogService(logger *log.Service) *DialogService { func NewDialogService(logger *log.LogService) *DialogService {
if logger == nil { if logger == nil {
logger = log.New() logger = log.New()
} }

View File

@@ -79,13 +79,13 @@ WHERE id = ?`
// DocumentService provides document management functionality // DocumentService provides document management functionality
type DocumentService struct { type DocumentService struct {
databaseService *DatabaseService databaseService *DatabaseService
logger *log.Service logger *log.LogService
mu sync.RWMutex mu sync.RWMutex
ctx context.Context ctx context.Context
} }
// NewDocumentService creates a new document service // NewDocumentService creates a new document service
func NewDocumentService(databaseService *DatabaseService, logger *log.Service) *DocumentService { func NewDocumentService(databaseService *DatabaseService, logger *log.LogService) *DocumentService {
if logger == nil { if logger == nil {
logger = log.New() logger = log.New()
} }
@@ -216,14 +216,11 @@ func (ds *DocumentService) CreateDocument(title string) (*models.Document, error
// LockDocument 锁定文档,防止删除 // LockDocument 锁定文档,防止删除
func (ds *DocumentService) LockDocument(id int64) error { func (ds *DocumentService) LockDocument(id int64) error {
ds.mu.Lock()
defer ds.mu.Unlock()
if ds.databaseService == nil || ds.databaseService.db == nil { if ds.databaseService == nil || ds.databaseService.db == nil {
return errors.New("database service not available") return errors.New("database service not available")
} }
// 检查文档是否存在且未删除 // 检查文档是否存在且未删除(不加锁避免死锁)
doc, err := ds.GetDocumentByID(id) doc, err := ds.GetDocumentByID(id)
if err != nil { if err != nil {
return fmt.Errorf("failed to get document: %w", err) return fmt.Errorf("failed to get document: %w", err)
@@ -240,6 +237,10 @@ func (ds *DocumentService) LockDocument(id int64) error {
return nil return nil
} }
// 现在加锁执行锁定操作
ds.mu.Lock()
defer ds.mu.Unlock()
_, err = ds.databaseService.db.Exec(sqlSetDocumentLocked, time.Now().Format("2006-01-02 15:04:05"), id) _, err = ds.databaseService.db.Exec(sqlSetDocumentLocked, time.Now().Format("2006-01-02 15:04:05"), id)
if err != nil { if err != nil {
return fmt.Errorf("failed to lock document: %w", err) return fmt.Errorf("failed to lock document: %w", err)
@@ -249,14 +250,11 @@ func (ds *DocumentService) LockDocument(id int64) error {
// UnlockDocument 解锁文档 // UnlockDocument 解锁文档
func (ds *DocumentService) UnlockDocument(id int64) error { func (ds *DocumentService) UnlockDocument(id int64) error {
ds.mu.Lock()
defer ds.mu.Unlock()
if ds.databaseService == nil || ds.databaseService.db == nil { if ds.databaseService == nil || ds.databaseService.db == nil {
return errors.New("database service not available") return errors.New("database service not available")
} }
// 检查文档是否存在 // 检查文档是否存在(不加锁避免死锁)
doc, err := ds.GetDocumentByID(id) doc, err := ds.GetDocumentByID(id)
if err != nil { if err != nil {
return fmt.Errorf("failed to get document: %w", err) return fmt.Errorf("failed to get document: %w", err)
@@ -270,6 +268,10 @@ func (ds *DocumentService) UnlockDocument(id int64) error {
return nil return nil
} }
// 现在加锁执行解锁操作
ds.mu.Lock()
defer ds.mu.Unlock()
_, err = ds.databaseService.db.Exec(sqlSetDocumentUnlocked, time.Now().Format("2006-01-02 15:04:05"), id) _, err = ds.databaseService.db.Exec(sqlSetDocumentUnlocked, time.Now().Format("2006-01-02 15:04:05"), id)
if err != nil { if err != nil {
return fmt.Errorf("failed to unlock document: %w", err) return fmt.Errorf("failed to unlock document: %w", err)
@@ -311,10 +313,8 @@ func (ds *DocumentService) UpdateDocumentTitle(id int64, title string) error {
// DeleteDocument marks a document as deleted (default document with ID=1 cannot be deleted) // DeleteDocument marks a document as deleted (default document with ID=1 cannot be deleted)
func (ds *DocumentService) DeleteDocument(id int64) error { func (ds *DocumentService) DeleteDocument(id int64) error {
ds.mu.Lock()
defer ds.mu.Unlock()
if ds.databaseService == nil || ds.databaseService.db == nil { if ds.databaseService == nil || ds.databaseService.db == nil {
ds.logger.Error("database service not available")
return errors.New("database service not available") return errors.New("database service not available")
} }
@@ -323,7 +323,7 @@ func (ds *DocumentService) DeleteDocument(id int64) error {
return fmt.Errorf("cannot delete the default document") return fmt.Errorf("cannot delete the default document")
} }
// 检查文档是否锁定 // 检查文档是否存在和锁定状态(不加锁避免死锁)
doc, err := ds.GetDocumentByID(id) doc, err := ds.GetDocumentByID(id)
if err != nil { if err != nil {
return fmt.Errorf("failed to get document: %w", err) return fmt.Errorf("failed to get document: %w", err)
@@ -335,6 +335,10 @@ func (ds *DocumentService) DeleteDocument(id int64) error {
return fmt.Errorf("cannot delete locked document: %d", id) return fmt.Errorf("cannot delete locked document: %d", id)
} }
// 现在加锁执行删除操作
ds.mu.Lock()
defer ds.mu.Unlock()
_, err = ds.databaseService.db.Exec(sqlMarkDocumentAsDeleted, time.Now().Format("2006-01-02 15:04:05"), id) _, err = ds.databaseService.db.Exec(sqlMarkDocumentAsDeleted, time.Now().Format("2006-01-02 15:04:05"), id)
if err != nil { if err != nil {
return fmt.Errorf("failed to mark document as deleted: %w", err) return fmt.Errorf("failed to mark document as deleted: %w", err)

View File

@@ -41,7 +41,7 @@ WHERE id = ?`
// ExtensionService 扩展管理服务 // ExtensionService 扩展管理服务
type ExtensionService struct { type ExtensionService struct {
databaseService *DatabaseService databaseService *DatabaseService
logger *log.Service logger *log.LogService
mu sync.RWMutex mu sync.RWMutex
ctx context.Context ctx context.Context
@@ -73,7 +73,7 @@ func (e *ExtensionError) Is(target error) bool {
} }
// NewExtensionService 创建扩展服务实例 // NewExtensionService 创建扩展服务实例
func NewExtensionService(databaseService *DatabaseService, logger *log.Service) *ExtensionService { func NewExtensionService(databaseService *DatabaseService, logger *log.LogService) *ExtensionService {
if logger == nil { if logger == nil {
logger = log.New() logger = log.New()
} }

View File

@@ -24,17 +24,19 @@ import (
// HotkeyService Windows全局热键服务 // HotkeyService Windows全局热键服务
type HotkeyService struct { type HotkeyService struct {
logger *log.Service logger *log.LogService
configService *ConfigService configService *ConfigService
windowService *WindowService
app *application.App app *application.App
mainWindow *application.WebviewWindow
mu sync.RWMutex mu sync.RWMutex
currentHotkey *models.HotkeyCombo currentHotkey *models.HotkeyCombo
isRegistered atomic.Bool isRegistered atomic.Bool
ctx context.Context ctx context.Context
cancel context.CancelFunc cancelFunc atomic.Value // 使用atomic.Value存储cancel函数避免竞态条件
wg sync.WaitGroup wg sync.WaitGroup
} }
// HotkeyError 热键错误 // HotkeyError 热键错误
@@ -51,24 +53,48 @@ func (e *HotkeyError) Unwrap() error {
return e.Err return e.Err
} }
// setCancelFunc 原子地设置cancel函数
func (hs *HotkeyService) setCancelFunc(cancel context.CancelFunc) {
hs.cancelFunc.Store(cancel)
}
// getCancelFunc 原子地获取cancel函数
func (hs *HotkeyService) getCancelFunc() context.CancelFunc {
if cancel := hs.cancelFunc.Load(); cancel != nil {
if cancelFunc, ok := cancel.(context.CancelFunc); ok {
return cancelFunc
}
}
return nil
}
// clearCancelFunc 原子地清除cancel函数
func (hs *HotkeyService) clearCancelFunc() {
hs.cancelFunc.Store((context.CancelFunc)(nil))
}
// NewHotkeyService 创建热键服务实例 // NewHotkeyService 创建热键服务实例
func NewHotkeyService(configService *ConfigService, logger *log.Service) *HotkeyService { func NewHotkeyService(configService *ConfigService, windowService *WindowService, logger *log.LogService) *HotkeyService {
if logger == nil { if logger == nil {
logger = log.New() logger = log.New()
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
return &HotkeyService{ service := &HotkeyService{
logger: logger, logger: logger,
configService: configService, configService: configService,
windowService: windowService,
ctx: ctx, ctx: ctx,
cancel: cancel,
} }
// 初始化时设置cancel函数
service.setCancelFunc(cancel)
return service
} }
// Initialize 初始化热键服务 // Initialize 初始化热键服务
func (hs *HotkeyService) Initialize(app *application.App) error { func (hs *HotkeyService) Initialize(app *application.App, mainWindow *application.WebviewWindow) error {
hs.app = app hs.app = app
hs.mainWindow = mainWindow
config, err := hs.configService.GetConfig() config, err := hs.configService.GetConfig()
if err != nil { if err != nil {
@@ -119,7 +145,7 @@ func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error {
hs.currentHotkey = hotkey hs.currentHotkey = hotkey
hs.isRegistered.Store(true) hs.isRegistered.Store(true)
hs.cancel = cancel hs.setCancelFunc(cancel)
return nil return nil
} }
@@ -137,13 +163,15 @@ func (hs *HotkeyService) unregisterInternal() error {
return nil return nil
} }
if hs.cancel != nil { // 原子地获取并调用cancel函数
hs.cancel() if cancel := hs.getCancelFunc(); cancel != nil {
cancel()
hs.wg.Wait() hs.wg.Wait()
} }
hs.currentHotkey = nil hs.currentHotkey = nil
hs.isRegistered.Store(false) hs.isRegistered.Store(false)
hs.clearCancelFunc()
return nil return nil
} }
@@ -165,7 +193,7 @@ func (hs *HotkeyService) hotkeyListener(ctx context.Context, hotkey *models.Hotk
return return
} }
ticker := time.NewTicker(100 * time.Millisecond) ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
var wasPressed bool var wasPressed bool
@@ -199,11 +227,61 @@ func cBool(b bool) C.int {
return 0 return 0
} }
// toggleWindow 切换窗口 // toggleWindow 切换窗口显示状态
func (hs *HotkeyService) toggleWindow() { func (hs *HotkeyService) toggleWindow() {
if hs.app != nil { if hs.mainWindow == nil {
hs.app.Event.Emit("hotkey:toggle-window", nil) hs.logger.Error("main window not set")
return
} }
// 检查主窗口是否可见
if hs.isWindowVisible(hs.mainWindow) {
// 如果主窗口可见,隐藏所有窗口
hs.hideAllWindows()
} else {
// 如果主窗口不可见,显示所有窗口
hs.showAllWindows()
}
}
// isWindowVisible 检查窗口是否可见
func (hs *HotkeyService) isWindowVisible(window *application.WebviewWindow) bool {
return window.IsVisible()
}
// hideAllWindows 隐藏所有窗口
func (hs *HotkeyService) hideAllWindows() {
// 隐藏主窗口
hs.mainWindow.Hide()
// 隐藏所有子窗口
if hs.windowService != nil {
openWindows := hs.windowService.GetOpenWindows()
for _, windowInfo := range openWindows {
windowInfo.Window.Hide()
}
}
hs.logger.Debug("all windows hidden")
}
// showAllWindows 显示所有窗口
func (hs *HotkeyService) showAllWindows() {
// 显示主窗口
hs.mainWindow.Show()
hs.mainWindow.Restore()
hs.mainWindow.Focus()
// 显示所有子窗口
if hs.windowService != nil {
openWindows := hs.windowService.GetOpenWindows()
for _, windowInfo := range openWindows {
windowInfo.Window.Show()
windowInfo.Window.Restore()
}
}
hs.logger.Debug("all windows shown")
} }
// keyToVirtualKeyCode 键名转虚拟键码 // keyToVirtualKeyCode 键名转虚拟键码
@@ -261,7 +339,10 @@ func (hs *HotkeyService) IsRegistered() bool {
// ServiceShutdown 关闭服务 // ServiceShutdown 关闭服务
func (hs *HotkeyService) ServiceShutdown() error { func (hs *HotkeyService) ServiceShutdown() error {
hs.cancel() // 原子地获取并调用cancel函数
if cancel := hs.getCancelFunc(); cancel != nil {
cancel()
}
hs.wg.Wait() hs.wg.Wait()
return nil return nil
} }

View File

@@ -66,6 +66,7 @@ int isHotkeyRegistered() {
import "C" import "C"
import ( import (
"context"
"fmt" "fmt"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -80,12 +81,15 @@ var globalHotkeyService *HotkeyService
// HotkeyService macOS全局热键服务 // HotkeyService macOS全局热键服务
type HotkeyService struct { type HotkeyService struct {
logger *log.Service logger *log.LogService
configService *ConfigService configService *ConfigService
windowService *WindowService
app *application.App app *application.App
mainWindow *application.WebviewWindow
mu sync.RWMutex mu sync.RWMutex
isRegistered atomic.Bool isRegistered atomic.Bool
currentHotkey *models.HotkeyCombo currentHotkey *models.HotkeyCombo
cancelFunc atomic.Value // 使用atomic.Value存储cancel函数避免竞态条件
} }
// HotkeyError 热键错误 // HotkeyError 热键错误
@@ -104,8 +108,28 @@ func (e *HotkeyError) Unwrap() error {
return e.Err return e.Err
} }
// setCancelFunc 原子地设置cancel函数
func (hs *HotkeyService) setCancelFunc(cancel context.CancelFunc) {
hs.cancelFunc.Store(cancel)
}
// getCancelFunc 原子地获取cancel函数
func (hs *HotkeyService) getCancelFunc() context.CancelFunc {
if cancel := hs.cancelFunc.Load(); cancel != nil {
if cancelFunc, ok := cancel.(context.CancelFunc); ok {
return cancelFunc
}
}
return nil
}
// clearCancelFunc 原子地清除cancel函数
func (hs *HotkeyService) clearCancelFunc() {
hs.cancelFunc.Store((context.CancelFunc)(nil))
}
// NewHotkeyService 创建新的热键服务实例 // NewHotkeyService 创建新的热键服务实例
func NewHotkeyService(configService *ConfigService, logger *log.Service) *HotkeyService { func NewHotkeyService(configService *ConfigService, windowService *WindowService, logger *log.LogService) *HotkeyService {
if logger == nil { if logger == nil {
logger = log.New() logger = log.New()
} }
@@ -113,6 +137,7 @@ func NewHotkeyService(configService *ConfigService, logger *log.Service) *Hotkey
service := &HotkeyService{ service := &HotkeyService{
logger: logger, logger: logger,
configService: configService, configService: configService,
windowService: windowService,
} }
// 设置全局实例 // 设置全局实例
@@ -122,8 +147,9 @@ func NewHotkeyService(configService *ConfigService, logger *log.Service) *Hotkey
} }
// Initialize 初始化热键服务 // Initialize 初始化热键服务
func (hs *HotkeyService) Initialize(app *application.App) error { func (hs *HotkeyService) Initialize(app *application.App, mainWindow *application.WebviewWindow) error {
hs.app = app hs.app = app
hs.mainWindow = mainWindow
// 加载并应用当前配置 // 加载并应用当前配置
config, err := hs.configService.GetConfig() config, err := hs.configService.GetConfig()
@@ -285,9 +311,59 @@ func (hs *HotkeyService) IsRegistered() bool {
// ToggleWindow 切换窗口显示状态 // ToggleWindow 切换窗口显示状态
func (hs *HotkeyService) ToggleWindow() { func (hs *HotkeyService) ToggleWindow() {
if hs.app != nil { if hs.mainWindow == nil {
hs.app.EmitEvent("hotkey:toggle-window", nil) hs.logger.Error("main window not set")
return
} }
// 检查主窗口是否可见
if hs.isWindowVisible(hs.mainWindow) {
// 如果主窗口可见,隐藏所有窗口
hs.hideAllWindows()
} else {
// 如果主窗口不可见,显示所有窗口
hs.showAllWindows()
}
}
// isWindowVisible 检查窗口是否可见
func (hs *HotkeyService) isWindowVisible(window *application.WebviewWindow) bool {
return window.IsVisible()
}
// hideAllWindows 隐藏所有窗口
func (hs *HotkeyService) hideAllWindows() {
// 隐藏主窗口
hs.mainWindow.Hide()
// 隐藏所有子窗口
if hs.windowService != nil {
openWindows := hs.windowService.GetOpenWindows()
for _, windowInfo := range openWindows {
windowInfo.Window.Hide()
}
}
hs.logger.Debug("all windows hidden")
}
// showAllWindows 显示所有窗口
func (hs *HotkeyService) showAllWindows() {
// 显示主窗口
hs.mainWindow.Show()
hs.mainWindow.Restore()
hs.mainWindow.Focus()
// 显示所有子窗口
if hs.windowService != nil {
openWindows := hs.windowService.GetOpenWindows()
for _, windowInfo := range openWindows {
windowInfo.Window.Show()
windowInfo.Window.Restore()
}
}
hs.logger.Debug("all windows shown")
} }
// ServiceShutdown 关闭热键服务 // ServiceShutdown 关闭热键服务

View File

@@ -141,17 +141,19 @@ import (
// HotkeyService Linux全局热键服务 // HotkeyService Linux全局热键服务
type HotkeyService struct { type HotkeyService struct {
logger *log.Service logger *log.LogService
configService *ConfigService configService *ConfigService
windowService *WindowService
app *application.App app *application.App
mainWindow *application.WebviewWindow
mu sync.RWMutex mu sync.RWMutex
currentHotkey *models.HotkeyCombo currentHotkey *models.HotkeyCombo
isRegistered atomic.Bool isRegistered atomic.Bool
ctx context.Context ctx context.Context
cancel context.CancelFunc cancelFunc atomic.Value // 使用atomic.Value存储cancel函数避免竞态条件
wg sync.WaitGroup wg sync.WaitGroup
} }
// HotkeyError 热键错误 // HotkeyError 热键错误
@@ -169,24 +171,48 @@ func (e *HotkeyError) Unwrap() error {
return e.Err return e.Err
} }
// setCancelFunc 原子地设置cancel函数
func (hs *HotkeyService) setCancelFunc(cancel context.CancelFunc) {
hs.cancelFunc.Store(cancel)
}
// getCancelFunc 原子地获取cancel函数
func (hs *HotkeyService) getCancelFunc() context.CancelFunc {
if cancel := hs.cancelFunc.Load(); cancel != nil {
if cancelFunc, ok := cancel.(context.CancelFunc); ok {
return cancelFunc
}
}
return nil
}
// clearCancelFunc 原子地清除cancel函数
func (hs *HotkeyService) clearCancelFunc() {
hs.cancelFunc.Store((context.CancelFunc)(nil))
}
// NewHotkeyService 创建热键服务实例 // NewHotkeyService 创建热键服务实例
func NewHotkeyService(configService *ConfigService, logger *log.Service) *HotkeyService { func NewHotkeyService(configService *ConfigService, windowService *WindowService, logger *log.LogService) *HotkeyService {
if logger == nil { if logger == nil {
logger = log.New() logger = log.New()
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
return &HotkeyService{ service := &HotkeyService{
logger: logger, logger: logger,
configService: configService, configService: configService,
windowService: windowService,
ctx: ctx, ctx: ctx,
cancel: cancel,
} }
// 初始化时设置cancel函数
service.setCancelFunc(cancel)
return service
} }
// Initialize 初始化热键服务 // Initialize 初始化热键服务
func (hs *HotkeyService) Initialize(app *application.App) error { func (hs *HotkeyService) Initialize(app *application.App, mainWindow *application.WebviewWindow) error {
hs.app = app hs.app = app
hs.mainWindow = mainWindow
if int(C.initX11Display()) == 0 { if int(C.initX11Display()) == 0 {
return &HotkeyError{"init_x11", fmt.Errorf("failed to initialize X11 display")} return &HotkeyError{"init_x11", fmt.Errorf("failed to initialize X11 display")}
@@ -253,7 +279,7 @@ func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error {
hs.currentHotkey = hotkey hs.currentHotkey = hotkey
hs.isRegistered.Store(true) hs.isRegistered.Store(true)
hs.cancel = cancel hs.setCancelFunc(cancel)
return nil return nil
} }
@@ -271,8 +297,9 @@ func (hs *HotkeyService) unregisterInternal() error {
return nil return nil
} }
if hs.cancel != nil { // 原子地获取并调用cancel函数
hs.cancel() if cancel := hs.getCancelFunc(); cancel != nil {
cancel()
hs.wg.Wait() hs.wg.Wait()
} }
@@ -283,6 +310,7 @@ func (hs *HotkeyService) unregisterInternal() error {
hs.currentHotkey = nil hs.currentHotkey = nil
hs.isRegistered.Store(false) hs.isRegistered.Store(false)
hs.clearCancelFunc()
return nil return nil
} }
@@ -298,7 +326,8 @@ func (hs *HotkeyService) UpdateHotkey(enable bool, hotkey *models.HotkeyCombo) e
func (hs *HotkeyService) hotkeyListener(ctx context.Context, ready chan<- error) { func (hs *HotkeyService) hotkeyListener(ctx context.Context, ready chan<- error) {
defer hs.wg.Done() defer hs.wg.Done()
ticker := time.NewTicker(100 * time.Millisecond) // 优化轮询频率从100ms改为50ms提高响应性
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
ready <- nil // 标记准备就绪 ready <- nil // 标记准备就绪
@@ -315,11 +344,61 @@ func (hs *HotkeyService) hotkeyListener(ctx context.Context, ready chan<- error)
} }
} }
// toggleWindow 切换窗口 // toggleWindow 切换窗口显示状态
func (hs *HotkeyService) toggleWindow() { func (hs *HotkeyService) toggleWindow() {
if hs.app != nil { if hs.mainWindow == nil {
hs.app.EmitEvent("hotkey:toggle-window", nil) hs.logger.Error("main window not set")
return
} }
// 检查主窗口是否可见
if hs.isWindowVisible(hs.mainWindow) {
// 如果主窗口可见,隐藏所有窗口
hs.hideAllWindows()
} else {
// 如果主窗口不可见,显示所有窗口
hs.showAllWindows()
}
}
// isWindowVisible 检查窗口是否可见
func (hs *HotkeyService) isWindowVisible(window *application.WebviewWindow) bool {
return window.IsVisible()
}
// hideAllWindows 隐藏所有窗口
func (hs *HotkeyService) hideAllWindows() {
// 隐藏主窗口
hs.mainWindow.Hide()
// 隐藏所有子窗口
if hs.windowService != nil {
openWindows := hs.windowService.GetOpenWindows()
for _, windowInfo := range openWindows {
windowInfo.Window.Hide()
}
}
hs.logger.Debug("all windows hidden")
}
// showAllWindows 显示所有窗口
func (hs *HotkeyService) showAllWindows() {
// 显示主窗口
hs.mainWindow.Show()
hs.mainWindow.Restore()
hs.mainWindow.Focus()
// 显示所有子窗口
if hs.windowService != nil {
openWindows := hs.windowService.GetOpenWindows()
for _, windowInfo := range openWindows {
windowInfo.Window.Show()
windowInfo.Window.Restore()
}
}
hs.logger.Debug("all windows shown")
} }
// keyToX11KeyCode 键名转X11键码 // keyToX11KeyCode 键名转X11键码
@@ -386,7 +465,10 @@ func (hs *HotkeyService) IsRegistered() bool {
// ServiceShutdown 关闭服务 // ServiceShutdown 关闭服务
func (hs *HotkeyService) ServiceShutdown() error { func (hs *HotkeyService) ServiceShutdown() error {
hs.cancel() // 原子地获取并调用cancel函数
if cancel := hs.getCancelFunc(); cancel != nil {
cancel()
}
hs.wg.Wait() hs.wg.Wait()
C.closeX11Display() C.closeX11Display()
return nil return nil

View File

@@ -51,7 +51,7 @@ const (
// KeyBindingService 快捷键管理服务 // KeyBindingService 快捷键管理服务
type KeyBindingService struct { type KeyBindingService struct {
databaseService *DatabaseService databaseService *DatabaseService
logger *log.Service logger *log.LogService
mu sync.RWMutex mu sync.RWMutex
ctx context.Context ctx context.Context
@@ -83,7 +83,7 @@ func (e *KeyBindingError) Is(target error) bool {
} }
// NewKeyBindingService 创建快捷键服务实例 // NewKeyBindingService 创建快捷键服务实例
func NewKeyBindingService(databaseService *DatabaseService, logger *log.Service) *KeyBindingService { func NewKeyBindingService(databaseService *DatabaseService, logger *log.LogService) *KeyBindingService {
if logger == nil { if logger == nil {
logger = log.New() logger = log.New()
} }

View File

@@ -33,22 +33,24 @@ type MigrationProgress struct {
// MigrationService 迁移服务 // MigrationService 迁移服务
type MigrationService struct { type MigrationService struct {
logger *log.Service logger *log.LogService
mu sync.RWMutex dbService *DatabaseService
progress atomic.Value // stores MigrationProgress mu sync.RWMutex
progress atomic.Value // stores MigrationProgress
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
} }
// NewMigrationService 创建迁移服务 // NewMigrationService 创建迁移服务
func NewMigrationService(logger *log.Service) *MigrationService { func NewMigrationService(dbService *DatabaseService, logger *log.LogService) *MigrationService {
if logger == nil { if logger == nil {
logger = log.New() logger = log.New()
} }
ms := &MigrationService{ ms := &MigrationService{
logger: logger, logger: logger,
dbService: dbService,
} }
// 初始化进度 // 初始化进度
@@ -104,6 +106,17 @@ func (ms *MigrationService) MigrateDirectory(srcPath, dstPath string) error {
return ms.failWithError(err) return ms.failWithError(err)
} }
// 迁移前断开数据库连接
ms.updateProgress(MigrationProgress{
Status: MigrationStatusMigrating,
Progress: 10,
})
if ms.dbService != nil {
if err := ms.dbService.ServiceShutdown(); err != nil {
ms.logger.Error("Failed to close database connection", "error", err)
}
}
// 执行原子迁移 // 执行原子迁移
if err := ms.atomicMove(ctx, srcPath, dstPath); err != nil { if err := ms.atomicMove(ctx, srcPath, dstPath); err != nil {
return ms.failWithError(err) return ms.failWithError(err)
@@ -177,10 +190,17 @@ func (ms *MigrationService) atomicMove(ctx context.Context, srcPath, dstPath str
}) })
if err := os.Rename(srcPath, dstPath); err == nil { if err := os.Rename(srcPath, dstPath); err == nil {
// 重命名成功更新进度到90%
ms.updateProgress(MigrationProgress{
Status: MigrationStatusMigrating,
Progress: 90,
})
ms.logger.Info("Directory migration completed using direct rename", "src", srcPath, "dst", dstPath)
return nil return nil
} }
// 重命名失败,使用压缩迁移 // 重命名失败,使用压缩迁移
ms.logger.Info("Direct rename failed, using compress migration", "src", srcPath, "dst", dstPath)
ms.updateProgress(MigrationProgress{ ms.updateProgress(MigrationProgress{
Status: MigrationStatusMigrating, Status: MigrationStatusMigrating,
Progress: 30, Progress: 30,
@@ -249,12 +269,18 @@ func (ms *MigrationService) compressMove(ctx context.Context, srcPath, dstPath s
default: default:
} }
// 验证迁移是否成功
if err := ms.verifyMigration(dstPath); err != nil {
// 迁移验证失败,清理目标目录
os.RemoveAll(dstPath)
return fmt.Errorf("migration verification failed: %v", err)
}
// 删除源目录 // 删除源目录
ms.updateProgress(MigrationProgress{ ms.updateProgress(MigrationProgress{
Status: MigrationStatusMigrating, Status: MigrationStatusMigrating,
Progress: 90, Progress: 90,
}) })
os.RemoveAll(srcPath) os.RemoveAll(srcPath)
return nil return nil
} }
@@ -404,6 +430,29 @@ func (ms *MigrationService) isSubDirectory(parent, target string) bool {
return len(target) > len(parent) && strings.HasPrefix(target, parent) return len(target) > len(parent) && strings.HasPrefix(target, parent)
} }
// verifyMigration 验证迁移是否成功
func (ms *MigrationService) verifyMigration(dstPath string) error {
// 检查目标目录是否存在
dstStat, err := os.Stat(dstPath)
if err != nil {
return fmt.Errorf("target directory does not exist: %v", err)
}
if !dstStat.IsDir() {
return fmt.Errorf("target path is not a directory")
}
// 简单验证:检查目标目录是否非空
isEmpty, err := ms.isDirectoryEmpty(dstPath)
if err != nil {
return fmt.Errorf("failed to check target directory: %v", err)
}
if isEmpty {
return fmt.Errorf("target directory is empty after migration")
}
return nil
}
// CancelMigration 取消迁移 // CancelMigration 取消迁移
func (ms *MigrationService) CancelMigration() error { func (ms *MigrationService) CancelMigration() error {
ms.mu.Lock() ms.mu.Lock()

View File

@@ -26,7 +26,7 @@ type SelfUpdateResult struct {
// SelfUpdateService 自我更新服务 // SelfUpdateService 自我更新服务
type SelfUpdateService struct { type SelfUpdateService struct {
logger *log.Service logger *log.LogService
configService *ConfigService configService *ConfigService
config *models.AppConfig config *models.AppConfig
@@ -35,7 +35,7 @@ type SelfUpdateService struct {
} }
// NewSelfUpdateService 创建自我更新服务实例 // NewSelfUpdateService 创建自我更新服务实例
func NewSelfUpdateService(configService *ConfigService, logger *log.Service) (*SelfUpdateService, error) { func NewSelfUpdateService(configService *ConfigService, logger *log.LogService) (*SelfUpdateService, error) {
// 获取配置 // 获取配置
appConfig, err := configService.GetConfig() appConfig, err := configService.GetConfig()
if err != nil { if err != nil {

View File

@@ -13,6 +13,7 @@ type ServiceManager struct {
databaseService *DatabaseService databaseService *DatabaseService
documentService *DocumentService documentService *DocumentService
windowService *WindowService windowService *WindowService
windowSnapService *WindowSnapService
migrationService *MigrationService migrationService *MigrationService
systemService *SystemService systemService *SystemService
hotkeyService *HotkeyService hotkeyService *HotkeyService
@@ -23,8 +24,9 @@ type ServiceManager struct {
startupService *StartupService startupService *StartupService
selfUpdateService *SelfUpdateService selfUpdateService *SelfUpdateService
translationService *TranslationService translationService *TranslationService
themeService *ThemeService
BackupService *BackupService BackupService *BackupService
logger *log.Service logger *log.LogService
} }
// NewServiceManager 创建新的服务管理器实例 // NewServiceManager 创建新的服务管理器实例
@@ -39,7 +41,7 @@ func NewServiceManager() *ServiceManager {
databaseService := NewDatabaseService(configService, logger) databaseService := NewDatabaseService(configService, logger)
// 初始化迁移服务 // 初始化迁移服务
migrationService := NewMigrationService(logger) migrationService := NewMigrationService(databaseService, logger)
// 初始化文档服务 // 初始化文档服务
documentService := NewDocumentService(databaseService, logger) documentService := NewDocumentService(databaseService, logger)
@@ -47,11 +49,17 @@ func NewServiceManager() *ServiceManager {
// 初始化窗口服务 // 初始化窗口服务
windowService := NewWindowService(logger, documentService) windowService := NewWindowService(logger, documentService)
// 初始化窗口吸附服务
windowSnapService := NewWindowSnapService(logger, configService)
// 将吸附服务与窗口服务关联
windowService.SetWindowSnapService(windowSnapService)
// 初始化系统服务 // 初始化系统服务
systemService := NewSystemService(logger) systemService := NewSystemService(logger)
// 初始化热键服务 // 初始化热键服务
hotkeyService := NewHotkeyService(configService, logger) hotkeyService := NewHotkeyService(configService, windowService, logger)
// 初始化对话服务 // 初始化对话服务
dialogService := NewDialogService(logger) dialogService := NewDialogService(logger)
@@ -77,6 +85,9 @@ func NewServiceManager() *ServiceManager {
// 初始化翻译服务 // 初始化翻译服务
translationService := NewTranslationService(logger) translationService := NewTranslationService(logger)
// 初始化主题服务
themeService := NewThemeService(databaseService, logger)
// 初始化备份服务 // 初始化备份服务
backupService := NewBackupService(configService, databaseService, logger) backupService := NewBackupService(configService, databaseService, logger)
@@ -104,11 +115,20 @@ func NewServiceManager() *ServiceManager {
panic(err) panic(err)
} }
// 设置窗口吸附配置变更回调
err = configService.SetWindowSnapConfigChangeCallback(func(enabled bool) error {
return windowSnapService.OnWindowSnapConfigChanged(enabled)
})
if err != nil {
panic(err)
}
return &ServiceManager{ return &ServiceManager{
configService: configService, configService: configService,
databaseService: databaseService, databaseService: databaseService,
documentService: documentService, documentService: documentService,
windowService: windowService, windowService: windowService,
windowSnapService: windowSnapService,
migrationService: migrationService, migrationService: migrationService,
systemService: systemService, systemService: systemService,
hotkeyService: hotkeyService, hotkeyService: hotkeyService,
@@ -119,6 +139,7 @@ func NewServiceManager() *ServiceManager {
startupService: startupService, startupService: startupService,
selfUpdateService: selfUpdateService, selfUpdateService: selfUpdateService,
translationService: translationService, translationService: translationService,
themeService: themeService,
BackupService: backupService, BackupService: backupService,
logger: logger, logger: logger,
} }
@@ -131,6 +152,7 @@ func (sm *ServiceManager) GetServices() []application.Service {
application.NewService(sm.databaseService), application.NewService(sm.databaseService),
application.NewService(sm.documentService), application.NewService(sm.documentService),
application.NewService(sm.windowService), application.NewService(sm.windowService),
application.NewService(sm.windowSnapService),
application.NewService(sm.keyBindingService), application.NewService(sm.keyBindingService),
application.NewService(sm.extensionService), application.NewService(sm.extensionService),
application.NewService(sm.migrationService), application.NewService(sm.migrationService),
@@ -141,6 +163,7 @@ func (sm *ServiceManager) GetServices() []application.Service {
application.NewService(sm.startupService), application.NewService(sm.startupService),
application.NewService(sm.selfUpdateService), application.NewService(sm.selfUpdateService),
application.NewService(sm.translationService), application.NewService(sm.translationService),
application.NewService(sm.themeService),
application.NewService(sm.BackupService), application.NewService(sm.BackupService),
} }
return services return services
@@ -157,7 +180,7 @@ func (sm *ServiceManager) GetDialogService() *DialogService {
} }
// GetLogger 获取日志服务实例 // GetLogger 获取日志服务实例
func (sm *ServiceManager) GetLogger() *log.Service { func (sm *ServiceManager) GetLogger() *log.LogService {
return sm.logger return sm.logger
} }
@@ -210,3 +233,13 @@ func (sm *ServiceManager) GetWindowService() *WindowService {
func (sm *ServiceManager) GetDocumentService() *DocumentService { func (sm *ServiceManager) GetDocumentService() *DocumentService {
return sm.documentService return sm.documentService
} }
// GetThemeService 获取主题服务实例
func (sm *ServiceManager) GetThemeService() *ThemeService {
return sm.themeService
}
// GetWindowSnapService 获取窗口吸附服务实例
func (sm *ServiceManager) GetWindowSnapService() *WindowSnapService {
return sm.windowSnapService
}

View File

@@ -15,14 +15,14 @@ import (
// DarwinStartupImpl macOS 平台开机启动实现 // DarwinStartupImpl macOS 平台开机启动实现
type DarwinStartupImpl struct { type DarwinStartupImpl struct {
logger *log.Service logger *log.LogService
disabled bool disabled bool
appPath string appPath string
appName string appName string
} }
// newStartupImplementation 创建平台特定的开机启动实现 // newStartupImplementation 创建平台特定的开机启动实现
func newStartupImplementation(logger *log.Service) StartupImplementation { func newStartupImplementation(logger *log.LogService) StartupImplementation {
return &DarwinStartupImpl{ return &DarwinStartupImpl{
logger: logger, logger: logger,
} }

View File

@@ -13,7 +13,7 @@ import (
// LinuxStartupImpl Linux 平台开机启动实现 // LinuxStartupImpl Linux 平台开机启动实现
type LinuxStartupImpl struct { type LinuxStartupImpl struct {
logger *log.Service logger *log.LogService
autostartDir string autostartDir string
execPath string execPath string
appName string appName string
@@ -37,7 +37,7 @@ X-GNOME-Autostart-enabled=true
` `
// newStartupImplementation 创建平台特定的开机启动实现 // newStartupImplementation 创建平台特定的开机启动实现
func newStartupImplementation(logger *log.Service) StartupImplementation { func newStartupImplementation(logger *log.LogService) StartupImplementation {
return &LinuxStartupImpl{ return &LinuxStartupImpl{
logger: logger, logger: logger,
} }

View File

@@ -7,7 +7,7 @@ import (
// StartupService 开机启动服务 // StartupService 开机启动服务
type StartupService struct { type StartupService struct {
configService *ConfigService configService *ConfigService
logger *log.Service logger *log.LogService
impl StartupImplementation impl StartupImplementation
initError error initError error
} }
@@ -19,7 +19,7 @@ type StartupImplementation interface {
} }
// NewStartupService 创建开机启动服务实例 // NewStartupService 创建开机启动服务实例
func NewStartupService(configService *ConfigService, logger *log.Service) *StartupService { func NewStartupService(configService *ConfigService, logger *log.LogService) *StartupService {
service := &StartupService{ service := &StartupService{
configService: configService, configService: configService,
logger: logger, logger: logger,

View File

@@ -15,7 +15,7 @@ import (
// WindowsStartupImpl Windows 平台开机启动实现 // WindowsStartupImpl Windows 平台开机启动实现
type WindowsStartupImpl struct { type WindowsStartupImpl struct {
logger *log.Service logger *log.LogService
registryKey string registryKey string
execPath string execPath string
workingDir string workingDir string
@@ -23,7 +23,7 @@ type WindowsStartupImpl struct {
} }
// newStartupImplementation 创建平台特定的开机启动实现 // newStartupImplementation 创建平台特定的开机启动实现
func newStartupImplementation(logger *log.Service) StartupImplementation { func newStartupImplementation(logger *log.LogService) StartupImplementation {
return &WindowsStartupImpl{ return &WindowsStartupImpl{
logger: logger, logger: logger,
} }

View File

@@ -15,7 +15,7 @@ import (
type StoreOption struct { type StoreOption struct {
FilePath string FilePath string
AutoSave bool AutoSave bool
Logger *log.Service Logger *log.LogService
} }
// Store 泛型存储服务 // Store 泛型存储服务
@@ -25,7 +25,7 @@ type Store[T any] struct {
dataMap sync.Map // thread-safe map dataMap sync.Map // thread-safe map
unsaved atomic.Bool unsaved atomic.Bool
initOnce sync.Once initOnce sync.Once
logger *log.Service logger *log.LogService
} }
// NewStore 存储服务 // NewStore 存储服务

View File

@@ -9,7 +9,7 @@ import (
// SystemService 系统监控服务 // SystemService 系统监控服务
type SystemService struct { type SystemService struct {
logger *log.Service logger *log.LogService
} }
// MemoryStats 内存统计信息 // MemoryStats 内存统计信息
@@ -29,7 +29,7 @@ type MemoryStats struct {
} }
// NewSystemService 创建新的系统服务实例 // NewSystemService 创建新的系统服务实例
func NewSystemService(logger *log.Service) *SystemService { func NewSystemService(logger *log.LogService) *SystemService {
return &SystemService{ return &SystemService{
logger: logger, logger: logger,
} }

View File

@@ -0,0 +1,275 @@
package services
import (
"context"
"database/sql"
"fmt"
"time"
"voidraft/internal/models"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/services/log"
)
// ThemeService 主题服务
type ThemeService struct {
databaseService *DatabaseService
logger *log.LogService
ctx context.Context
}
// NewThemeService 创建新的主题服务
func NewThemeService(databaseService *DatabaseService, logger *log.LogService) *ThemeService {
if logger == nil {
logger = log.New()
}
return &ThemeService{
databaseService: databaseService,
logger: logger,
}
}
// ServiceStartup 服务启动时初始化
func (ts *ThemeService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
ts.ctx = ctx
// 初始化默认主题
return ts.initializeDefaultThemes()
}
// getDB 获取数据库连接
func (ts *ThemeService) getDB() *sql.DB {
return ts.databaseService.db
}
// initializeDefaultThemes 初始化默认主题
func (ts *ThemeService) initializeDefaultThemes() error {
db := ts.getDB()
if db == nil {
return fmt.Errorf("database not available")
}
// 检查是否已存在默认主题
var count int
err := db.QueryRow("SELECT COUNT(*) FROM themes WHERE is_default = 1").Scan(&count)
if err != nil {
return fmt.Errorf("failed to check existing themes: %w", err)
}
if count > 0 {
return nil // 默认主题已存在
}
// 创建默认深色主题
darkTheme := &models.Theme{
Name: "Default Dark",
Type: models.ThemeTypeDark,
Colors: *models.NewDefaultDarkTheme(),
IsDefault: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// 创建默认浅色主题
lightTheme := &models.Theme{
Name: "Default Light",
Type: models.ThemeTypeLight,
Colors: *models.NewDefaultLightTheme(),
IsDefault: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// 插入默认主题
if _, err := ts.CreateTheme(darkTheme); err != nil {
return fmt.Errorf("failed to create default dark theme: %w", err)
}
if _, err := ts.CreateTheme(lightTheme); err != nil {
return fmt.Errorf("failed to create default light theme: %w", err)
}
return nil
}
// GetDefaultThemes 获取默认主题
func (ts *ThemeService) GetDefaultThemes() (map[models.ThemeType]*models.Theme, error) {
query := `
SELECT id, name, type, colors, is_default, created_at, updated_at
FROM themes
WHERE is_default = 1
ORDER BY type
`
db := ts.getDB()
rows, err := db.Query(query)
if err != nil {
return nil, fmt.Errorf("failed to query default themes: %w", err)
}
defer rows.Close()
themes := make(map[models.ThemeType]*models.Theme)
for rows.Next() {
theme := &models.Theme{}
err := rows.Scan(
&theme.ID,
&theme.Name,
&theme.Type,
&theme.Colors,
&theme.IsDefault,
&theme.CreatedAt,
&theme.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan theme: %w", err)
}
themes[theme.Type] = theme
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("failed to iterate themes: %w", err)
}
return themes, nil
}
// GetThemeByType 根据类型获取默认主题
func (ts *ThemeService) GetThemeByType(themeType models.ThemeType) (*models.Theme, error) {
query := `
SELECT id, name, type, colors, is_default, created_at, updated_at
FROM themes
WHERE type = ? AND is_default = 1
LIMIT 1
`
theme := &models.Theme{}
db := ts.getDB()
err := db.QueryRow(query, themeType).Scan(
&theme.ID,
&theme.Name,
&theme.Type,
&theme.Colors,
&theme.IsDefault,
&theme.CreatedAt,
&theme.UpdatedAt,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("no default theme found for type: %s", themeType)
}
return nil, fmt.Errorf("failed to get theme by type: %w", err)
}
return theme, nil
}
// UpdateThemeColors 更新主题颜色
func (ts *ThemeService) UpdateThemeColors(themeType models.ThemeType, colors models.ThemeColorConfig) error {
query := `
UPDATE themes
SET colors = ?, updated_at = ?
WHERE type = ? AND is_default = 1
`
db := ts.getDB()
_, err := db.Exec(query, colors, time.Now(), themeType)
if err != nil {
return fmt.Errorf("failed to update theme colors: %w", err)
}
return nil
}
// ResetThemeColors 重置主题颜色为默认值
func (ts *ThemeService) ResetThemeColors(themeType models.ThemeType) error {
var defaultColors models.ThemeColorConfig
switch themeType {
case models.ThemeTypeDark:
defaultColors = *models.NewDefaultDarkTheme()
case models.ThemeTypeLight:
defaultColors = *models.NewDefaultLightTheme()
default:
return fmt.Errorf("unknown theme type: %s", themeType)
}
return ts.UpdateThemeColors(themeType, defaultColors)
}
// CreateTheme 创建新主题
func (ts *ThemeService) CreateTheme(theme *models.Theme) (*models.Theme, error) {
query := `
INSERT INTO themes (name, type, colors, is_default, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
`
db := ts.getDB()
result, err := db.Exec(
query,
theme.Name,
theme.Type,
theme.Colors,
theme.IsDefault,
theme.CreatedAt,
theme.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to create theme: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return nil, fmt.Errorf("failed to get theme ID: %w", err)
}
theme.ID = int(id)
return theme, nil
}
// GetAllThemes 获取所有主题
func (ts *ThemeService) GetAllThemes() ([]*models.Theme, error) {
query := `
SELECT id, name, type, colors, is_default, created_at, updated_at
FROM themes
ORDER BY is_default DESC, created_at ASC
`
db := ts.getDB()
rows, err := db.Query(query)
if err != nil {
return nil, fmt.Errorf("failed to query themes: %w", err)
}
defer rows.Close()
var themes []*models.Theme
for rows.Next() {
theme := &models.Theme{}
err := rows.Scan(
&theme.ID,
&theme.Name,
&theme.Type,
&theme.Colors,
&theme.IsDefault,
&theme.CreatedAt,
&theme.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan theme: %w", err)
}
themes = append(themes, theme)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("failed to iterate themes: %w", err)
}
return themes, nil
}
// ServiceShutdown 服务关闭
func (ts *ThemeService) ServiceShutdown() error {
return nil
}

View File

@@ -10,7 +10,7 @@ import (
// TranslationService 翻译服务 // TranslationService 翻译服务
type TranslationService struct { type TranslationService struct {
logger *log.Service logger *log.LogService
factory *translator.TranslatorFactory factory *translator.TranslatorFactory
defaultTimeout time.Duration defaultTimeout time.Duration
translators map[translator.TranslatorType]translator.Translator translators map[translator.TranslatorType]translator.Translator
@@ -18,7 +18,7 @@ type TranslationService struct {
} }
// NewTranslationService 创建翻译服务实例 // NewTranslationService 创建翻译服务实例
func NewTranslationService(logger *log.Service) *TranslationService { func NewTranslationService(logger *log.LogService) *TranslationService {
service := &TranslationService{ service := &TranslationService{
logger: logger, logger: logger,
factory: translator.NewTranslatorFactory(), factory: translator.NewTranslatorFactory(),

View File

@@ -7,14 +7,14 @@ import (
// TrayService 系统托盘服务 // TrayService 系统托盘服务
type TrayService struct { type TrayService struct {
logger *log.Service logger *log.LogService
configService *ConfigService configService *ConfigService
app *application.App app *application.App
mainWindow *application.WebviewWindow mainWindow *application.WebviewWindow
} }
// NewTrayService 创建新的系统托盘服务实例 // NewTrayService 创建新的系统托盘服务实例
func NewTrayService(logger *log.Service, configService *ConfigService) *TrayService { func NewTrayService(logger *log.LogService, configService *ConfigService) *TrayService {
return &TrayService{ return &TrayService{
logger: logger, logger: logger,
configService: configService, configService: configService,
@@ -42,7 +42,6 @@ func (ts *TrayService) HandleWindowClose() {
if ts.ShouldMinimizeToTray() { if ts.ShouldMinimizeToTray() {
// 隐藏到托盘 // 隐藏到托盘
ts.mainWindow.Hide() ts.mainWindow.Hide()
ts.app.Event.Emit("window:hidden", nil)
} else { } else {
// 直接退出应用 // 直接退出应用
ts.app.Quit() ts.app.Quit()
@@ -54,7 +53,6 @@ func (ts *TrayService) HandleWindowMinimize() {
if ts.ShouldMinimizeToTray() { if ts.ShouldMinimizeToTray() {
// 隐藏到托盘 // 隐藏到托盘
ts.mainWindow.Hide() ts.mainWindow.Hide()
ts.app.Event.Emit("window:hidden", nil)
} }
} }
@@ -64,9 +62,6 @@ func (ts *TrayService) ShowWindow() {
ts.mainWindow.Show() ts.mainWindow.Show()
ts.mainWindow.Restore() ts.mainWindow.Restore()
ts.mainWindow.Focus() ts.mainWindow.Focus()
if ts.app != nil {
ts.app.Event.Emit("window:shown", nil)
}
} }
} }

View File

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

View File

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

View File

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

View File

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

11
main.go
View File

@@ -25,6 +25,7 @@ var assets embed.FS
// logs any error that might occur. // logs any error that might occur.
func main() { func main() {
serviceManager := services.NewServiceManager() serviceManager := services.NewServiceManager()
var window *application.WebviewWindow
var encryptionKey = [32]byte{ var encryptionKey = [32]byte{
0x1e, 0x1f, 0x1c, 0x1d, 0x1a, 0x1b, 0x18, 0x19, 0x1e, 0x1f, 0x1c, 0x1d, 0x1a, 0x1b, 0x18, 0x19,
@@ -51,6 +52,13 @@ func main() {
SingleInstance: &application.SingleInstanceOptions{ SingleInstance: &application.SingleInstanceOptions{
UniqueID: "com.voidraft", UniqueID: "com.voidraft",
EncryptionKey: encryptionKey, EncryptionKey: encryptionKey,
OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
if window != nil {
window.Show()
window.Restore()
window.Focus()
}
},
AdditionalData: map[string]string{ AdditionalData: map[string]string{
"launchtime": time.Now().Local().String(), "launchtime": time.Now().Local().String(),
}, },
@@ -82,6 +90,7 @@ func main() {
URL: "/", URL: "/",
}) })
mainWindow.Center() mainWindow.Center()
window = mainWindow
// 获取托盘服务并设置应用引用 // 获取托盘服务并设置应用引用
trayService := serviceManager.GetTrayService() trayService := serviceManager.GetTrayService()
@@ -96,7 +105,7 @@ func main() {
// 初始化热键服务 // 初始化热键服务
hotkeyService := serviceManager.GetHotkeyService() hotkeyService := serviceManager.GetHotkeyService()
err := hotkeyService.Initialize(app) err := hotkeyService.Initialize(app, mainWindow)
if err != nil { if err != nil {
panic(err) panic(err)
} }

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