19 Commits

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-28 19:44:41 +00:00
bc01fdf362 Added window snapping feature 2025-08-24 16:07:48 +08:00
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
116 changed files with 15268 additions and 1766 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,58 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as color$0 from "../../../../../../../image/color/models.js";
export class Options {
"TextColour": color$0.RGBA;
"BackgroundColour": color$0.RGBA;
"FontName": string;
"FontSize": number;
"SmallFontSize": number;
/** Creates a new Options instance. */
constructor($$source: Partial<Options> = {}) {
if (!("TextColour" in $$source)) {
this["TextColour"] = (new color$0.RGBA());
}
if (!("BackgroundColour" in $$source)) {
this["BackgroundColour"] = (new color$0.RGBA());
}
if (!("FontName" in $$source)) {
this["FontName"] = "";
}
if (!("FontSize" in $$source)) {
this["FontSize"] = 0;
}
if (!("SmallFontSize" in $$source)) {
this["SmallFontSize"] = 0;
}
Object.assign(this, $$source);
}
/**
* Creates a new Options instance from a string or object.
*/
static createFrom($$source: any = {}): Options {
const $$createField0_0 = $$createType0;
const $$createField1_0 = $$createType0;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("TextColour" in $$parsedSource) {
$$parsedSource["TextColour"] = $$createField0_0($$parsedSource["TextColour"]);
}
if ("BackgroundColour" in $$parsedSource) {
$$parsedSource["BackgroundColour"] = $$createField1_0($$parsedSource["BackgroundColour"]);
}
return new Options($$parsedSource as Partial<Options>);
}
}
// Private type creation functions
const $$createType0 = color$0.RGBA.createFrom;

View File

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

View File

@@ -0,0 +1,107 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Create as $Create} from "@wailsio/runtime";
/**
* NotificationAction represents an action button for a notification.
*/
export class NotificationAction {
"id"?: string;
"title"?: string;
/**
* (macOS-specific)
*/
"destructive"?: boolean;
/** Creates a new NotificationAction instance. */
constructor($$source: Partial<NotificationAction> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new NotificationAction instance from a string or object.
*/
static createFrom($$source: any = {}): NotificationAction {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new NotificationAction($$parsedSource as Partial<NotificationAction>);
}
}
/**
* NotificationCategory groups actions for notifications.
*/
export class NotificationCategory {
"id"?: string;
"actions"?: NotificationAction[];
"hasReplyField"?: boolean;
"replyPlaceholder"?: string;
"replyButtonTitle"?: string;
/** Creates a new NotificationCategory instance. */
constructor($$source: Partial<NotificationCategory> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new NotificationCategory instance from a string or object.
*/
static createFrom($$source: any = {}): NotificationCategory {
const $$createField1_0 = $$createType1;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("actions" in $$parsedSource) {
$$parsedSource["actions"] = $$createField1_0($$parsedSource["actions"]);
}
return new NotificationCategory($$parsedSource as Partial<NotificationCategory>);
}
}
/**
* NotificationOptions contains configuration for a notification
*/
export class NotificationOptions {
"id": string;
"title": string;
/**
* (macOS and Linux only)
*/
"subtitle"?: string;
"body"?: string;
"categoryId"?: string;
"data"?: { [_: string]: any };
/** Creates a new NotificationOptions instance. */
constructor($$source: Partial<NotificationOptions> = {}) {
if (!("id" in $$source)) {
this["id"] = "";
}
if (!("title" in $$source)) {
this["title"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new NotificationOptions instance from a string or object.
*/
static createFrom($$source: any = {}): NotificationOptions {
const $$createField5_0 = $$createType2;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("data" in $$parsedSource) {
$$parsedSource["data"] = $$createField5_0($$parsedSource["data"]);
}
return new NotificationOptions($$parsedSource as Partial<NotificationOptions>);
}
}
// Private type creation functions
const $$createType0 = NotificationAction.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType2 = $Create.Map($Create.Any, $Create.Any);

View File

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

View File

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

View File

@@ -0,0 +1,46 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Create as $Create} from "@wailsio/runtime";
/**
* RGBA represents a traditional 32-bit alpha-premultiplied color, having 8
* bits for each of red, green, blue and alpha.
*
* An alpha-premultiplied color component C has been scaled by alpha (A), so
* has valid values 0 <= C <= A.
*/
export class RGBA {
"R": number;
"G": number;
"B": number;
"A": number;
/** Creates a new RGBA instance. */
constructor($$source: Partial<RGBA> = {}) {
if (!("R" in $$source)) {
this["R"] = 0;
}
if (!("G" in $$source)) {
this["G"] = 0;
}
if (!("B" in $$source)) {
this["B"] = 0;
}
if (!("A" in $$source)) {
this["A"] = 0;
}
Object.assign(this, $$source);
}
/**
* Creates a new RGBA instance from a string or object.
*/
static createFrom($$source: any = {}): RGBA {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new RGBA($$parsedSource as Partial<RGBA>);
}
}

View File

@@ -114,11 +114,6 @@ export class AppearanceConfig {
*/
"systemTheme": SystemThemeType;
/**
* 自定义主题配置
*/
"customTheme": CustomThemeConfig;
/** Creates a new AppearanceConfig instance. */
constructor($$source: Partial<AppearanceConfig> = {}) {
if (!("language" in $$source)) {
@@ -127,9 +122,6 @@ export class AppearanceConfig {
if (!("systemTheme" in $$source)) {
this["systemTheme"] = ("" as SystemThemeType);
}
if (!("customTheme" in $$source)) {
this["customTheme"] = (new CustomThemeConfig());
}
Object.assign(this, $$source);
}
@@ -138,11 +130,7 @@ export class AppearanceConfig {
* Creates a new AppearanceConfig instance from a string or object.
*/
static createFrom($$source: any = {}): AppearanceConfig {
const $$createField2_0 = $$createType6;
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>);
}
}
@@ -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
*/
@@ -428,7 +373,7 @@ export class Extension {
* Creates a new Extension instance from a string or object.
*/
static createFrom($$source: any = {}): Extension {
const $$createField3_0 = $$createType8;
const $$createField3_0 = $$createType6;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("config" in $$parsedSource) {
$$parsedSource["config"] = $$createField3_0($$parsedSource["config"]);
@@ -522,6 +467,12 @@ export class GeneralConfig {
*/
"startAtLogin": boolean;
/**
* 窗口吸附设置
* 是否启用窗口吸附功能(阈值现在是自适应的)
*/
"enableWindowSnap": boolean;
/**
* 全局热键设置
* 是否启用全局热键
@@ -547,6 +498,9 @@ export class GeneralConfig {
if (!("startAtLogin" in $$source)) {
this["startAtLogin"] = false;
}
if (!("enableWindowSnap" in $$source)) {
this["enableWindowSnap"] = false;
}
if (!("enableGlobalHotkey" in $$source)) {
this["enableGlobalHotkey"] = false;
}
@@ -561,10 +515,10 @@ export class GeneralConfig {
* Creates a new GeneralConfig instance from a string or object.
*/
static createFrom($$source: any = {}): GeneralConfig {
const $$createField5_0 = $$createType10;
const $$createField6_0 = $$createType8;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("globalHotkey" in $$parsedSource) {
$$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]);
$$parsedSource["globalHotkey"] = $$createField6_0($$parsedSource["globalHotkey"]);
}
return new GeneralConfig($$parsedSource as Partial<GeneralConfig>);
}
@@ -1171,6 +1125,58 @@ export enum TabType {
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 主题颜色配置
*/
@@ -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 更新源类型
*/
@@ -1477,8 +1496,8 @@ export class UpdatesConfig {
* Creates a new UpdatesConfig instance from a string or object.
*/
static createFrom($$source: any = {}): UpdatesConfig {
const $$createField6_0 = $$createType11;
const $$createField7_0 = $$createType12;
const $$createField6_0 = $$createType10;
const $$createField7_0 = $$createType11;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("github" in $$parsedSource) {
$$parsedSource["github"] = $$createField6_0($$parsedSource["github"]);
@@ -1497,15 +1516,14 @@ const $$createType2 = AppearanceConfig.createFrom;
const $$createType3 = UpdatesConfig.createFrom;
const $$createType4 = GitBackupConfig.createFrom;
const $$createType5 = ConfigMetadata.createFrom;
const $$createType6 = CustomThemeConfig.createFrom;
const $$createType7 = ThemeColorConfig.createFrom;
var $$createType8 = (function $$initCreateType8(...args): any {
if ($$createType8 === $$initCreateType8) {
$$createType8 = $$createType9;
var $$createType6 = (function $$initCreateType6(...args): any {
if ($$createType6 === $$initCreateType6) {
$$createType6 = $$createType7;
}
return $$createType8(...args);
return $$createType6(...args);
});
const $$createType9 = $Create.Map($Create.Any, $Create.Any);
const $$createType10 = HotkeyCombo.createFrom;
const $$createType11 = GithubConfig.createFrom;
const $$createType12 = GiteaConfig.createFrom;
const $$createType7 = $Create.Map($Create.Any, $Create.Any);
const $$createType8 = HotkeyCombo.createFrom;
const $$createType9 = ThemeColorConfig.createFrom;
const $$createType10 = GithubConfig.createFrom;
const $$createType11 = GiteaConfig.createFrom;

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,55 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* TestService 测试服务 - 仅在开发环境使用
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
/**
* ClearAll 清除所有测试状态
*/
export function ClearAll(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2179720854) as any;
return $resultPromise;
}
/**
* ServiceStartup 服务启动时调用
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(617408198, options) as any;
return $resultPromise;
}
/**
* TestBadge 测试Badge功能
*/
export function TestBadge(text: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(4242952145, text) as any;
return $resultPromise;
}
/**
* TestNotification 测试通知功能
*/
export function TestNotification(title: string, subtitle: string, body: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1697553289, title, subtitle, body) as any;
return $resultPromise;
}
/**
* TestUpdateNotification 测试更新通知
*/
export function TestUpdateNotification(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3091730060) as any;
return $resultPromise;
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -12,36 +12,35 @@
"lint:fix": "eslint --fix"
},
"dependencies": {
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/autocomplete": "^6.18.7",
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-angular": "^0.1.4",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-html": "^6.4.10",
"@codemirror/lang-java": "^6.0.2",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-less": "^6.0.2",
"@codemirror/lang-lezer": "^6.0.2",
"@codemirror/lang-liquid": "^6.2.3",
"@codemirror/lang-markdown": "^6.3.3",
"@codemirror/lang-liquid": "^6.3.0",
"@codemirror/lang-markdown": "^6.3.4",
"@codemirror/lang-php": "^6.0.2",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-rust": "^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-wast": "^6.0.2",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.11.2",
"@codemirror/language": "^6.11.3",
"@codemirror/language-data": "^6.5.1",
"@codemirror/legacy-modes": "^6.5.1",
"@codemirror/lint": "^6.8.5",
"@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.1",
"@codemirror/view": "^6.38.2",
"@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.2",
"codemirror": "^6.0.2",
@@ -50,32 +49,38 @@
"colors-named-hex": "^1.0.2",
"franc-min": "^6.2.0",
"hsl-matcher": "^1.2.4",
"jsox": "^1.2.123",
"lezer": "^0.13.5",
"linguist-languages": "^9.0.0",
"node-sql-parser": "^5.3.12",
"php-parser": "^3.2.5",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.4.1",
"pinia-plugin-persistedstate": "^4.5.0",
"prettier": "^3.6.2",
"remarkable": "^2.0.1",
"sass": "^1.89.2",
"vue": "^3.5.17",
"vue-i18n": "^11.1.10",
"sass": "^1.92.1",
"sql-formatter": "^15.6.9",
"vue": "^3.5.21",
"vue-i18n": "^11.1.12",
"vue-pick-colors": "^1.8.0",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@eslint/js": "^9.31.0",
"@eslint/js": "^9.35.0",
"@lezer/generator": "^1.8.0",
"@types/node": "^24.0.14",
"@types/node": "^24.3.1",
"@types/remarkable": "^2.0.8",
"@vitejs/plugin-vue": "^6.0.0",
"@vitejs/plugin-vue": "^6.0.1",
"@wailsio/runtime": "latest",
"eslint": "^9.31.0",
"eslint-plugin-vue": "^10.3.0",
"globals": "^16.3.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.37.0",
"unplugin-vue-components": "^28.8.0",
"vite": "^7.0.4",
"eslint": "^9.35.0",
"eslint-plugin-vue": "^10.4.0",
"globals": "^16.4.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.43.0",
"unplugin-vue-components": "^29.0.0",
"vite": "^7.1.5",
"vite-plugin-node-polyfills": "^0.24.0",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.0.1"
"vue-tsc": "^3.0.6"
}
}

BIN
frontend/public/go.wasm Normal file

Binary file not shown.

View File

@@ -0,0 +1,561 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
"use strict";
(() => {
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!globalThis.fs) {
let outputBuf = "";
globalThis.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substring(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!globalThis.process) {
globalThis.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!globalThis.crypto) {
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
}
if (!globalThis.performance) {
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
}
if (!globalThis.TextEncoder) {
throw new Error("globalThis.TextEncoder is not available, polyfill required");
}
if (!globalThis.TextDecoder) {
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
globalThis.Go = class {
constructor() {
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}
const setInt32 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = this.mem.getUint32(addr, true);
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
}
this.mem.setFloat64(addr, v, true);
return;
}
if (v === undefined) {
this.mem.setFloat64(addr, 0, true);
return;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 0;
switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string":
typeFlag = 2;
break;
case "symbol":
typeFlag = 3;
break;
case "function":
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
_gotest: {
add: (a, b) => a + b,
},
gojs: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 32, result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
setInt64(sp + 24, str.length);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
"debug": (value) => {
console.log(value);
},
}
};
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
globalThis,
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
}
return ptr;
};
const argc = this.argv.length;
const argvPtrs = [];
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);
const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);
const argv = offset;
argvPtrs.forEach((ptr) => {
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
});
// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error("total length of command line and environment variables exceeds limit");
}
this._inst.exports.run(argc, argv);
if (this.exited) {
this._resolveExitPromise();
}
await this._exitPromise;
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
})();

View File

@@ -2,26 +2,26 @@
<div class="linux-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
<div class="titlebar-icon">
<img src="/appicon.png" alt="voidraft" />
<img src="/appicon.png" alt="voidraft"/>
</div>
<div class="titlebar-title">{{ titleText }}</div>
</div>
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
<button
class="titlebar-button minimize-button"
@click="minimizeWindow"
:title="t('titlebar.minimize')"
<button
class="titlebar-button minimize-button"
@click="minimizeWindow"
:title="t('titlebar.minimize')"
>
<svg width="16" height="16" viewBox="0 0 16 16">
<path d="M4 8h8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<button
class="titlebar-button maximize-button"
@click="toggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
<button
class="titlebar-button maximize-button"
@click="toggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
>
<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"/>
@@ -31,11 +31,11 @@
<rect x="7" y="3" width="6" height="6" fill="none" stroke="currentColor" stroke-width="1.5"/>
</svg>
</button>
<button
class="titlebar-button close-button"
@click="closeWindow"
:title="t('titlebar.close')"
<button
class="titlebar-button close-button"
@click="closeWindow"
:title="t('titlebar.close')"
>
<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"/>
@@ -46,85 +46,56 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import {computed, onMounted, ref} from 'vue';
import {useI18n} from 'vue-i18n';
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 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 currentDoc = documentStore.currentDocument;
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 () => {
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>
@@ -139,7 +110,7 @@ onUnmounted(() => {
width: 100%;
font-family: 'Ubuntu', 'Cantarell', 'DejaVu Sans', system-ui, sans-serif;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
@@ -155,7 +126,7 @@ onUnmounted(() => {
font-size: 13px;
font-weight: 500;
cursor: default;
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
@@ -164,7 +135,7 @@ onUnmounted(() => {
.titlebar-content .titlebar-icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
@@ -181,7 +152,7 @@ onUnmounted(() => {
.titlebar-controls {
display: flex;
height: 100%;
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
@@ -201,22 +172,22 @@ onUnmounted(() => {
padding: 0;
margin: 0;
border-radius: 0;
svg {
width: 16px;
height: 16px;
opacity: 0.8;
transition: opacity 0.15s ease;
}
&:hover {
background: var(--toolbar-button-hover, rgba(0, 0, 0, 0.1));
svg {
opacity: 1;
}
}
&:active {
background: var(--toolbar-button-active, rgba(0, 0, 0, 0.15));
}
@@ -226,12 +197,12 @@ onUnmounted(() => {
&:hover {
background: #e74c3c;
color: #ffffff;
svg {
opacity: 1;
}
}
&:active {
background: #c0392b;
}
@@ -244,19 +215,19 @@ onUnmounted(() => {
border-bottom-color: var(--toolbar-border, #1e1e1e);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.titlebar-content,
.titlebar-title {
color: var(--toolbar-text, #f0f0f0);
}
.titlebar-button {
color: var(--toolbar-text, #ccc);
&:hover {
background: var(--toolbar-button-hover, rgba(255, 255, 255, 0.1));
}
&:active {
background: var(--toolbar-button-active, rgba(255, 255, 255, 0.15));
}
@@ -267,13 +238,13 @@ onUnmounted(() => {
.linux-titlebar.gnome-style {
height: 38px;
border-radius: 12px 12px 0 0;
.titlebar-button {
height: 38px;
width: 32px;
border-radius: 6px;
margin: 3px 2px;
&:hover {
background: rgba(255, 255, 255, 0.1);
}
@@ -284,11 +255,11 @@ onUnmounted(() => {
.linux-titlebar.kde-style {
background: var(--toolbar-bg, #eff0f1);
border-bottom: 1px solid var(--toolbar-border, #bdc3c7);
.titlebar-button {
border-radius: 4px;
margin: 2px 1px;
&:hover {
background: rgba(61, 174, 233, 0.2);
}

View File

@@ -53,7 +53,6 @@
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import * as runtime from '@wailsio/runtime';
import { useWindowStore } from '@/stores/windowStore';
import { useDocumentStore } from '@/stores/documentStore';
const { t } = useI18n();
@@ -65,26 +64,16 @@ const minimizeWindow = async () => {
try {
await runtime.Window.Minimise();
} catch (error) {
// Error handling
console.error(error)
}
};
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);
await runtime.Window.ToggleMaximise();
await checkMaximizedState();
} catch (error) {
isMaximized.value = !isMaximized.value;
console.error(error)
}
};
@@ -92,7 +81,7 @@ const closeWindow = async () => {
try {
await runtime.Window.Close();
} catch (error) {
// Error handling
console.error(error)
}
};
@@ -100,7 +89,7 @@ const checkMaximizedState = async () => {
try {
isMaximized.value = await runtime.Window.IsMaximised();
} catch (error) {
// Error handling
console.error(error)
}
};
@@ -112,24 +101,6 @@ const titleText = computed(() => {
onMounted(async () => {
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>

View File

@@ -1,5 +1,5 @@
<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-icon">
<img src="/appicon.png" alt="voidraft"/>
@@ -36,11 +36,10 @@
</template>
<script setup lang="ts">
import {computed, onMounted, onUnmounted, ref} from 'vue';
import {computed, onMounted, ref} from 'vue';
import {useI18n} from 'vue-i18n';
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 isMaximized = ref(false);
@@ -59,30 +58,16 @@ const minimizeWindow = async () => {
try {
await runtime.Window.Minimise();
} catch (error) {
// Error handling
console.error(error);
}
};
const toggleMaximize = async () => {
try {
// 立即更新UI状态提供即时反馈
const newState = !isMaximized.value;
isMaximized.value = newState;
// 然后执行实际操作
if (newState) {
await runtime.Window.Maximise();
} else {
await runtime.Window.UnMaximise();
}
// 操作完成后再次确认状态(防止操作失败时状态不一致)
setTimeout(async () => {
await checkMaximizedState();
}, 100);
await runtime.Window.ToggleMaximise();
await checkMaximizedState();
} catch (error) {
// 如果操作失败,恢复原状态
isMaximized.value = !isMaximized.value;
console.error(error);
}
};
@@ -90,7 +75,7 @@ const closeWindow = async () => {
try {
await runtime.Window.Close();
} catch (error) {
// Error handling
console.error(error);
}
};
@@ -98,31 +83,12 @@ const checkMaximizedState = async () => {
try {
isMaximized.value = await runtime.Window.IsMaximised();
} catch (error) {
// Error handling
console.error(error);
}
};
onMounted(async () => {
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,9 +16,7 @@ import {
} from '@/../bindings/voidraft/internal/models/models';
import {useI18n} from 'vue-i18n';
import {ConfigUtils} from '@/utils/configUtils';
import {WindowController} from '@/utils/windowController';
import * as runtime from '@wailsio/runtime';
import {useBackupStore} from '@/stores/backupStore';
// 国际化相关导入
export type SupportedLocaleType = 'zh-CN' | 'en-US';
@@ -64,7 +62,8 @@ const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
enableSystemTray: 'general.enableSystemTray',
startAtLogin: 'general.startAtLogin',
enableGlobalHotkey: 'general.enableGlobalHotkey',
globalHotkey: 'general.globalHotkey'
globalHotkey: 'general.globalHotkey',
enableWindowSnap: 'general.enableWindowSnap',
} as const;
const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
@@ -80,8 +79,7 @@ const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
language: 'appearance.language',
systemTheme: 'appearance.systemTheme',
customTheme: 'appearance.customTheme'
systemTheme: 'appearance.systemTheme'
} as const;
const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
@@ -179,7 +177,8 @@ const DEFAULT_CONFIG: AppConfig = {
alt: true,
win: false,
key: 'X'
}
},
enableWindowSnap: true,
},
editing: {
fontSize: CONFIG_LIMITS.fontSize.default,
@@ -191,80 +190,10 @@ const DEFAULT_CONFIG: AppConfig = {
tabType: CONFIG_LIMITS.tabType.default,
autoSaveDelay: 5000
},
appearance: {
language: LanguageType.LangZhCN,
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)'
}
}
},
appearance: {
language: LanguageType.LangZhCN,
systemTheme: SystemThemeType.SystemThemeAuto
},
updates: {
version: "1.0.0",
autoUpdate: true,
@@ -416,9 +345,6 @@ export const useConfigStore = defineStore('config', () => {
state.configLoaded = true;
// 初始化热键监听器
const windowController = WindowController.getInstance();
await windowController.initializeHotkeyListener();
} finally {
state.isLoading = false;
}
@@ -483,50 +409,7 @@ export const useConfigStore = defineStore('config', () => {
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> => {
@@ -591,8 +474,6 @@ export const useConfigStore = defineStore('config', () => {
// 主题相关方法
setSystemTheme,
updateCustomTheme,
setCustomTheme,
// 字体大小操作
...adjusters.fontSize,
@@ -641,6 +522,9 @@ export const useConfigStore = defineStore('config', () => {
// 再调用系统设置API
await StartupService.SetEnabled(value);
},
// 窗口吸附配置相关方法
setEnableWindowSnap: async (value: boolean) => await updateGeneralConfig('enableWindowSnap', value),
// 更新配置相关方法
setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value),

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import {defineStore} from 'pinia';
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 {useEditorStore} from './editorStore';
import {defaultDarkColors} from '@/views/editor/theme/dark';
@@ -24,16 +25,21 @@ export const useThemeStore = defineStore('theme', () => {
configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
);
// 初始化主题颜色 - 从配置加载
const initializeThemeColors = () => {
const customTheme = configStore.config?.appearance?.customTheme;
if (customTheme) {
if (customTheme.darkTheme) {
Object.assign(themeColors.darkTheme, customTheme.darkTheme);
// 初始化主题颜色 - 从数据库加载
const initializeThemeColors = async () => {
try {
const themes = await ThemeService.GetDefaultThemes();
if (themes.dark) {
Object.assign(themeColors.darkTheme, themes.dark.colors);
}
if (customTheme.lightTheme) {
Object.assign(themeColors.lightTheme, customTheme.lightTheme);
if (themes.light) {
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;
applyThemeToDOM(theme);
initializeThemeColors();
await initializeThemeColors();
};
// 设置主题
@@ -84,31 +90,35 @@ export const useThemeStore = defineStore('theme', () => {
return hasChanges;
};
// 保存主题颜色到配置
// 保存主题颜色到数据库
const saveThemeColors = async () => {
const customTheme = {
darkTheme: { ...themeColors.darkTheme },
lightTheme: { ...themeColors.lightTheme }
};
await configStore.setCustomTheme(customTheme);
try {
const darkColors = ThemeColorConfig.createFrom(themeColors.darkTheme);
const lightColors = ThemeColorConfig.createFrom(themeColors.lightTheme);
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') => {
try {
// 1. 更新内存中的颜色状态
const dbThemeType = themeType === 'darkTheme' ? ThemeType.ThemeTypeDark : ThemeType.ThemeTypeLight;
// 1. 调用后端重置服务
await ThemeService.ResetThemeColors(dbThemeType);
// 2. 更新内存中的颜色状态
if (themeType === 'darkTheme') {
Object.assign(themeColors.darkTheme, defaultDarkColors);
}
if (themeType === 'lightTheme') {
} else {
Object.assign(themeColors.lightTheme, defaultLightColors);
}
// 2. 保存到配置
await saveThemeColors();
// 3. 刷新编辑器主题
refreshEditorTheme();

View File

@@ -0,0 +1,32 @@
@echo off
rem Build script for Go Prettier Plugin WASM
rem This script compiles the Go code to WebAssembly
echo 🔨 Building Go Prettier Plugin WASM...
rem Set WASM build environment
set GOOS=js
set GOARCH=wasm
rem Build the WASM file
echo Compiling main.go to go.wasm...
go build -o go.wasm main.go
if %ERRORLEVEL% EQU 0 (
echo ✅ Build successful!
rem Show file size (Windows version)
for %%A in (go.wasm) do echo 📊 WASM file size: %%~zA bytes
rem Copy to public directory for browser access
if exist "..\..\..\..\..\public" (
copy go.wasm ..\..\..\..\..\public\go.wasm > nul
echo 📋 Copied to public directory
)
echo 🎉 Go Prettier Plugin WASM is ready!
) else (
echo ❌ Build failed!
pause
exit /b 1
)

View File

@@ -0,0 +1,30 @@
#!/bin/bash
# Build script for Go Prettier Plugin WASM
# This script compiles the Go code to WebAssembly
echo "🔨 Building Go Prettier Plugin WASM..."
# Set WASM build environment
export GOOS=js
export GOARCH=wasm
# Build the WASM file
echo "Compiling main.go to go.wasm..."
go build -o go.wasm main.go
if [ $? -eq 0 ]; then
echo "✅ Build successful!"
echo "📊 WASM file size: $(du -h go.wasm | cut -f1)"
# Copy to public directory for browser access
if [ -d "../../../../../public" ]; then
cp go.wasm ../../../../../public/go.wasm
echo "📋 Copied to public directory"
fi
echo "🎉 Go Prettier Plugin WASM is ready!"
else
echo "❌ Build failed!"
exit 1
fi

View File

@@ -0,0 +1,10 @@
import { Parser, Plugin } from "prettier";
export declare const languages: Plugin["languages"];
export declare const parsers: {
go: Parser;
};
export declare const printers: Plugin["printers"];
declare const plugin: Plugin;
export default plugin;

View File

@@ -0,0 +1,116 @@
/**
* Go Prettier Plugin for Vite + Vue3 Environment
* WebAssembly-based Go code formatter for Prettier
*/
let initializePromise = null;
// Load WASM file from public directory
const loadWasm = async () => {
try {
const response = await fetch('/go.wasm');
if (!response.ok) {
throw new Error(`Failed to load WASM file: ${response.status} ${response.statusText}`);
}
return await response.arrayBuffer();
} catch (error) {
console.error('WASM loading failed:', error);
throw error;
}
};
// Initialize Go runtime
const initGoRuntime = async () => {
if (globalThis.Go) return;
// Auto-load wasm_exec.js if not available
try {
const script = document.createElement('script');
script.src = '/wasm_exec.js';
document.head.appendChild(script);
await new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = () => reject(new Error('Failed to load wasm_exec.js'));
setTimeout(() => reject(new Error('wasm_exec.js loading timeout')), 5000);
});
if (!globalThis.Go) {
throw new Error('Go WASM runtime is not available after loading wasm_exec.js');
}
} catch (error) {
console.error('Failed to load wasm_exec.js:', error);
throw error;
}
};
const initialize = async () => {
if (initializePromise) return initializePromise;
initializePromise = (async () => {
await initGoRuntime();
const go = new globalThis.Go();
const wasmBuffer = await loadWasm();
const {instance} = await WebAssembly.instantiate(wasmBuffer, go.importObject);
// Run Go program
go.run(instance).catch(err => {
console.error('Go WASM program exit error:', err);
});
// Wait for initialization to complete
await new Promise(resolve => setTimeout(resolve, 200));
// Check if formatGo function is available
if (typeof globalThis.formatGo !== 'function') {
throw new Error('Go WASM module not properly initialized - formatGo function not available');
}
})();
return initializePromise;
};
export const languages = [
{
name: "Go",
parsers: ["go"],
extensions: [".go"],
vscodeLanguageIds: ["go"],
},
];
export const parsers = {
go: {
parse: (text) => text,
astFormat: "go-format",
locStart: (node) => 0,
locEnd: (node) => node.length,
},
};
export const printers = {
"go-format": {
print: async (path) => {
await initialize();
const text = path.getValue();
if (typeof globalThis.formatGo !== 'function') {
throw new Error('Go WASM module not properly initialized - formatGo function missing');
}
try {
return globalThis.formatGo(text);
} catch (error) {
throw new Error(`Go formatting failed: ${error.message}`);
}
},
},
};
// Default export for Prettier plugin compatibility
export default {
languages,
parsers,
printers
};

View File

@@ -0,0 +1,255 @@
//go:build js && wasm
// Package main implements a WebAssembly module that provides Go code formatting
// functionality for the Prettier plugin. This package exposes the formatGo function
// to JavaScript, enabling web-based Go code formatting with better error tolerance.
package main
import (
"bytes"
"fmt"
"go/format"
"go/parser"
"go/token"
"strings"
"syscall/js"
)
// formatGoCode attempts to format Go source code with better error tolerance
func formatGoCode(src string) (string, error) {
// Trim input but preserve leading/trailing newlines structure
trimmed := strings.TrimSpace(src)
if trimmed == "" {
return src, nil
}
// First try the standard format.Source for complete, valid code
if formatted, err := format.Source([]byte(src)); err == nil {
return string(formatted), nil
}
// Create a new file set for parsing
fset := token.NewFileSet()
// Strategy 1: Try as complete Go file
if parsed, err := parser.ParseFile(fset, "", src, parser.ParseComments); err == nil {
return formatASTNode(fset, parsed)
}
// Strategy 2: Try wrapping as package-level declarations
packageWrapped := fmt.Sprintf("package main\n\n%s", trimmed)
if parsed, err := parser.ParseFile(fset, "", packageWrapped, parser.ParseComments); err == nil {
if formatted, err := formatASTNode(fset, parsed); err == nil {
return extractPackageContent(formatted), nil
}
}
// Strategy 3: Try wrapping in main function
funcWrapped := fmt.Sprintf("package main\n\nfunc main() {\n%s\n}", indentLines(trimmed, "\t"))
if parsed, err := parser.ParseFile(fset, "", funcWrapped, parser.ParseComments); err == nil {
if formatted, err := formatASTNode(fset, parsed); err == nil {
return extractFunctionBody(formatted), nil
}
}
// Strategy 4: Try wrapping in anonymous function
anonWrapped := fmt.Sprintf("package main\n\nvar _ = func() {\n%s\n}", indentLines(trimmed, "\t"))
if parsed, err := parser.ParseFile(fset, "", anonWrapped, parser.ParseComments); err == nil {
if formatted, err := formatASTNode(fset, parsed); err == nil {
return extractFunctionBody(formatted), nil
}
}
// Strategy 5: Try line-by-line formatting for complex cases
return formatLineByLine(trimmed, fset)
}
// formatASTNode formats an AST node using the standard formatter
func formatASTNode(fset *token.FileSet, node interface{}) (string, error) {
var buf bytes.Buffer
if err := format.Node(&buf, fset, node); err != nil {
return "", err
}
return buf.String(), nil
}
// extractPackageContent extracts content after package declaration
func extractPackageContent(formatted string) string {
lines := strings.Split(formatted, "\n")
var contentLines []string
skipNext := false
for _, line := range lines {
if strings.HasPrefix(line, "package ") {
skipNext = true
continue
}
if skipNext && strings.TrimSpace(line) == "" {
skipNext = false
continue
}
if !skipNext {
contentLines = append(contentLines, line)
}
}
return strings.Join(contentLines, "\n")
}
// extractFunctionBody extracts content from within a function body
func extractFunctionBody(formatted string) string {
lines := strings.Split(formatted, "\n")
var bodyLines []string
inFunction := false
braceCount := 0
for _, line := range lines {
if strings.Contains(line, "func ") && strings.Contains(line, "{") {
inFunction = true
braceCount = 1
continue
}
if inFunction {
// Count braces to know when function ends
braceCount += strings.Count(line, "{")
braceCount -= strings.Count(line, "}")
if braceCount == 0 {
break
}
// Remove one level of indentation and add the line
if strings.HasPrefix(line, "\t") {
bodyLines = append(bodyLines, line[1:])
} else {
bodyLines = append(bodyLines, line)
}
}
}
// Remove empty lines from start and end
for len(bodyLines) > 0 && strings.TrimSpace(bodyLines[0]) == "" {
bodyLines = bodyLines[1:]
}
for len(bodyLines) > 0 && strings.TrimSpace(bodyLines[len(bodyLines)-1]) == "" {
bodyLines = bodyLines[:len(bodyLines)-1]
}
return strings.Join(bodyLines, "\n")
}
// indentLines adds indentation to each non-empty line
func indentLines(text, indent string) string {
lines := strings.Split(text, "\n")
var indentedLines []string
for _, line := range lines {
if strings.TrimSpace(line) == "" {
indentedLines = append(indentedLines, "")
} else {
indentedLines = append(indentedLines, indent+line)
}
}
return strings.Join(indentedLines, "\n")
}
// formatLineByLine attempts to format each statement individually
func formatLineByLine(src string, fset *token.FileSet) (string, error) {
lines := strings.Split(src, "\n")
var formattedLines []string
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
if trimmedLine == "" {
formattedLines = append(formattedLines, "")
continue
}
// Try different wrapping strategies for individual lines
attempts := []string{
fmt.Sprintf("package main\n\nfunc main() {\n\t%s\n}", trimmedLine),
fmt.Sprintf("package main\n\n%s", trimmedLine),
fmt.Sprintf("package main\n\nvar _ = %s", trimmedLine),
}
formatted := trimmedLine // fallback
for _, attempt := range attempts {
if parsed, err := parser.ParseFile(fset, "", attempt, parser.ParseComments); err == nil {
if result, err := formatASTNode(fset, parsed); err == nil {
if extracted := extractSingleStatement(result, trimmedLine); extracted != "" {
formatted = extracted
break
}
}
}
}
formattedLines = append(formattedLines, formatted)
}
return strings.Join(formattedLines, "\n"), nil
}
// extractSingleStatement tries to extract a single formatted statement
func extractSingleStatement(formatted, original string) string {
lines := strings.Split(formatted, "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed != "" && !strings.HasPrefix(trimmed, "package ") &&
!strings.HasPrefix(trimmed, "func ") && !strings.HasPrefix(trimmed, "var _ =") &&
trimmed != "{" && trimmed != "}" {
// Remove leading tabs/spaces to normalize indentation
return strings.TrimLeft(line, " \t")
}
}
return original
}
// formatGo is a JavaScript-callable function that formats Go source code.
// It attempts multiple strategies to format code, including handling incomplete
// or syntactically invalid code fragments.
//
// Parameters:
// - this: The JavaScript 'this' context (unused)
// - i: JavaScript arguments array where i[0] should contain the Go source code as a string
//
// Returns:
// - js.Value: The formatted Go source code as a JavaScript string value
// - If formatting fails completely, returns the original code unchanged
// - If no arguments are provided, returns js.Null() and logs an error
func formatGo(this js.Value, i []js.Value) interface{} {
if len(i) == 0 {
js.Global().Get("console").Call("error", "formatGo: missing code argument")
return js.Null()
}
code := i[0].String()
if strings.TrimSpace(code) == "" {
return js.ValueOf(code)
}
formatted, err := formatGoCode(code)
if err != nil {
js.Global().Get("console").Call("warn", "Go formatting had issues:", err.Error())
return js.ValueOf(code) // Return original code if all attempts fail
}
return js.ValueOf(formatted)
}
// main initializes the WebAssembly module and exposes the formatGo function
// to the JavaScript global scope.
func main() {
// Create a channel to keep the Go program running
c := make(chan struct{}, 0)
// Expose the formatGo function to the JavaScript global scope
js.Global().Set("formatGo", js.FuncOf(formatGo))
// Block forever
<-c
}

View File

@@ -0,0 +1,4 @@
import type { Plugin } from "prettier";
declare const plugin: Plugin;
export default plugin;

View File

@@ -0,0 +1,19 @@
export {
languages,
printers,
parsers,
options,
defaultOptions
} from './src/index.mjs';
import { languages, printers, parsers, options, defaultOptions } from './src/index.mjs';
const phpPlugin = {
languages,
printers,
parsers,
options,
defaultOptions
};
export default phpPlugin;

View File

@@ -0,0 +1,111 @@
import { printNumber, normalizeMagicMethodName } from "./util.mjs";
const ignoredProperties = new Set([
"loc",
"range",
"raw",
"comments",
"leadingComments",
"trailingComments",
"parenthesizedExpression",
"parent",
"prev",
"start",
"end",
"tokens",
"errors",
"extra",
]);
/**
* This function takes the existing ast node and a copy, by reference
* We use it for testing, so that we can compare pre-post versions of the AST,
* excluding things we don't care about (like node location, case that will be
* changed by the printer, etc.)
*/
function clean(node, newObj) {
if (node.kind === "string") {
// TODO if options are available in this method, replace with
// newObj.isDoubleQuote = !useSingleQuote(node, options);
delete newObj.isDoubleQuote;
}
if (["array", "list"].includes(node.kind)) {
// TODO if options are available in this method, assign instead of delete
delete newObj.shortForm;
}
if (node.kind === "inline") {
if (node.value.includes("___PSEUDO_INLINE_PLACEHOLDER___")) {
return null;
}
newObj.value = newObj.value.replace(/\n/g, "");
}
// continue ((2)); -> continue 2;
// continue 1; -> continue;
if ((node.kind === "continue" || node.kind === "break") && node.level) {
const { level } = newObj;
if (level.kind === "number") {
newObj.level = level.value === "1" ? null : level;
}
}
// if () {{ }} -> if () {}
if (node.kind === "block") {
if (node.children.length === 1 && node.children[0].kind === "block") {
while (newObj.children[0].kind === "block") {
newObj.children = newObj.children[0].children;
}
}
}
// Normalize numbers
if (node.kind === "number") {
newObj.value = printNumber(node.value);
}
const statements = ["foreach", "for", "if", "while", "do"];
if (statements.includes(node.kind)) {
if (node.body && node.body.kind !== "block") {
newObj.body = {
kind: "block",
children: [newObj.body],
};
} else {
newObj.body = newObj.body ? newObj.body : null;
}
if (node.alternate && node.alternate.kind !== "block") {
newObj.alternate = {
kind: "block",
children: [newObj.alternate],
};
} else {
newObj.alternate = newObj.alternate ? newObj.alternate : null;
}
}
if (node.kind === "usegroup" && typeof node.name === "string") {
newObj.name = newObj.name.replace(/^\\/, "");
}
if (node.kind === "useitem") {
newObj.name = newObj.name.replace(/^\\/, "");
}
if (node.kind === "method" && node.name.kind === "identifier") {
newObj.name.name = normalizeMagicMethodName(newObj.name.name);
}
if (node.kind === "noop") {
return null;
}
}
clean.ignoredProperties = ignoredProperties;
export default clean;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,173 @@
import { doc } from "prettier";
import LINGUIST_LANGUAGES_PHP from "linguist-languages/data/PHP";
import LINGUIST_LANGUAGES_HTML_PHP from "linguist-languages/data/HTML_2b_PHP";
import parse from "./parser.mjs";
import print from "./printer.mjs";
import clean from "./clean.mjs";
import options from "./options.mjs";
import {
handleOwnLineComment,
handleEndOfLineComment,
handleRemainingComment,
getCommentChildNodes,
canAttachComment,
isBlockComment,
} from "./comments.mjs";
import { hasPragma, insertPragma } from "./pragma.mjs";
import { locStart, locEnd } from "./loc.mjs";
const { join, hardline } = doc.builders;
function createLanguage(linguistData, { extend, override }) {
const language = {};
for (const key in linguistData) {
const newKey = key === "languageId" ? "linguistLanguageId" : key;
language[newKey] = linguistData[key];
}
if (extend) {
for (const key in extend) {
language[key] = (language[key] || []).concat(extend[key]);
}
}
for (const key in override) {
language[key] = override[key];
}
return language;
}
const languages = [
createLanguage(LINGUIST_LANGUAGES_PHP, {
override: {
parsers: ["php"],
vscodeLanguageIds: ["php"],
},
}),
createLanguage(LINGUIST_LANGUAGES_HTML_PHP, {
override: {
parsers: ["php"],
vscodeLanguageIds: ["php"],
},
}),
];
const parsers = {
php: {
parse,
astFormat: "php",
locStart,
locEnd,
hasPragma,
},
};
const ignoredKeys = new Set([
"kind",
"loc",
"errors",
"extra",
"comments",
"leadingComments",
"enclosingNode",
"precedingNode",
"followingNode",
]);
function getVisitorKeys(node, nonTraversableKeys) {
return Object.keys(node).filter(
(key) => !nonTraversableKeys.has(key) && !ignoredKeys.has(key)
);
}
const printers = {
php: {
print,
getVisitorKeys,
insertPragma,
massageAstNode: clean,
getCommentChildNodes,
canAttachComment,
isBlockComment,
handleComments: {
ownLine: handleOwnLineComment,
endOfLine: handleEndOfLineComment,
remaining: handleRemainingComment,
},
willPrintOwnComments(path) {
const { node } = path;
return node && node.kind === "noop";
},
printComment(path) {
const comment = path.node;
switch (comment.kind) {
case "commentblock": {
// for now, don't touch single line block comments
if (!comment.value.includes("\n")) {
return comment.value;
}
const lines = comment.value.split("\n");
// if this is a block comment, handle indentation
if (
lines
.slice(1, lines.length - 1)
.every((line) => line.trim()[0] === "*")
) {
return join(
hardline,
lines.map(
(line, index) =>
(index > 0 ? " " : "") +
(index < lines.length - 1 ? line.trim() : line.trimLeft())
)
);
}
// otherwise we can't be sure about indentation, so just print as is
return comment.value;
}
case "commentline": {
return comment.value.trimRight();
}
/* c8 ignore next 2 */
default:
throw new Error(`Not a comment: ${JSON.stringify(comment)}`);
}
},
hasPrettierIgnore(path) {
const isSimpleIgnore = (comment) =>
comment.value.includes("prettier-ignore") &&
!comment.value.includes("prettier-ignore-start") &&
!comment.value.includes("prettier-ignore-end");
const { node, parent: parentNode } = path;
return (
(node &&
node.kind !== "classconstant" &&
node.comments &&
node.comments.length > 0 &&
node.comments.some(isSimpleIgnore)) ||
// For proper formatting, the classconstant ignore formatting should
// run on the "constant" child
(node &&
node.kind === "constant" &&
parentNode &&
parentNode.kind === "classconstant" &&
parentNode.comments &&
parentNode.comments.length > 0 &&
parentNode.comments.some(isSimpleIgnore))
);
},
},
};
const defaultOptions = {
tabWidth: 4,
};
export { languages, printers, parsers, options, defaultOptions };

View File

@@ -0,0 +1,4 @@
const loc = (prop) => (node) => node.loc?.[prop]?.offset;
export const locStart = loc("start");
export const locEnd = loc("end");

View File

@@ -0,0 +1,250 @@
import { getPrecedence, shouldFlatten, isBitwiseOperator } from "./util.mjs";
function needsParens(path, options) {
const { parent } = path;
if (!parent) {
return false;
}
const { key, node } = path;
if (
[
// No need parens for top level children of this nodes
"program",
"expressionstatement",
"namespace",
"declare",
"block",
// No need parens
"include",
"print",
"return",
"echo",
].includes(parent.kind)
) {
return false;
}
switch (node.kind) {
case "pre":
case "post":
if (parent.kind === "unary") {
return (
node.kind === "pre" &&
((node.type === "+" && parent.type === "+") ||
(node.type === "-" && parent.type === "-"))
);
}
// else fallthrough
case "unary":
switch (parent.kind) {
case "unary":
return (
node.type === parent.type &&
(node.type === "+" || node.type === "-")
);
case "propertylookup":
case "nullsafepropertylookup":
case "staticlookup":
case "offsetlookup":
case "call":
return key === "what";
case "bin":
return parent.type === "**" && key === "left";
default:
return false;
}
case "bin": {
switch (parent.kind) {
case "assign":
case "retif":
return ["and", "xor", "or"].includes(node.type);
case "silent":
case "cast":
// TODO: bug https://github.com/glayzzle/php-parser/issues/172
return node.parenthesizedExpression;
case "pre":
case "post":
case "unary":
return true;
case "call":
case "propertylookup":
case "nullsafepropertylookup":
case "staticlookup":
case "offsetlookup":
return key === "what";
case "bin": {
const po = parent.type;
const pp = getPrecedence(po);
const no = node.type;
const np = getPrecedence(no);
if (pp > np) {
return true;
}
if (po === "||" && no === "&&") {
return true;
}
if (pp === np && key === "right") {
return true;
}
if (pp === np && !shouldFlatten(po, no)) {
return true;
}
if (pp < np && no === "%") {
return po === "+" || po === "-";
}
// Add parenthesis when working with bitwise operators
// It's not stricly needed but helps with code understanding
if (isBitwiseOperator(po)) {
return true;
}
return false;
}
default:
return false;
}
}
case "propertylookup":
case "nullsafepropertylookup":
case "staticlookup": {
switch (parent.kind) {
case "call":
return key === "what" && node.parenthesizedExpression;
default:
return false;
}
}
case "clone":
case "new": {
const requiresParens =
node.kind === "clone" ||
(node.kind === "new" && options.phpVersion < 8.4);
switch (parent.kind) {
case "propertylookup":
case "nullsafepropertylookup":
case "staticlookup":
case "offsetlookup":
case "call":
return key === "what" && requiresParens;
default:
return false;
}
}
case "yield": {
switch (parent.kind) {
case "propertylookup":
case "nullsafepropertylookup":
case "staticlookup":
case "offsetlookup":
case "call":
return key === "what";
case "retif":
return key === "test";
default:
return !!(node.key || node.value);
}
}
case "assign": {
if (
parent.kind === "for" &&
(parent.init.includes(node) || parent.increment.includes(node))
) {
return false;
} else if (parent.kind === "assign") {
return false;
} else if (parent.kind === "static") {
return false;
} else if (
["if", "do", "while", "foreach", "switch"].includes(parent.kind)
) {
return false;
} else if (parent.kind === "silent") {
return false;
} else if (parent.kind === "call") {
return false;
}
return true;
}
case "retif":
switch (parent.kind) {
case "cast":
return true;
case "unary":
case "bin":
case "retif":
if (key === "test" && !parent.trueExpr) {
return false;
}
return true;
case "propertylookup":
case "nullsafepropertylookup":
case "staticlookup":
case "offsetlookup":
case "call":
return key === "what";
default:
return false;
}
case "closure":
switch (parent.kind) {
case "call":
return key === "what";
// https://github.com/prettier/plugin-php/issues/1675
case "propertylookup":
case "nullsafepropertylookup":
return true;
default:
return false;
}
case "silence":
case "cast":
// TODO: bug https://github.com/glayzzle/php-parser/issues/172
return node.parenthesizedExpression;
// else fallthrough
case "string":
case "array":
switch (parent.kind) {
case "propertylookup":
case "nullsafepropertylookup":
case "staticlookup":
case "offsetlookup":
case "call":
if (
["string", "array"].includes(node.kind) &&
parent.kind === "offsetlookup"
) {
return false;
}
return key === "what";
default:
return false;
}
case "print":
case "include":
return parent.kind === "bin";
}
return false;
}
export default needsParens;

View File

@@ -0,0 +1,69 @@
const CATEGORY_PHP = "PHP";
// prettier-ignore
const SUPPORTED_PHP_VERSIONS = [
5.0, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6,
7.0, 7.1, 7.2, 7.3, 7.4,
8.0, 8.1, 8.2, 8.3, 8.4,
];
export const LATEST_SUPPORTED_PHP_VERSION = Math.max(...SUPPORTED_PHP_VERSIONS);
/**
* Resolve the PHP version to a number based on the provided options.
*/
export function resolvePhpVersion(options) {
if (!options) {
return;
}
if (options.phpVersion === "auto" || options.phpVersion === "composer") {
options.phpVersion = LATEST_SUPPORTED_PHP_VERSION;
} else {
options.phpVersion = parseFloat(options.phpVersion);
}
}
export default {
phpVersion: {
since: "0.13.0",
category: CATEGORY_PHP,
type: "choice",
default: "auto",
description: "Minimum target PHP version.",
choices: [
...SUPPORTED_PHP_VERSIONS.map((v) => ({ value: v.toFixed(1) })),
{
value: "auto",
description: `Use latest PHP Version (${LATEST_SUPPORTED_PHP_VERSION})`,
},
],
},
trailingCommaPHP: {
since: "0.0.0",
category: CATEGORY_PHP,
type: "boolean",
default: true,
description: "Print trailing commas wherever possible when multi-line.",
},
braceStyle: {
since: "0.10.0",
category: CATEGORY_PHP,
type: "choice",
default: "per-cs",
description:
"Print one space or newline for code blocks (classes and functions).",
choices: [
{ value: "psr-2", description: "(deprecated) Use per-cs" },
{ value: "per-cs", description: "Use the PER Coding Style brace style." },
{ value: "1tbs", description: "Use 1tbs brace style." },
],
},
singleQuote: {
since: "0.0.0",
category: CATEGORY_PHP,
type: "boolean",
default: false,
description: "Use single quotes instead of double quotes.",
},
};

View File

@@ -0,0 +1,67 @@
import engine from "php-parser";
import { LATEST_SUPPORTED_PHP_VERSION } from "./options.mjs";
import { resolvePhpVersion } from "./options.mjs";
function parse(text, opts) {
const inMarkdown = opts && opts.parentParser === "markdown";
if (!text && inMarkdown) {
return "";
}
resolvePhpVersion(opts);
// Todo https://github.com/glayzzle/php-parser/issues/170
text = text.replace(/\?>\n<\?/g, "?>\n___PSEUDO_INLINE_PLACEHOLDER___<?");
// initialize a new parser instance
const parser = new engine({
parser: {
extractDoc: true,
version: `${LATEST_SUPPORTED_PHP_VERSION}`,
},
ast: {
withPositions: true,
withSource: true,
},
});
const hasOpenPHPTag = text.indexOf("<?php") !== -1;
const parseAsEval = inMarkdown && !hasOpenPHPTag;
let ast;
try {
ast = parseAsEval ? parser.parseEval(text) : parser.parseCode(text);
} catch (err) {
if (err instanceof SyntaxError && "lineNumber" in err) {
err.loc = {
start: {
line: err.lineNumber,
column: err.columnNumber,
},
};
delete err.lineNumber;
delete err.columnNumber;
}
throw err;
}
ast.extra = {
parseAsEval,
};
// https://github.com/glayzzle/php-parser/issues/155
// currently inline comments include the line break at the end, we need to
// strip those out and update the end location for each comment manually
ast.comments.forEach((comment) => {
if (comment.value[comment.value.length - 1] === "\n") {
comment.value = comment.value.slice(0, -1);
comment.loc.end.offset = comment.loc.end.offset - 1;
}
});
return ast;
}
export default parse;

View File

@@ -0,0 +1,92 @@
import { memoize } from "./util.mjs";
import parse from "./parser.mjs";
const reHasPragma = /@prettier|@format/;
const getPageLevelDocBlock = memoize((text) => {
const parsed = parse(text);
const [firstChild] = parsed.children;
const [firstDocBlock] = parsed.comments.filter(
(el) => el.kind === "commentblock"
);
if (
firstChild &&
firstDocBlock &&
firstDocBlock.loc.start.line < firstChild.loc.start.line
) {
return firstDocBlock;
}
});
function hasPragma(text) {
// fast path optimization - check if the pragma shows up in the file at all
if (!reHasPragma.test(text)) {
return false;
}
const pageLevelDocBlock = getPageLevelDocBlock(text);
if (pageLevelDocBlock) {
const { value } = pageLevelDocBlock;
return reHasPragma.test(value);
}
return false;
}
function injectPragma(docblock) {
let lines = docblock.split("\n");
if (lines.length === 1) {
// normalize to multiline for simplicity
const [, line] = /\/*\*\*(.*)\*\//.exec(lines[0]);
lines = ["/**", ` * ${line.trim()}`, " */"];
}
// find the first @pragma
// if there happens to be one on the opening line, just put it on the next line.
const pragmaIndex = lines.findIndex((line) => /@\S/.test(line)) || 1;
// not found => index == -1, which conveniently will splice 1 from the end.
lines.splice(pragmaIndex, 0, " * @format");
return lines.join("\n");
}
function insertPragma(text) {
const pageLevelDocBlock = getPageLevelDocBlock(text);
if (pageLevelDocBlock) {
const {
start: { offset: startOffset },
end: { offset: endOffset },
} = pageLevelDocBlock.loc;
const before = text.substring(0, startOffset);
const after = text.substring(endOffset);
return `${before}${injectPragma(pageLevelDocBlock.value, text)}${after}`;
}
const openTag = "<?php";
if (!text.startsWith(openTag)) {
// bail out
return text;
}
const splitAt = openTag.length;
const phpTag = text.substring(0, splitAt);
const after = text.substring(splitAt);
return `${phpTag}
/**
* @format
*/
${after}`;
}
export { hasPragma, insertPragma };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,743 @@
import { util as prettierUtil } from "prettier";
import { locStart } from "./loc.mjs";
const { hasNewline, skipEverythingButNewLine, skipNewline } = prettierUtil;
function printNumber(rawNumber) {
return (
rawNumber
.toLowerCase()
// Remove unnecessary plus and zeroes from scientific notation.
.replace(/^([+-]?[\d.]+e)(?:\+|(-))?0*(\d)/, "$1$2$3")
// Remove unnecessary scientific notation (1e0).
.replace(/^([+-]?[\d.]+)e[+-]?0+$/, "$1")
// Make sure numbers always start with a digit.
.replace(/^([+-])?\./, "$10.")
// Remove extraneous trailing decimal zeroes.
.replace(/(\.\d+?)0+(?=e|$)/, "$1")
// Remove unnecessary .e notation
.replace(/\.(?=e)/, "")
);
}
// http://php.net/manual/en/language.operators.precedence.php
const PRECEDENCE = new Map(
[
["or"],
["xor"],
["and"],
[
"=",
"+=",
"-=",
"*=",
"**=",
"/=",
".=",
"%=",
"&=",
"|=",
"^=",
"<<=",
">>=",
],
["??"],
["||"],
["&&"],
["|"],
["^"],
["&"],
["==", "===", "!=", "!==", "<>", "<=>"],
["<", ">", "<=", ">="],
[">>", "<<"],
["+", "-", "."],
["*", "/", "%"],
["!"],
["instanceof"],
["++", "--", "~"],
["**"],
].flatMap((operators, index) =>
operators.map((operator) => [operator, index])
)
);
function getPrecedence(operator) {
return PRECEDENCE.get(operator);
}
const equalityOperators = ["==", "!=", "===", "!==", "<>", "<=>"];
const multiplicativeOperators = ["*", "/", "%"];
const bitshiftOperators = [">>", "<<"];
function isBitwiseOperator(operator) {
return (
!!bitshiftOperators[operator] ||
operator === "|" ||
operator === "^" ||
operator === "&"
);
}
function shouldFlatten(parentOp, nodeOp) {
if (getPrecedence(nodeOp) !== getPrecedence(parentOp)) {
return false;
}
// ** is right-associative
// x ** y ** z --> x ** (y ** z)
if (parentOp === "**") {
return false;
}
// x == y == z --> (x == y) == z
if (
equalityOperators.includes(parentOp) &&
equalityOperators.includes(nodeOp)
) {
return false;
}
// x * y % z --> (x * y) % z
if (
(nodeOp === "%" && multiplicativeOperators.includes(parentOp)) ||
(parentOp === "%" && multiplicativeOperators.includes(nodeOp))
) {
return false;
}
// x * y / z --> (x * y) / z
// x / y * z --> (x / y) * z
if (
nodeOp !== parentOp &&
multiplicativeOperators.includes(nodeOp) &&
multiplicativeOperators.includes(parentOp)
) {
return false;
}
// x << y << z --> (x << y) << z
if (
bitshiftOperators.includes(parentOp) &&
bitshiftOperators.includes(nodeOp)
) {
return false;
}
return true;
}
function nodeHasStatement(node) {
return [
"block",
"program",
"namespace",
"class",
"enum",
"interface",
"trait",
"traituse",
"declare",
].includes(node.kind);
}
function getBodyFirstChild({ body }) {
if (!body) {
return null;
}
if (body.kind === "block") {
body = body.children;
}
return body[0];
}
function getNodeListProperty(node) {
const body = node.children || node.body || node.adaptations;
return Array.isArray(body) ? body : null;
}
function getLast(arr) {
if (arr.length > 0) {
return arr[arr.length - 1];
}
return null;
}
function getPenultimate(arr) {
if (arr.length > 1) {
return arr[arr.length - 2];
}
return null;
}
function isFirstChildrenInlineNode(path) {
const { node } = path;
if (node.kind === "program") {
const children = getNodeListProperty(node);
if (!children || children.length === 0) {
return false;
}
return children[0].kind === "inline";
}
if (node.kind === "switch") {
if (!node.body) {
return false;
}
const children = getNodeListProperty(node.body);
if (children.length === 0) {
return false;
}
const [firstCase] = children;
if (!firstCase.body) {
return false;
}
const firstCaseChildren = getNodeListProperty(firstCase.body);
if (firstCaseChildren.length === 0) {
return false;
}
return firstCaseChildren[0].kind === "inline";
}
const firstChild = getBodyFirstChild(node);
if (!firstChild) {
return false;
}
return firstChild.kind === "inline";
}
function isDocNode(node) {
return (
node.kind === "nowdoc" ||
(node.kind === "encapsed" && node.type === "heredoc")
);
}
/**
* Heredoc/Nowdoc nodes need a trailing linebreak if they
* appear as function arguments or array elements
*/
function docShouldHaveTrailingNewline(path, recurse = 0) {
const node = path.getNode(recurse);
const parent = path.getNode(recurse + 1);
const parentParent = path.getNode(recurse + 2);
if (!parent) {
return false;
}
if (
(parentParent &&
["call", "new", "echo"].includes(parentParent.kind) &&
!["call", "array"].includes(parent.kind)) ||
parent.kind === "parameter"
) {
const lastIndex = parentParent.arguments.length - 1;
const index = parentParent.arguments.indexOf(parent);
return index !== lastIndex;
}
if (parentParent && parentParent.kind === "for") {
const initIndex = parentParent.init.indexOf(parent);
if (initIndex !== -1) {
return initIndex !== parentParent.init.length - 1;
}
const testIndex = parentParent.test.indexOf(parent);
if (testIndex !== -1) {
return testIndex !== parentParent.test.length - 1;
}
const incrementIndex = parentParent.increment.indexOf(parent);
if (incrementIndex !== -1) {
return incrementIndex !== parentParent.increment.length - 1;
}
}
if (parent.kind === "bin") {
return (
parent.left === node || docShouldHaveTrailingNewline(path, recurse + 1)
);
}
if (parent.kind === "case" && parent.test === node) {
return true;
}
if (parent.kind === "staticvariable") {
const lastIndex = parentParent.variables.length - 1;
const index = parentParent.variables.indexOf(parent);
return index !== lastIndex;
}
if (parent.kind === "entry") {
if (parent.key === node) {
return true;
}
const lastIndex = parentParent.items.length - 1;
const index = parentParent.items.indexOf(parent);
return index !== lastIndex;
}
if (["call", "new"].includes(parent.kind)) {
const lastIndex = parent.arguments.length - 1;
const index = parent.arguments.indexOf(node);
return index !== lastIndex;
}
if (parent.kind === "echo") {
const lastIndex = parent.expressions.length - 1;
const index = parent.expressions.indexOf(node);
return index !== lastIndex;
}
if (parent.kind === "array") {
const lastIndex = parent.items.length - 1;
const index = parent.items.indexOf(node);
return index !== lastIndex;
}
if (parent.kind === "retif") {
return docShouldHaveTrailingNewline(path, recurse + 1);
}
return false;
}
function lineShouldEndWithSemicolon(path) {
const { node, parent: parentNode } = path;
if (!parentNode) {
return false;
}
// for single line control structures written in a shortform (ie without a block),
// we need to make sure the single body node gets a semicolon
if (
["for", "foreach", "while", "do", "if", "switch"].includes(
parentNode.kind
) &&
node.kind !== "block" &&
node.kind !== "if" &&
(parentNode.body === node || parentNode.alternate === node)
) {
return true;
}
if (!nodeHasStatement(parentNode)) {
return false;
}
if (node.kind === "echo" && node.shortForm) {
return false;
}
if (node.kind === "traituse") {
return !node.adaptations;
}
if (node.kind === "method" && node.isAbstract) {
return true;
}
if (node.kind === "method") {
const { parent } = path;
if (parent && parent.kind === "interface") {
return true;
}
}
return [
"expressionstatement",
"do",
"usegroup",
"classconstant",
"propertystatement",
"traitprecedence",
"traitalias",
"goto",
"constantstatement",
"enumcase",
"global",
"static",
"echo",
"unset",
"return",
"break",
"continue",
"throw",
].includes(node.kind);
}
function fileShouldEndWithHardline(path) {
const { node } = path;
const isProgramNode = node.kind === "program";
const lastNode = node.children && getLast(node.children);
if (!isProgramNode) {
return false;
}
if (lastNode && ["halt", "inline"].includes(lastNode.kind)) {
return false;
}
if (
lastNode &&
(lastNode.kind === "declare" || lastNode.kind === "namespace")
) {
const lastNestedNode =
lastNode.children.length > 0 && getLast(lastNode.children);
if (lastNestedNode && ["halt", "inline"].includes(lastNestedNode.kind)) {
return false;
}
}
return true;
}
function maybeStripLeadingSlashFromUse(name) {
const nameWithoutLeadingSlash = name.replace(/^\\/, "");
if (nameWithoutLeadingSlash.indexOf("\\") !== -1) {
return nameWithoutLeadingSlash;
}
return name;
}
function hasDanglingComments(node) {
return (
node.comments &&
node.comments.some((comment) => !comment.leading && !comment.trailing)
);
}
function isLookupNode(node) {
return (
node.kind === "propertylookup" ||
node.kind === "nullsafepropertylookup" ||
node.kind === "staticlookup" ||
node.kind === "offsetlookup"
);
}
function shouldPrintHardLineAfterStartInControlStructure(path) {
const { node } = path;
if (["try", "catch"].includes(node.kind)) {
return false;
}
return isFirstChildrenInlineNode(path);
}
function shouldPrintHardLineBeforeEndInControlStructure(path) {
const { node } = path;
if (["try", "catch"].includes(node.kind)) {
return true;
}
if (node.kind === "switch") {
const children = getNodeListProperty(node.body);
if (children.length === 0) {
return true;
}
const lastCase = getLast(children);
if (!lastCase.body) {
return true;
}
const childrenInCase = getNodeListProperty(lastCase.body);
if (childrenInCase.length === 0) {
return true;
}
return childrenInCase[0].kind !== "inline";
}
return !isFirstChildrenInlineNode(path);
}
function getAlignment(text) {
const lines = text.split("\n");
const lastLine = lines.pop();
return lastLine.length - lastLine.trimLeft().length + 1;
}
function isProgramLikeNode(node) {
return ["program", "declare", "namespace"].includes(node.kind);
}
function isReferenceLikeNode(node) {
return [
"name",
"parentreference",
"selfreference",
"staticreference",
].includes(node.kind);
}
// Return `logical` value for `bin` node containing `||` or `&&` type otherwise return kind of node.
// Require for grouping logical and binary nodes in right way.
function getNodeKindIncludingLogical(node) {
if (node.kind === "bin" && ["||", "&&"].includes(node.type)) {
return "logical";
}
return node.kind;
}
/**
* Check if string can safely be converted from double to single quotes and vice-versa, i.e.
*
* - no embedded variables ("foo $bar")
* - no linebreaks
* - no special characters like \n, \t, ...
* - no octal/hex/unicode characters
*
* See https://php.net/manual/en/language.types.string.php#language.types.string.syntax.double
*/
function useDoubleQuote(node, options) {
if (node.isDoubleQuote === options.singleQuote) {
// We have a double quote and the user passed singleQuote:true, or the other way around.
const rawValue = node.raw.slice(node.raw[0] === "b" ? 2 : 1, -1);
const isComplex = rawValue.match(
/\\([$nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3}|u{([0-9a-fA-F]+)})|\r?\n|'|"|\$/
);
return node.isDoubleQuote ? isComplex : !isComplex;
}
return node.isDoubleQuote;
}
function hasEmptyBody(path, name = "body") {
const { node } = path;
return (
node[name] &&
node[name].children &&
node[name].children.length === 0 &&
(!node[name].comments || node[name].comments.length === 0)
);
}
function isNextLineEmptyAfterNamespace(text, node) {
let idx = locStart(node);
idx = skipEverythingButNewLine(text, idx);
idx = skipNewline(text, idx);
return hasNewline(text, idx);
}
function shouldPrintHardlineBeforeTrailingComma(lastElem) {
if (
lastElem.kind === "nowdoc" ||
(lastElem.kind === "encapsed" && lastElem.type === "heredoc")
) {
return true;
}
if (
lastElem.kind === "entry" &&
(lastElem.value.kind === "nowdoc" ||
(lastElem.value.kind === "encapsed" && lastElem.value.type === "heredoc"))
) {
return true;
}
return false;
}
function getAncestorCounter(path, typeOrTypes) {
const types = [].concat(typeOrTypes);
let counter = -1;
let ancestorNode;
while ((ancestorNode = path.getParentNode(++counter))) {
if (types.indexOf(ancestorNode.kind) !== -1) {
return counter;
}
}
return -1;
}
function getAncestorNode(path, typeOrTypes) {
const counter = getAncestorCounter(path, typeOrTypes);
return counter === -1 ? null : path.getParentNode(counter);
}
const magicMethods = [
"__construct",
"__destruct",
"__call",
"__callStatic",
"__get",
"__set",
"__isset",
"__unset",
"__sleep",
"__wakeup",
"__toString",
"__invoke",
"__set_state",
"__clone",
"__debugInfo",
];
const magicMethodsMap = new Map(
magicMethods.map((name) => [name.toLowerCase(), name])
);
function normalizeMagicMethodName(name) {
const loweredName = name.toLowerCase();
if (magicMethodsMap.has(loweredName)) {
return magicMethodsMap.get(loweredName);
}
return name;
}
/**
* @param {string[]} kindsArray
* @returns {(node: Node | Comment) => Boolean}
*/
function createTypeCheckFunction(kindsArray) {
const kinds = new Set(kindsArray);
return (node) => kinds.has(node?.kind);
}
const isSingleWordType = createTypeCheckFunction([
"variadicplaceholder",
"namedargument",
"nullkeyword",
"identifier",
"parameter",
"variable",
"variadic",
"boolean",
"literal",
"number",
"string",
"clone",
"cast",
]);
const isArrayExpression = createTypeCheckFunction(["array"]);
const isCallLikeExpression = createTypeCheckFunction([
"nullsafepropertylookup",
"propertylookup",
"staticlookup",
"offsetlookup",
"call",
"new",
]);
const isArrowFuncExpression = createTypeCheckFunction(["arrowfunc"]);
function getChainParts(node, prev = []) {
const parts = prev;
if (isCallLikeExpression(node)) {
parts.push(node);
}
if (!node.what) {
return parts;
}
return getChainParts(node.what, parts);
}
function isSimpleCallArgument(node, depth = 2) {
if (depth <= 0) {
return false;
}
const isChildSimple = (child) => isSimpleCallArgument(child, depth - 1);
if (isSingleWordType(node)) {
return true;
}
if (isArrayExpression(node)) {
return node.items.every((x) => x === null || isChildSimple(x));
}
if (isCallLikeExpression(node)) {
const parts = getChainParts(node);
parts.unshift();
return (
parts.length <= depth &&
parts.every((node) =>
isLookupNode(node)
? isChildSimple(node.offset)
: node.arguments.every(isChildSimple)
)
);
}
if (isArrowFuncExpression(node)) {
return (
node.arguments.length <= depth && node.arguments.every(isChildSimple)
);
}
return false;
}
function memoize(fn) {
const cache = new Map();
return (key) => {
if (!cache.has(key)) {
cache.set(key, fn(key));
}
return cache.get(key);
};
}
export {
printNumber,
getPrecedence,
isBitwiseOperator,
shouldFlatten,
nodeHasStatement,
getLast,
getPenultimate,
getBodyFirstChild,
lineShouldEndWithSemicolon,
fileShouldEndWithHardline,
maybeStripLeadingSlashFromUse,
hasDanglingComments,
docShouldHaveTrailingNewline,
isLookupNode,
isFirstChildrenInlineNode,
shouldPrintHardLineAfterStartInControlStructure,
shouldPrintHardLineBeforeEndInControlStructure,
getAlignment,
isProgramLikeNode,
isReferenceLikeNode,
getNodeKindIncludingLogical,
useDoubleQuote,
hasEmptyBody,
isNextLineEmptyAfterNamespace,
shouldPrintHardlineBeforeTrailingComma,
isDocNode,
getAncestorNode,
normalizeMagicMethodName,
isSimpleCallArgument,
memoize,
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
// SQL语言定义
export const languages = [
{
name: "SQL",
parsers: ["sql"],
extensions: [
".sql",
".ddl",
".dml",
".hql",
".psql",
".plsql",
".mysql",
".mssql",
".pgsql",
".sqlite",
".bigquery",
".snowflake",
".redshift",
".db2",
".n1ql",
".cql"
],
filenames: [
"*.sql",
"*.ddl",
"*.dml"
]
}
];

View File

@@ -0,0 +1,40 @@
import { AST, Option } from 'node-sql-parser';
import { Options, ParserOptions, Plugin } from 'prettier';
import {
FormatOptions,
FormatOptionsWithLanguage,
} from 'sql-formatter';
export type SqlBaseOptions = Option &
Partial<
| (FormatOptions & { dialect: string })
| (FormatOptionsWithLanguage & { dialect?: never })
> & {
formatter?: 'sql-formatter' | 'node-sql-parser' | 'sql-cst';
params?: string;
paramTypes?: string;
autoDetectDialect?: boolean;
};
export type SqlOptions = ParserOptions<AST> & SqlBaseOptions;
export type SqlFormatOptions = Options & SqlBaseOptions;
export declare const languages: Plugin["languages"];
export declare const parsers: {
sql: {
parse(text: string, options?: SqlOptions): AST | string;
astFormat: 'sql';
locStart(): number;
locEnd(): number;
};
};
export declare const printers: Plugin["printers"];
export declare const options: Plugin["options"];
export declare function detectDialect(sql: string): string;
declare const SqlPlugin: Plugin<AST | string>;
export default SqlPlugin;

View File

@@ -0,0 +1,459 @@
import { JSOX } from 'jsox';
import nodeSqlParser from 'node-sql-parser';
import {
format,
formatDialect,
} from 'sql-formatter';
import { detectDialect } from './detect.mjs';
import { languages } from './languages.js';
const parser = new nodeSqlParser.Parser();
const SQL_FORMATTER = 'sql-formatter';
const NODE_SQL_PARSER = 'node-sql-parser';
// Parsers
export const parsers = {
sql: {
parse(text, options = {}) {
const { formatter, type, database } = options;
return formatter === SQL_FORMATTER
? text
: parser.astify(text, { type, database });
},
astFormat: 'sql',
locStart: () => -1,
locEnd: () => -1,
},
};
// Printers
export const printers = {
sql: {
print(path, options = {}) {
const value = path.node;
const {
formatter = SQL_FORMATTER,
type,
database,
dialect,
language,
params,
paramTypes,
autoDetectDialect = true,
...formatOptions
} = options;
let formatted;
if (typeof value === 'string') {
// 准备sql-formatter选项
const sqlFormatterOptions = {
...formatOptions,
params:
params == null
? undefined
: JSOX.parse(params),
paramTypes:
paramTypes == null
? undefined
: JSOX.parse(paramTypes),
};
let finalLanguage = language;
let finalDialect = dialect;
if (autoDetectDialect && !language && !dialect) {
const detectedDialect = detectDialect(value);
finalLanguage = detectedDialect;
}
// 使用适当的格式化方法
if (finalDialect != null) {
// 使用formatDialect方法
formatted = formatDialect(value, {
...sqlFormatterOptions,
dialect: JSOX.parse(finalDialect),
});
} else {
// 使用format方法
formatted = format(value, {
...sqlFormatterOptions,
language: finalLanguage,
});
}
} else {
// 使用node-sql-parser进行格式化
formatted = parser.sqlify(value, { type, database });
}
return formatted + '\n';
},
},
};
// 插件选项
export const options = {
formatter: {
category: 'Config',
type: 'choice',
default: SQL_FORMATTER,
description: 'Choose which formatter to be used',
choices: [
{
value: SQL_FORMATTER,
description: 'use `sql-formatter` as formatter',
},
{
value: NODE_SQL_PARSER,
description: 'use `node-sql-parser` as formatter',
},
],
},
autoDetectDialect: {
category: 'Config',
type: 'boolean',
default: true,
description: 'Automatically detect SQL dialect if language/dialect is not specified',
},
dialect: {
category: 'Config',
type: 'string',
description: 'SQL dialect for `sql-formatter` formatDialect()',
},
language: {
category: 'Config',
type: 'choice',
default: 'sql',
description: 'SQL dialect for `sql-formatter` format()',
choices: [
{
value: 'sql',
description: 'Standard SQL: https://en.wikipedia.org/wiki/SQL:2011',
},
{
value: 'bigquery',
description:
'Google Standard SQL (Bigquery): https://cloud.google.com/bigquery',
},
{
value: 'db2',
description: 'IBM DB2: https://www.ibm.com/products/db2',
},
{
value: 'db2i',
description:
'IBM DB2i (experimental): https://www.ibm.com/docs/en/i/7.5?topic=overview-db2-i',
},
{
value: 'hive',
description: 'Apache Hive: https://hive.apache.org',
},
{
value: 'mariadb',
description: 'MariaDB: https://mariadb.com',
},
{
value: 'mysql',
description: 'MySQL: https://www.mysql.com',
},
{
value: 'n1ql',
description:
'Couchbase N1QL: https://www.couchbase.com/products/n1ql',
},
{
value: 'plsql',
description:
'Oracle PL/SQL: https://www.oracle.com/database/technologies/appdev/plsql.html',
},
{
value: 'postgresql',
description: 'PostgreSQL: https://www.postgresql.org',
},
{
value: 'redshift',
description:
'Amazon Redshift: https://docs.aws.amazon.com/redshift/latest/dg/cm_chap_SQLCommandRef.html',
},
{
value: 'singlestoredb',
description:
'SingleStoreDB: https://docs.singlestore.com/db/v7.8/en/introduction/singlestore-documentation.html',
},
{
value: 'snowflake',
description: 'Snowflake: https://docs.snowflake.com',
},
{
value: 'spark',
description: 'Spark: https://spark.apache.org',
},
{
value: 'sqlite',
description: 'SQLite: https://www.sqlite.org',
},
{
value: 'transactsql',
description:
'SQL Server Transact-SQL: https://docs.microsoft.com/en-us/sql/sql-server/',
},
{
value: 'tsql',
description:
'SQL Server Transact-SQL: https://docs.microsoft.com/en-us/sql/sql-server/',
},
{
value: 'trino',
description: 'Trino: https://trino.io',
},
],
},
keywordCase: {
category: 'Output',
type: 'choice',
default: 'preserve',
description:
'Converts reserved keywords to upper- or lowercase for `sql-formatter`',
choices: [
{
value: 'preserve',
description: 'preserves the original case of reserved keywords',
},
{
value: 'upper',
description: 'converts reserved keywords to uppercase',
},
{
value: 'lower',
description: 'converts reserved keywords to lowercase',
},
],
},
dataTypeCase: {
category: 'Output',
type: 'choice',
default: 'preserve',
description:
'Converts data types to upper- or lowercase for `sql-formatter`',
choices: [
{
value: 'preserve',
description: 'preserves the original case of data types',
},
{
value: 'upper',
description: 'converts data types to uppercase',
},
{
value: 'lower',
description: 'converts data types to lowercase',
},
],
},
functionCase: {
category: 'Output',
type: 'choice',
default: 'preserve',
description:
'Converts functions to upper- or lowercase for `sql-formatter`',
choices: [
{
value: 'preserve',
description: 'preserves the original case of functions',
},
{
value: 'upper',
description: 'converts functions to uppercase',
},
{
value: 'lower',
description: 'converts functions to lowercase',
},
],
},
identifierCase: {
category: 'Output',
type: 'choice',
default: 'preserve',
description:
'Converts identifiers to upper- or lowercase for `sql-formatter`. Only unquoted identifiers are converted. (experimental)',
choices: [
{
value: 'preserve',
description: 'preserves the original case of identifiers',
},
{
value: 'upper',
description: 'converts identifiers to uppercase',
},
{
value: 'lower',
description: 'converts identifiers to lowercase',
},
],
},
uppercase: {
category: 'Output',
type: 'boolean',
deprecated: '0.7.0',
description: 'Use `keywordCase` option instead',
},
indentStyle: {
category: 'Format',
type: 'choice',
default: 'standard',
description: `Switches between different indentation styles for \`sql-formatter\`.
Caveats of using \`"tabularLeft"\` and \`"tabularRight"\`:
- \`tabWidth\` option is ignored. Indentation will always be 10 spaces, regardless of what is specified by \`tabWidth\``,
choices: [
{
value: 'standard',
description:
'indents code by the amount specified by `tabWidth` option',
},
{
value: 'tabularLeft',
description:
'indents in tabular style with 10 spaces, aligning keywords to left',
},
{
value: 'tabularRight',
description:
'indents in tabular style with 10 spaces, aligning keywords to right',
},
],
},
logicalOperatorNewline: {
category: 'Format',
type: 'choice',
default: 'before',
description:
'Decides newline placement before or after logical operators (AND, OR, XOR)',
choices: [
{
value: 'before',
description: 'adds newline before the operator',
},
{
value: 'after',
description: 'adds newline after the operator',
},
],
},
expressionWidth: {
category: 'Format',
type: 'int',
default: 50,
description:
'Determines maximum length of parenthesized expressions for `sql-formatter`',
},
linesBetweenQueries: {
category: 'Format',
type: 'int',
default: 1,
description:
'Decides how many empty lines to leave between SQL statements for `sql-formatter`',
},
denseOperators: {
category: 'Format',
type: 'boolean',
default: false,
description:
'Decides whitespace around operators for `sql-formatter`. Does not apply to logical operators (AND, OR, XOR).',
},
newlineBeforeSemicolon: {
category: 'Format',
type: 'boolean',
default: false,
description:
'Whether to place query separator (`;`) on a separate line for `sql-formatter`',
},
params: {
category: 'Format',
type: 'string',
description:
'Specifies `JSOX` **stringified** parameter values to fill in for placeholders inside SQL for `sql-formatter`. This option is designed to be used through API (though nothing really prevents usage from command line).',
},
paramTypes: {
category: 'Config',
type: 'string',
description:
'Specifies `JSOX` **stringified** parameter types to support when parsing SQL prepared statements for `sql-formatter`.',
},
type: {
category: 'Config',
type: 'choice',
default: 'table',
description: 'Check the SQL with Authority List for `node-sql-parser`',
choices: [
{
value: 'table',
description: '`table` mode',
},
{
value: 'column',
description: '`column` mode',
},
],
},
database: {
category: 'Config',
type: 'choice',
default: 'mysql',
description: 'SQL dialect for `node-sql-parser`',
choices: [
{
value: 'bigquery',
description: 'BigQuery: https://cloud.google.com/bigquery',
},
{
value: 'db2',
description: 'IBM DB2: https://www.ibm.com/analytics/db2',
},
{
value: 'hive',
description: 'Hive: https://hive.apache.org',
},
{
value: 'mariadb',
description: 'MariaDB: https://mariadb.com',
},
{
value: 'mysql',
description: 'MySQL: https://www.mysql.com',
},
{
value: 'postgresql',
description: 'PostgreSQL: https://www.postgresql.org',
},
{
value: 'transactsql',
description:
'TransactSQL: https://docs.microsoft.com/en-us/sql/t-sql',
},
{
value: 'flinksql',
description:
'FlinkSQL: https://ci.apache.org/projects/flink/flink-docs-stable',
},
{
value: 'snowflake',
description: 'Snowflake (alpha): https://docs.snowflake.com',
},
],
},
};
const SqlPlugin = {
languages,
parsers,
printers,
options,
};
export { languages };
export default SqlPlugin;

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

@@ -2,35 +2,33 @@
* 语言映射和解析器配置
*/
import { jsonLanguage } from "@codemirror/lang-json";
import { pythonLanguage } from "@codemirror/lang-python";
import { javascriptLanguage, typescriptLanguage } from "@codemirror/lang-javascript";
import { htmlLanguage } from "@codemirror/lang-html";
import { StandardSQL } from "@codemirror/lang-sql";
import { markdownLanguage } from "@codemirror/lang-markdown";
import { javaLanguage } from "@codemirror/lang-java";
import { phpLanguage } from "@codemirror/lang-php";
import { cssLanguage } from "@codemirror/lang-css";
import { cppLanguage } from "@codemirror/lang-cpp";
import { xmlLanguage } from "@codemirror/lang-xml";
import { rustLanguage } from "@codemirror/lang-rust";
import { yamlLanguage } from "@codemirror/lang-yaml";
import {jsonLanguage} from "@codemirror/lang-json";
import {pythonLanguage} from "@codemirror/lang-python";
import {javascriptLanguage, typescriptLanguage} from "@codemirror/lang-javascript";
import {htmlLanguage} from "@codemirror/lang-html";
import {StandardSQL} from "@codemirror/lang-sql";
import {markdownLanguage} from "@codemirror/lang-markdown";
import {javaLanguage} from "@codemirror/lang-java";
import {phpLanguage} from "@codemirror/lang-php";
import {cssLanguage} from "@codemirror/lang-css";
import {cppLanguage} from "@codemirror/lang-cpp";
import {xmlLanguage} from "@codemirror/lang-xml";
import {rustLanguage} from "@codemirror/lang-rust";
import {yamlLanguage} from "@codemirror/lang-yaml";
import { StreamLanguage } from "@codemirror/language";
import { ruby } from "@codemirror/legacy-modes/mode/ruby";
import { shell } from "@codemirror/legacy-modes/mode/shell";
import { go } from "@codemirror/legacy-modes/mode/go";
import { csharp } from "@codemirror/legacy-modes/mode/clike";
import { clojure } from "@codemirror/legacy-modes/mode/clojure";
import { erlang } from "@codemirror/legacy-modes/mode/erlang";
import { swift } from "@codemirror/legacy-modes/mode/swift";
import { kotlin } from "@codemirror/legacy-modes/mode/clike";
import { groovy } from "@codemirror/legacy-modes/mode/groovy";
import { powerShell } from "@codemirror/legacy-modes/mode/powershell";
import { scala } from "@codemirror/legacy-modes/mode/clike";
import { toml } from "@codemirror/legacy-modes/mode/toml";
import { elixir } from "codemirror-lang-elixir";
import { SupportedLanguage } from '../types';
import {StreamLanguage} from "@codemirror/language";
import {ruby} from "@codemirror/legacy-modes/mode/ruby";
import {shell} from "@codemirror/legacy-modes/mode/shell";
import {go} from "@codemirror/legacy-modes/mode/go";
import {csharp, kotlin, scala} from "@codemirror/legacy-modes/mode/clike";
import {clojure} from "@codemirror/legacy-modes/mode/clojure";
import {erlang} from "@codemirror/legacy-modes/mode/erlang";
import {swift} from "@codemirror/legacy-modes/mode/swift";
import {groovy} from "@codemirror/legacy-modes/mode/groovy";
import {powerShell} from "@codemirror/legacy-modes/mode/powershell";
import {toml} from "@codemirror/legacy-modes/mode/toml";
import {elixir} from "codemirror-lang-elixir";
import {SupportedLanguage} from '../types';
import typescriptPlugin from "prettier/plugins/typescript"
import babelPrettierPlugin from "prettier/plugins/babel"
@@ -38,96 +36,108 @@ import htmlPrettierPlugin from "prettier/plugins/html"
import cssPrettierPlugin from "prettier/plugins/postcss"
import markdownPrettierPlugin from "prettier/plugins/markdown"
import yamlPrettierPlugin from "prettier/plugins/yaml"
import goPrettierPlugin from "@/utils/prettier/plugins/go/go"
import sqlPrettierPlugin from "@/utils/prettier/plugins/sql/sql"
import phpPrettierPlugin from "@/utils/prettier/plugins/php"
import * as prettierPluginEstree from "prettier/plugins/estree";
/**
* 语言信息类
*/
export class LanguageInfo {
constructor(
public token: SupportedLanguage,
public name: string,
public parser: any,
public prettier?: {
parser: string;
plugins: any[];
constructor(
public token: SupportedLanguage,
public name: string,
public parser: any,
public prettier?: {
parser: string;
plugins: any[];
}) {
}
) {}
}
/**
* 支持的语言列表(与 Worker 中的 LANGUAGES 对应)
*/
export const LANGUAGES: LanguageInfo[] = [
new LanguageInfo("text", "Plain Text", null),
new LanguageInfo("json", "JSON", jsonLanguage.parser, {
parser: "json",
plugins: [babelPrettierPlugin, prettierPluginEstree]
}),
new LanguageInfo("py", "Python", pythonLanguage.parser),
new LanguageInfo("html", "HTML", htmlLanguage.parser, {
parser: "html",
plugins: [htmlPrettierPlugin]
}),
new LanguageInfo("sql", "SQL", StandardSQL.language.parser),
new LanguageInfo("md", "Markdown", markdownLanguage.parser, {
parser: "markdown",
plugins: [markdownPrettierPlugin]
}),
new LanguageInfo("java", "Java", javaLanguage.parser),
new LanguageInfo("php", "PHP", phpLanguage.configure({top:"Program"}).parser),
new LanguageInfo("css", "CSS", cssLanguage.parser, {
parser: "css",
plugins: [cssPrettierPlugin]
}),
new LanguageInfo("xml", "XML", xmlLanguage.parser),
new LanguageInfo("cpp", "C++", cppLanguage.parser),
new LanguageInfo("rs", "Rust", rustLanguage.parser),
new LanguageInfo("cs", "C#", StreamLanguage.define(csharp).parser),
new LanguageInfo("rb", "Ruby", StreamLanguage.define(ruby).parser),
new LanguageInfo("sh", "Shell", StreamLanguage.define(shell).parser),
new LanguageInfo("yaml", "YAML", yamlLanguage.parser, {
parser: "yaml",
plugins: [yamlPrettierPlugin]
}),
new LanguageInfo("toml", "TOML", StreamLanguage.define(toml).parser),
new LanguageInfo("go", "Go", StreamLanguage.define(go).parser),
new LanguageInfo("clj", "Clojure", StreamLanguage.define(clojure).parser),
new LanguageInfo("ex", "Elixir", elixir().language.parser),
new LanguageInfo("erl", "Erlang", StreamLanguage.define(erlang).parser),
new LanguageInfo("js", "JavaScript", javascriptLanguage.parser, {
parser: "babel",
plugins: [babelPrettierPlugin, prettierPluginEstree]
}),
new LanguageInfo("ts", "TypeScript", typescriptLanguage.parser, {
parser: "typescript",
plugins: [typescriptPlugin, prettierPluginEstree]
}),
new LanguageInfo("swift", "Swift", StreamLanguage.define(swift).parser),
new LanguageInfo("kt", "Kotlin", StreamLanguage.define(kotlin).parser),
new LanguageInfo("groovy", "Groovy", StreamLanguage.define(groovy).parser),
new LanguageInfo("ps1", "PowerShell", StreamLanguage.define(powerShell).parser),
new LanguageInfo("dart", "Dart", null), // 暂无解析器
new LanguageInfo("scala", "Scala", StreamLanguage.define(scala).parser),
new LanguageInfo("text", "Plain Text", null),
new LanguageInfo("json", "JSON", jsonLanguage.parser, {
parser: "json",
plugins: [babelPrettierPlugin, prettierPluginEstree]
}),
new LanguageInfo("py", "Python", pythonLanguage.parser),
new LanguageInfo("html", "HTML", htmlLanguage.parser, {
parser: "html",
plugins: [htmlPrettierPlugin]
}),
new LanguageInfo("sql", "SQL", StandardSQL.language.parser, {
parser: "sql",
plugins: [sqlPrettierPlugin]
}),
new LanguageInfo("md", "Markdown", markdownLanguage.parser, {
parser: "markdown",
plugins: [markdownPrettierPlugin]
}),
new LanguageInfo("java", "Java", javaLanguage.parser),
new LanguageInfo("php", "PHP", phpLanguage.configure({top: "Program"}).parser, {
parser: "php",
plugins: [phpPrettierPlugin]
}),
new LanguageInfo("css", "CSS", cssLanguage.parser, {
parser: "css",
plugins: [cssPrettierPlugin]
}),
new LanguageInfo("xml", "XML", xmlLanguage.parser),
new LanguageInfo("cpp", "C++", cppLanguage.parser),
new LanguageInfo("rs", "Rust", rustLanguage.parser),
new LanguageInfo("cs", "C#", StreamLanguage.define(csharp).parser),
new LanguageInfo("rb", "Ruby", StreamLanguage.define(ruby).parser),
new LanguageInfo("sh", "Shell", StreamLanguage.define(shell).parser),
new LanguageInfo("yaml", "YAML", yamlLanguage.parser, {
parser: "yaml",
plugins: [yamlPrettierPlugin]
}),
new LanguageInfo("toml", "TOML", StreamLanguage.define(toml).parser),
new LanguageInfo("go", "Go", StreamLanguage.define(go).parser, {
parser: "go",
plugins: [goPrettierPlugin]
}),
new LanguageInfo("clj", "Clojure", StreamLanguage.define(clojure).parser),
new LanguageInfo("ex", "Elixir", elixir().language.parser),
new LanguageInfo("erl", "Erlang", StreamLanguage.define(erlang).parser),
new LanguageInfo("js", "JavaScript", javascriptLanguage.parser, {
parser: "babel",
plugins: [babelPrettierPlugin, prettierPluginEstree]
}),
new LanguageInfo("ts", "TypeScript", typescriptLanguage.parser, {
parser: "typescript",
plugins: [typescriptPlugin, prettierPluginEstree]
}),
new LanguageInfo("swift", "Swift", StreamLanguage.define(swift).parser),
new LanguageInfo("kt", "Kotlin", StreamLanguage.define(kotlin).parser),
new LanguageInfo("groovy", "Groovy", StreamLanguage.define(groovy).parser),
new LanguageInfo("ps1", "PowerShell", StreamLanguage.define(powerShell).parser),
new LanguageInfo("dart", "Dart", null), // 暂无解析器
new LanguageInfo("scala", "Scala", StreamLanguage.define(scala).parser),
];
/**
* 语言映射表
*/
export const languageMapping = Object.fromEntries(
LANGUAGES.map(l => [l.token, l.parser])
LANGUAGES.map(l => [l.token, l.parser])
);
/**
* 根据 token 获取语言信息
*/
export function getLanguage(token: SupportedLanguage): LanguageInfo | undefined {
return LANGUAGES.find(lang => lang.token === token);
return LANGUAGES.find(lang => lang.token === token);
}
/**
* 获取所有语言的 token 列表
*/
export function getLanguageTokens(): SupportedLanguage[] {
return LANGUAGES.map(lang => lang.token);
return LANGUAGES.map(lang => lang.token);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import {defineConfig, loadEnv} from 'vite';
import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
import * as path from 'path';
import { nodePolyfills } from 'vite-plugin-node-polyfills'
export default defineConfig(({mode}: { mode: string }): object => {
const env: Record<string, string> = loadEnv(mode, process.cwd());
@@ -15,6 +16,7 @@ export default defineConfig(({mode}: { mode: string }): object => {
},
plugins: [
vue(),
nodePolyfills(),
Components({
dts: true,
dirs: ['src/components'],

25
go.mod
View File

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

60
go.sum
View File

@@ -2,6 +2,8 @@ code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
@@ -158,14 +160,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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA=
github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
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.12/go.mod h1:4LCCW7s9e4PuSmu7l9OTvfWIGMO8TaSiftSeR5NpBIc=
github.com/wailsapp/wails/v3 v3.0.0-alpha.25 h1:o05zUiPEvmrq2lqqCs4wqnrnAjGmhryYHRhjQmtkvk8=
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/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@@ -174,19 +176,21 @@ 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-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.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/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-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-20210505024714-0287a6fb4125/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.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
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.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
@@ -203,21 +207,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.1.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.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
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.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
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.3/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.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
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/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
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.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
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=
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=
@@ -234,18 +238,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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
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.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
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/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
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/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
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/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -254,8 +258,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
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/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
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/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

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

View File

@@ -1,5 +1,20 @@
package models
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
)
// ThemeType 主题类型枚举
type ThemeType string
const (
ThemeTypeDark ThemeType = "dark"
ThemeTypeLight ThemeType = "light"
)
// ThemeColorConfig 主题颜色配置
type ThemeColorConfig struct {
// 基础色调
@@ -36,15 +51,44 @@ type ThemeColorConfig struct {
MatchingBracket string `json:"matchingBracket"` // 匹配括号
}
// CustomThemeConfig 自定义主题配置
type CustomThemeConfig struct {
DarkTheme ThemeColorConfig `json:"darkTheme"` // 深色主题配置
LightTheme ThemeColorConfig `json:"lightTheme"` // 浅色主题配置
// Theme 主题数据库模型
type Theme struct {
ID int `db:"id" json:"id"`
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 创建默认深色主题配置
func NewDefaultDarkTheme() ThemeColorConfig {
return ThemeColorConfig{
func NewDefaultDarkTheme() *ThemeColorConfig {
return &ThemeColorConfig{
// 基础色调
Background: "#252B37",
BackgroundSecondary: "#213644",
@@ -81,8 +125,8 @@ func NewDefaultDarkTheme() ThemeColorConfig {
}
// NewDefaultLightTheme 创建默认浅色主题配置
func NewDefaultLightTheme() ThemeColorConfig {
return ThemeColorConfig{
func NewDefaultLightTheme() *ThemeColorConfig {
return &ThemeColorConfig{
// 基础色调
Background: "#ffffff",
BackgroundSecondary: "#f1faf1",
@@ -117,11 +161,3 @@ func NewDefaultLightTheme() ThemeColorConfig {
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
dbService *DatabaseService
repository *git.Repository
logger *log.Service
logger *log.LogService
isInitialized bool
autoBackupTicker *time.Ticker
autoBackupStop chan bool
}
// NewBackupService 创建新的备份服务实例
func NewBackupService(configService *ConfigService, dbService *DatabaseService, logger *log.Service) *BackupService {
func NewBackupService(configService *ConfigService, dbService *DatabaseService, logger *log.LogService) *BackupService {
return &BackupService{
configService: configService,
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{
Author: &object.Signature{
Name: "VoidRaft",
Name: "voidraft",
Email: "backup@voidraft.app",
When: time.Now(),
},

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,8 @@ const (
ConfigChangeTypeDataPath ConfigChangeType = "datapath"
// ConfigChangeTypeBackup 备份配置变更
ConfigChangeTypeBackup ConfigChangeType = "backup"
// ConfigChangeTypeWindowSnap 窗口吸附配置变更
ConfigChangeTypeWindowSnap ConfigChangeType = "windowsnap"
)
// ConfigChangeCallback 配置变更回调函数类型
@@ -51,7 +53,7 @@ type ConfigListener struct {
type ConfigNotificationService struct {
listeners map[ConfigChangeType][]*ConfigListener // 支持多监听器的map
mu sync.RWMutex // 监听器map的读写锁
logger *log.Service // 日志服务
logger *log.LogService // 日志服务
koanf *koanf.Koanf // koanf实例
ctx context.Context
cancel context.CancelFunc
@@ -59,7 +61,7 @@ type ConfigNotificationService struct {
}
// 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())
return &ConfigNotificationService{
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 防抖通知
func (cns *ConfigNotificationService) debounceNotify(listener *ConfigListener, oldConfig, newConfig *models.AppConfig) {
listener.mu.Lock()
@@ -470,6 +453,29 @@ func CreateBackupConfigListener(name string, callback func(config *models.GitBac
}
}
// CreateWindowSnapConfigListener 创建窗口吸附配置监听器
func CreateWindowSnapConfigListener(name string, callback func(enabled bool) error) *ConfigListener {
return &ConfigListener{
Name: name,
ChangeType: ConfigChangeTypeWindowSnap,
Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
if newConfig == nil {
defaultConfig := models.NewDefaultAppConfig()
return callback(defaultConfig.General.EnableWindowSnap)
}
return callback(newConfig.General.EnableWindowSnap)
},
DebounceDelay: 200 * time.Millisecond,
GetConfigFunc: func(k *koanf.Koanf) *models.AppConfig {
var config models.AppConfig
if err := k.Unmarshal("", &config); err != nil {
return nil
}
return &config
},
}
}
// ServiceShutdown 关闭服务
func (cns *ConfigNotificationService) ServiceShutdown() error {
cns.Cleanup()

View File

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

View File

@@ -63,6 +63,19 @@ CREATE TABLE IF NOT EXISTS key_bindings (
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
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 存储列的信息
@@ -80,7 +93,7 @@ type TableModel struct {
// DatabaseService provides shared database functionality
type DatabaseService struct {
configService *ConfigService
logger *log.Service
logger *log.LogService
db *sql.DB
mu sync.RWMutex
ctx context.Context
@@ -88,7 +101,7 @@ type DatabaseService struct {
}
// 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 {
logger = log.New()
}
@@ -112,6 +125,8 @@ func (ds *DatabaseService) registerAllModels() {
ds.RegisterModel("extensions", &models.Extension{})
// 快捷键表
ds.RegisterModel("key_bindings", &models.KeyBinding{})
// 主题表
ds.RegisterModel("themes", &models.Theme{})
}
// ServiceStartup initializes the service when the application starts
@@ -181,6 +196,7 @@ func (ds *DatabaseService) createTables() error {
sqlCreateDocumentsTable,
sqlCreateExtensionsTable,
sqlCreateKeyBindingsTable,
sqlCreateThemesTable,
}
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_extension ON key_bindings(extension)`,
`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 {

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ package services
#cgo CFLAGS: -I../lib
#cgo LDFLAGS: -luser32
#include "../lib/hotkey_windows.c"
#include "hotkey_windows.h"
#include "../lib/hotkey_windows.h"
*/
import "C"
@@ -24,17 +24,19 @@ import (
// HotkeyService Windows全局热键服务
type HotkeyService struct {
logger *log.Service
logger *log.LogService
configService *ConfigService
windowService *WindowService
app *application.App
mainWindow *application.WebviewWindow
mu sync.RWMutex
currentHotkey *models.HotkeyCombo
isRegistered atomic.Bool
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
ctx context.Context
cancelFunc atomic.Value // 使用atomic.Value存储cancel函数避免竞态条件
wg sync.WaitGroup
}
// HotkeyError 热键错误
@@ -51,24 +53,48 @@ func (e *HotkeyError) Unwrap() error {
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 创建热键服务实例
func NewHotkeyService(configService *ConfigService, logger *log.Service) *HotkeyService {
func NewHotkeyService(configService *ConfigService, windowService *WindowService, logger *log.LogService) *HotkeyService {
if logger == nil {
logger = log.New()
}
ctx, cancel := context.WithCancel(context.Background())
return &HotkeyService{
service := &HotkeyService{
logger: logger,
configService: configService,
windowService: windowService,
ctx: ctx,
cancel: cancel,
}
// 初始化时设置cancel函数
service.setCancelFunc(cancel)
return service
}
// Initialize 初始化热键服务
func (hs *HotkeyService) Initialize(app *application.App) error {
func (hs *HotkeyService) Initialize(app *application.App, mainWindow *application.WebviewWindow) error {
hs.app = app
hs.mainWindow = mainWindow
config, err := hs.configService.GetConfig()
if err != nil {
@@ -119,7 +145,7 @@ func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error {
hs.currentHotkey = hotkey
hs.isRegistered.Store(true)
hs.cancel = cancel
hs.setCancelFunc(cancel)
return nil
}
@@ -137,13 +163,15 @@ func (hs *HotkeyService) unregisterInternal() error {
return nil
}
if hs.cancel != nil {
hs.cancel()
// 原子地获取并调用cancel函数
if cancel := hs.getCancelFunc(); cancel != nil {
cancel()
hs.wg.Wait()
}
hs.currentHotkey = nil
hs.isRegistered.Store(false)
hs.clearCancelFunc()
return nil
}
@@ -165,7 +193,7 @@ func (hs *HotkeyService) hotkeyListener(ctx context.Context, hotkey *models.Hotk
return
}
ticker := time.NewTicker(100 * time.Millisecond)
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()
var wasPressed bool
@@ -199,11 +227,61 @@ func cBool(b bool) C.int {
return 0
}
// toggleWindow 切换窗口
// toggleWindow 切换窗口显示状态
func (hs *HotkeyService) toggleWindow() {
if hs.app != nil {
hs.app.Event.Emit("hotkey:toggle-window", nil)
if hs.mainWindow == 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 键名转虚拟键码
@@ -261,7 +339,10 @@ func (hs *HotkeyService) IsRegistered() bool {
// ServiceShutdown 关闭服务
func (hs *HotkeyService) ServiceShutdown() error {
hs.cancel()
// 原子地获取并调用cancel函数
if cancel := hs.getCancelFunc(); cancel != nil {
cancel()
}
hs.wg.Wait()
return nil
}

View File

@@ -66,6 +66,7 @@ int isHotkeyRegistered() {
import "C"
import (
"context"
"fmt"
"sync"
"sync/atomic"
@@ -80,12 +81,15 @@ var globalHotkeyService *HotkeyService
// HotkeyService macOS全局热键服务
type HotkeyService struct {
logger *log.Service
logger *log.LogService
configService *ConfigService
windowService *WindowService
app *application.App
mainWindow *application.WebviewWindow
mu sync.RWMutex
isRegistered atomic.Bool
currentHotkey *models.HotkeyCombo
cancelFunc atomic.Value // 使用atomic.Value存储cancel函数避免竞态条件
}
// HotkeyError 热键错误
@@ -104,8 +108,28 @@ func (e *HotkeyError) Unwrap() error {
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 创建新的热键服务实例
func NewHotkeyService(configService *ConfigService, logger *log.Service) *HotkeyService {
func NewHotkeyService(configService *ConfigService, windowService *WindowService, logger *log.LogService) *HotkeyService {
if logger == nil {
logger = log.New()
}
@@ -113,6 +137,7 @@ func NewHotkeyService(configService *ConfigService, logger *log.Service) *Hotkey
service := &HotkeyService{
logger: logger,
configService: configService,
windowService: windowService,
}
// 设置全局实例
@@ -122,8 +147,9 @@ func NewHotkeyService(configService *ConfigService, logger *log.Service) *Hotkey
}
// Initialize 初始化热键服务
func (hs *HotkeyService) Initialize(app *application.App) error {
func (hs *HotkeyService) Initialize(app *application.App, mainWindow *application.WebviewWindow) error {
hs.app = app
hs.mainWindow = mainWindow
// 加载并应用当前配置
config, err := hs.configService.GetConfig()
@@ -285,9 +311,59 @@ func (hs *HotkeyService) IsRegistered() bool {
// ToggleWindow 切换窗口显示状态
func (hs *HotkeyService) ToggleWindow() {
if hs.app != nil {
hs.app.EmitEvent("hotkey:toggle-window", nil)
if hs.mainWindow == 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 关闭热键服务

View File

@@ -141,17 +141,19 @@ import (
// HotkeyService Linux全局热键服务
type HotkeyService struct {
logger *log.Service
logger *log.LogService
configService *ConfigService
windowService *WindowService
app *application.App
mainWindow *application.WebviewWindow
mu sync.RWMutex
currentHotkey *models.HotkeyCombo
isRegistered atomic.Bool
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
ctx context.Context
cancelFunc atomic.Value // 使用atomic.Value存储cancel函数避免竞态条件
wg sync.WaitGroup
}
// HotkeyError 热键错误
@@ -169,24 +171,48 @@ func (e *HotkeyError) Unwrap() error {
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 创建热键服务实例
func NewHotkeyService(configService *ConfigService, logger *log.Service) *HotkeyService {
func NewHotkeyService(configService *ConfigService, windowService *WindowService, logger *log.LogService) *HotkeyService {
if logger == nil {
logger = log.New()
}
ctx, cancel := context.WithCancel(context.Background())
return &HotkeyService{
service := &HotkeyService{
logger: logger,
configService: configService,
windowService: windowService,
ctx: ctx,
cancel: cancel,
}
// 初始化时设置cancel函数
service.setCancelFunc(cancel)
return service
}
// Initialize 初始化热键服务
func (hs *HotkeyService) Initialize(app *application.App) error {
func (hs *HotkeyService) Initialize(app *application.App, mainWindow *application.WebviewWindow) error {
hs.app = app
hs.mainWindow = mainWindow
if int(C.initX11Display()) == 0 {
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.isRegistered.Store(true)
hs.cancel = cancel
hs.setCancelFunc(cancel)
return nil
}
@@ -271,8 +297,9 @@ func (hs *HotkeyService) unregisterInternal() error {
return nil
}
if hs.cancel != nil {
hs.cancel()
// 原子地获取并调用cancel函数
if cancel := hs.getCancelFunc(); cancel != nil {
cancel()
hs.wg.Wait()
}
@@ -283,6 +310,7 @@ func (hs *HotkeyService) unregisterInternal() error {
hs.currentHotkey = nil
hs.isRegistered.Store(false)
hs.clearCancelFunc()
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) {
defer hs.wg.Done()
ticker := time.NewTicker(100 * time.Millisecond)
// 优化轮询频率从100ms改为50ms提高响应性
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()
ready <- nil // 标记准备就绪
@@ -315,11 +344,61 @@ func (hs *HotkeyService) hotkeyListener(ctx context.Context, ready chan<- error)
}
}
// toggleWindow 切换窗口
// toggleWindow 切换窗口显示状态
func (hs *HotkeyService) toggleWindow() {
if hs.app != nil {
hs.app.EmitEvent("hotkey:toggle-window", nil)
if hs.mainWindow == 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键码
@@ -386,7 +465,10 @@ func (hs *HotkeyService) IsRegistered() bool {
// ServiceShutdown 关闭服务
func (hs *HotkeyService) ServiceShutdown() error {
hs.cancel()
// 原子地获取并调用cancel函数
if cancel := hs.getCancelFunc(); cancel != nil {
cancel()
}
hs.wg.Wait()
C.closeX11Display()
return nil

View File

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

View File

@@ -33,22 +33,24 @@ type MigrationProgress struct {
// MigrationService 迁移服务
type MigrationService struct {
logger *log.Service
mu sync.RWMutex
progress atomic.Value // stores MigrationProgress
logger *log.LogService
dbService *DatabaseService
mu sync.RWMutex
progress atomic.Value // stores MigrationProgress
ctx context.Context
cancel context.CancelFunc
}
// NewMigrationService 创建迁移服务
func NewMigrationService(logger *log.Service) *MigrationService {
func NewMigrationService(dbService *DatabaseService, logger *log.LogService) *MigrationService {
if logger == nil {
logger = log.New()
}
ms := &MigrationService{
logger: logger,
logger: logger,
dbService: dbService,
}
// 初始化进度
@@ -104,6 +106,17 @@ func (ms *MigrationService) MigrateDirectory(srcPath, dstPath string) error {
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 {
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 {
// 重命名成功更新进度到90%
ms.updateProgress(MigrationProgress{
Status: MigrationStatusMigrating,
Progress: 90,
})
ms.logger.Info("Directory migration completed using direct rename", "src", srcPath, "dst", dstPath)
return nil
}
// 重命名失败,使用压缩迁移
ms.logger.Info("Direct rename failed, using compress migration", "src", srcPath, "dst", dstPath)
ms.updateProgress(MigrationProgress{
Status: MigrationStatusMigrating,
Progress: 30,
@@ -249,12 +269,18 @@ func (ms *MigrationService) compressMove(ctx context.Context, srcPath, dstPath s
default:
}
// 验证迁移是否成功
if err := ms.verifyMigration(dstPath); err != nil {
// 迁移验证失败,清理目标目录
os.RemoveAll(dstPath)
return fmt.Errorf("migration verification failed: %v", err)
}
// 删除源目录
ms.updateProgress(MigrationProgress{
Status: MigrationStatusMigrating,
Progress: 90,
})
os.RemoveAll(srcPath)
return nil
}
@@ -404,6 +430,29 @@ func (ms *MigrationService) isSubDirectory(parent, target string) bool {
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 取消迁移
func (ms *MigrationService) CancelMigration() error {
ms.mu.Lock()

View File

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

View File

@@ -4,27 +4,34 @@ import (
"voidraft/internal/models"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/services/badge"
"github.com/wailsapp/wails/v3/pkg/services/log"
"github.com/wailsapp/wails/v3/pkg/services/notifications"
)
// ServiceManager 服务管理器,负责协调各个服务
type ServiceManager struct {
configService *ConfigService
databaseService *DatabaseService
documentService *DocumentService
windowService *WindowService
migrationService *MigrationService
systemService *SystemService
hotkeyService *HotkeyService
dialogService *DialogService
trayService *TrayService
keyBindingService *KeyBindingService
extensionService *ExtensionService
startupService *StartupService
selfUpdateService *SelfUpdateService
translationService *TranslationService
BackupService *BackupService
logger *log.Service
configService *ConfigService
databaseService *DatabaseService
documentService *DocumentService
windowService *WindowService
windowSnapService *WindowSnapService
migrationService *MigrationService
systemService *SystemService
hotkeyService *HotkeyService
dialogService *DialogService
trayService *TrayService
keyBindingService *KeyBindingService
extensionService *ExtensionService
startupService *StartupService
selfUpdateService *SelfUpdateService
translationService *TranslationService
themeService *ThemeService
badgeService *badge.BadgeService
notificationService *notifications.NotificationService
testService *TestService // 测试服务(仅开发环境)
BackupService *BackupService
logger *log.LogService
}
// NewServiceManager 创建新的服务管理器实例
@@ -32,6 +39,12 @@ func NewServiceManager() *ServiceManager {
// 初始化日志服务
logger := log.New()
// 初始化badge服务
badgeService := badge.New()
// 初始化通知服务
notificationService := notifications.New()
// 初始化配置服务
configService := NewConfigService(logger)
@@ -39,7 +52,7 @@ func NewServiceManager() *ServiceManager {
databaseService := NewDatabaseService(configService, logger)
// 初始化迁移服务
migrationService := NewMigrationService(logger)
migrationService := NewMigrationService(databaseService, logger)
// 初始化文档服务
documentService := NewDocumentService(databaseService, logger)
@@ -47,11 +60,17 @@ func NewServiceManager() *ServiceManager {
// 初始化窗口服务
windowService := NewWindowService(logger, documentService)
// 初始化窗口吸附服务
windowSnapService := NewWindowSnapService(logger, configService)
// 将吸附服务与窗口服务关联
windowService.SetWindowSnapService(windowSnapService)
// 初始化系统服务
systemService := NewSystemService(logger)
// 初始化热键服务
hotkeyService := NewHotkeyService(configService, logger)
hotkeyService := NewHotkeyService(configService, windowService, logger)
// 初始化对话服务
dialogService := NewDialogService(logger)
@@ -69,7 +88,7 @@ func NewServiceManager() *ServiceManager {
startupService := NewStartupService(configService, logger)
// 初始化自我更新服务
selfUpdateService, err := NewSelfUpdateService(configService, logger)
selfUpdateService, err := NewSelfUpdateService(configService, badgeService, notificationService, logger)
if err != nil {
panic(err)
}
@@ -77,9 +96,15 @@ func NewServiceManager() *ServiceManager {
// 初始化翻译服务
translationService := NewTranslationService(logger)
// 初始化主题服务
themeService := NewThemeService(databaseService, logger)
// 初始化备份服务
backupService := NewBackupService(configService, databaseService, logger)
// 初始化测试服务(开发环境使用)
testService := NewTestService(badgeService, notificationService, logger)
// 使用新的配置通知系统设置热键配置变更监听
err = configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error {
return hotkeyService.UpdateHotkey(enable, hotkey)
@@ -104,23 +129,36 @@ func NewServiceManager() *ServiceManager {
panic(err)
}
// 设置窗口吸附配置变更回调
err = configService.SetWindowSnapConfigChangeCallback(func(enabled bool) error {
return windowSnapService.OnWindowSnapConfigChanged(enabled)
})
if err != nil {
panic(err)
}
return &ServiceManager{
configService: configService,
databaseService: databaseService,
documentService: documentService,
windowService: windowService,
migrationService: migrationService,
systemService: systemService,
hotkeyService: hotkeyService,
dialogService: dialogService,
trayService: trayService,
keyBindingService: keyBindingService,
extensionService: extensionService,
startupService: startupService,
selfUpdateService: selfUpdateService,
translationService: translationService,
BackupService: backupService,
logger: logger,
configService: configService,
databaseService: databaseService,
documentService: documentService,
windowService: windowService,
windowSnapService: windowSnapService,
migrationService: migrationService,
systemService: systemService,
hotkeyService: hotkeyService,
dialogService: dialogService,
trayService: trayService,
keyBindingService: keyBindingService,
extensionService: extensionService,
startupService: startupService,
selfUpdateService: selfUpdateService,
translationService: translationService,
themeService: themeService,
badgeService: badgeService,
notificationService: notificationService,
testService: testService,
BackupService: backupService,
logger: logger,
}
}
@@ -131,6 +169,7 @@ func (sm *ServiceManager) GetServices() []application.Service {
application.NewService(sm.databaseService),
application.NewService(sm.documentService),
application.NewService(sm.windowService),
application.NewService(sm.windowSnapService),
application.NewService(sm.keyBindingService),
application.NewService(sm.extensionService),
application.NewService(sm.migrationService),
@@ -141,6 +180,10 @@ func (sm *ServiceManager) GetServices() []application.Service {
application.NewService(sm.startupService),
application.NewService(sm.selfUpdateService),
application.NewService(sm.translationService),
application.NewService(sm.themeService),
application.NewService(sm.badgeService),
application.NewService(sm.notificationService),
application.NewService(sm.testService), // 注册测试服务
application.NewService(sm.BackupService),
}
return services
@@ -157,7 +200,7 @@ func (sm *ServiceManager) GetDialogService() *DialogService {
}
// GetLogger 获取日志服务实例
func (sm *ServiceManager) GetLogger() *log.Service {
func (sm *ServiceManager) GetLogger() *log.LogService {
return sm.logger
}
@@ -210,3 +253,23 @@ func (sm *ServiceManager) GetWindowService() *WindowService {
func (sm *ServiceManager) GetDocumentService() *DocumentService {
return sm.documentService
}
// GetThemeService 获取主题服务实例
func (sm *ServiceManager) GetThemeService() *ThemeService {
return sm.themeService
}
// GetWindowSnapService 获取窗口吸附服务实例
func (sm *ServiceManager) GetWindowSnapService() *WindowSnapService {
return sm.windowSnapService
}
// GetBadgeService 获取badge服务实例
func (sm *ServiceManager) GetBadgeService() *badge.BadgeService {
return sm.badgeService
}
// GetNotificationService 获取通知服务实例
func (sm *ServiceManager) GetNotificationService() *notifications.NotificationService {
return sm.notificationService
}

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More