Compare commits
24
Commits
v1.2.0
..
1fb4f64cb3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fb4f64cb3 | ||
|
|
1f8e8981ce | ||
|
|
a257d30dba | ||
|
|
97ee3b0667 | ||
|
|
8e2bafba5f | ||
|
|
6149bc133d | ||
|
|
5f22ee3b1f | ||
|
|
fa72ff8061 | ||
|
|
65f24860e6 | ||
|
|
4881233211 | ||
|
|
bc01fdf362 | ||
|
|
709998ff9c | ||
|
|
6adeadeed4 | ||
|
|
7b70a39b23 | ||
|
|
873a3c0e60 | ||
|
|
5b88efcfbe | ||
|
|
f37c659c89 | ||
|
|
9fff7bcfca | ||
|
|
b4b0ad9bba | ||
|
|
6d8fdf62f1 | ||
|
|
9f53d7421d | ||
|
|
80c8ecb4cf | ||
|
|
d10059a82d | ||
|
|
737f83cd5f |
No files matched your search
@@ -1,10 +1,10 @@
|
|||||||
# <img src="./frontend/public/appicon.png" alt="VoidRaft Logo" width="32" height="32" style="vertical-align: middle;"> VoidRaft
|
# <img src="./frontend/public/appicon.png" alt="voidraft Logo" width="32" height="32" style="vertical-align: middle;"> voidraft
|
||||||
|
|
||||||
[中文](README_ZH.md) | **English**
|
[中文](README_ZH.md) | **English**
|
||||||
|
|
||||||
> *An elegant text snippet recording tool designed for developers.*
|
> *An elegant text snippet recording tool designed for developers.*
|
||||||
|
|
||||||
VoidRaft is a modern developer-focused text editor that allows you to record, organize, and manage various text snippets anytime, anywhere. Whether it's temporary code snippets, API responses, meeting notes, or daily to-do lists, VoidRaft provides a smooth and elegant editing experience.
|
voidraft is a modern developer-focused text editor that allows you to record, organize, and manage various text snippets anytime, anywhere. Whether it's temporary code snippets, API responses, meeting notes, or daily to-do lists, voidraft provides a smooth and elegant editing experience.
|
||||||
|
|
||||||
## Core Features
|
## Core Features
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ After building, the executable will be generated in the `bin` directory.
|
|||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
Voidraft/
|
voidraft/
|
||||||
├── frontend/ # Vue 3 frontend application
|
├── frontend/ # Vue 3 frontend application
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── views/editor/ # Editor core views
|
│ │ ├── views/editor/ # Editor core views
|
||||||
@@ -121,19 +121,19 @@ Voidraft/
|
|||||||
### Planned Features
|
### Planned Features
|
||||||
- ✅ Custom themes - Customize editor themes
|
- ✅ Custom themes - Customize editor themes
|
||||||
- ✅ Multi-window support - Support editing multiple documents simultaneously
|
- ✅ Multi-window support - Support editing multiple documents simultaneously
|
||||||
|
- ✅ Data synchronization - Cloud backup for documents
|
||||||
- [ ] Enhanced clipboard - Monitor and manage clipboard history
|
- [ ] Enhanced clipboard - Monitor and manage clipboard history
|
||||||
- [ ] Data synchronization - Cloud backup for configurations and documents
|
|
||||||
- [ ] Extension system - Support for custom plugins
|
- [ ] Extension system - Support for custom plugins
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
> Standing on the shoulders of giants, paying tribute to the open source spirit
|
> Standing on the shoulders of giants, paying tribute to the open source spirit
|
||||||
|
|
||||||
The birth of VoidRaft is inseparable from the following excellent open source projects:
|
The birth of voidraft is inseparable from the following excellent open source projects:
|
||||||
|
|
||||||
### Special Thanks
|
### Special Thanks
|
||||||
|
|
||||||
- **[Heynote](https://github.com/heyman/heynote/)** - VoidRaft is a feature-enhanced version based on Heynote's concept
|
- **[Heynote](https://github.com/heyman/heynote/)** - voidraft is a feature-enhanced version based on Heynote's concept
|
||||||
- Inherits Heynote's elegant block editing philosophy
|
- Inherits Heynote's elegant block editing philosophy
|
||||||
- Adds more practical features on the original foundation
|
- Adds more practical features on the original foundation
|
||||||
- Rebuilt with modern technology stack
|
- Rebuilt with modern technology stack
|
||||||
@@ -157,7 +157,7 @@ This project is open source under the [MIT License](LICENSE).
|
|||||||
Welcome to Fork, Star, and contribute code.
|
Welcome to Fork, Star, and contribute code.
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://github.com/yourusername/Voidraft)
|
[](https://github.com/yourusername/voidraft)
|
||||||
[](https://github.com/yourusername/Voidraft)
|
[](https://github.com/yourusername/voidraft)
|
||||||
|
|
||||||
*Made with ❤️ by landaiqing*
|
*Made with ❤️ by landaiqing*
|
||||||
+8
-8
@@ -1,10 +1,10 @@
|
|||||||
# <img src="./frontend/public/appicon.png" alt="Voidraft Logo" width="32" height="32" style="vertical-align: middle;"> Voidraft
|
# <img src="./frontend/public/appicon.png" alt="voidraft Logo" width="32" height="32" style="vertical-align: middle;"> voidraft
|
||||||
|
|
||||||
**中文** | [English](README.md)
|
**中文** | [English](README.md)
|
||||||
|
|
||||||
> *一个专为开发者打造的优雅文本片段记录工具。*
|
> *一个专为开发者打造的优雅文本片段记录工具。*
|
||||||
|
|
||||||
Voidraft 是一个现代化的开发者专用文本编辑器,让你能够随时随地记录、整理和管理各种文本片段。无论是临时的代码片段、API 响应、会议笔记,还是日常的待办事项,Voidraft 都能为你提供流畅而优雅的编辑体验。
|
voidraft 是一个现代化的开发者专用文本编辑器,让你能够随时随地记录、整理和管理各种文本片段。无论是临时的代码片段、API 响应、会议笔记,还是日常的待办事项,voidraft 都能为你提供流畅而优雅的编辑体验。
|
||||||
|
|
||||||
## 核心特性
|
## 核心特性
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ wails3 package
|
|||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
Voidraft/
|
voidraft/
|
||||||
├── frontend/ # Vue 3 前端应用
|
├── frontend/ # Vue 3 前端应用
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── views/editor/ # 编辑器核心视图
|
│ │ ├── views/editor/ # 编辑器核心视图
|
||||||
@@ -122,8 +122,8 @@ Voidraft/
|
|||||||
### 计划添加的功能
|
### 计划添加的功能
|
||||||
- ✅ 自定义主题 - 自定义编辑器主题
|
- ✅ 自定义主题 - 自定义编辑器主题
|
||||||
- ✅ 多窗口支持 - 支持同时编辑多个文档
|
- ✅ 多窗口支持 - 支持同时编辑多个文档
|
||||||
|
- ✅ 数据同步 - 文档云端备份
|
||||||
- [ ] 剪切板增强 - 监听和管理剪切板历史
|
- [ ] 剪切板增强 - 监听和管理剪切板历史
|
||||||
- [ ] 数据同步 - 配置和文档云端备份
|
|
||||||
- [ ] 扩展系统 - 支持自定义插件
|
- [ ] 扩展系统 - 支持自定义插件
|
||||||
|
|
||||||
|
|
||||||
@@ -131,11 +131,11 @@ Voidraft/
|
|||||||
|
|
||||||
> 站在巨人的肩膀上,致敬开源精神
|
> 站在巨人的肩膀上,致敬开源精神
|
||||||
|
|
||||||
Voidraft 的诞生离不开以下优秀的开源项目:
|
voidraft 的诞生离不开以下优秀的开源项目:
|
||||||
|
|
||||||
### 特别感谢
|
### 特别感谢
|
||||||
|
|
||||||
- **[Heynote](https://github.com/heyman/heynote/)** - Voidraft 是基于 Heynote 概念的功能增强版本
|
- **[Heynote](https://github.com/heyman/heynote/)** - voidraft 是基于 Heynote 概念的功能增强版本
|
||||||
- 继承了 Heynote 优雅的块状编辑理念
|
- 继承了 Heynote 优雅的块状编辑理念
|
||||||
- 在原有基础上增加了更多实用功能
|
- 在原有基础上增加了更多实用功能
|
||||||
- 采用现代化技术栈重新构建
|
- 采用现代化技术栈重新构建
|
||||||
@@ -159,7 +159,7 @@ Voidraft 的诞生离不开以下优秀的开源项目:
|
|||||||
欢迎 Fork、Star 和贡献代码。
|
欢迎 Fork、Star 和贡献代码。
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://github.com/yourusername/Voidraft)
|
[](https://github.com/yourusername/voidraft)
|
||||||
[](https://github.com/yourusername/Voidraft)
|
[](https://github.com/yourusername/voidraft)
|
||||||
|
|
||||||
*Made with ❤️ by landaiqing*
|
*Made with ❤️ by landaiqing*
|
||||||
@@ -12,13 +12,25 @@ vars:
|
|||||||
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
|
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
version:
|
||||||
|
summary: Generate version information
|
||||||
|
cmds:
|
||||||
|
- '{{if eq OS "windows"}}cmd /c ".\scripts\version.bat"{{else}}bash ./scripts/version.sh{{end}}'
|
||||||
|
sources:
|
||||||
|
- scripts/version.bat
|
||||||
|
- scripts/version.sh
|
||||||
|
generates:
|
||||||
|
- version.txt
|
||||||
|
|
||||||
build:
|
build:
|
||||||
summary: Builds the application
|
summary: Builds the application
|
||||||
|
deps: [version]
|
||||||
cmds:
|
cmds:
|
||||||
- task: "{{OS}}:build"
|
- task: "{{OS}}:build"
|
||||||
|
|
||||||
package:
|
package:
|
||||||
summary: Packages a production build of the application
|
summary: Packages a production build of the application
|
||||||
|
deps: [version]
|
||||||
cmds:
|
cmds:
|
||||||
- task: "{{OS}}:package"
|
- task: "{{OS}}:package"
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -5,12 +5,12 @@ version: '3'
|
|||||||
|
|
||||||
# This information is used to generate the build assets.
|
# This information is used to generate the build assets.
|
||||||
info:
|
info:
|
||||||
companyName: "Voidraft" # The name of the company
|
companyName: "voidraft" # The name of the company
|
||||||
productName: "Voidraft" # The name of the application
|
productName: "voidraft" # The name of the application
|
||||||
productIdentifier: "landaiqing" # The unique product identifier
|
productIdentifier: "landaiqing" # The unique product identifier
|
||||||
description: "Voidraft" # The application description
|
description: "voidraft" # The application description
|
||||||
copyright: "© 2025 Voidraft. All rights reserved." # Copyright text
|
copyright: "© 2025 voidraft. All rights reserved." # Copyright text
|
||||||
comments: "Voidraft" # Comments
|
comments: "voidraft" # Comments
|
||||||
version: "0.0.1.0" # The application version
|
version: "0.0.1.0" # The application version
|
||||||
|
|
||||||
# Dev mode configuration
|
# Dev mode configuration
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Voidraft</string>
|
<string>voidraft</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>Voidraft</string>
|
<string>voidraft</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>landaiqing</string>
|
<string>landaiqing</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>0.0.1.0</string>
|
<string>0.0.1.0</string>
|
||||||
<key>CFBundleGetInfoString</key>
|
<key>CFBundleGetInfoString</key>
|
||||||
<string>Voidraft</string>
|
<string>voidraft</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.0.1.0</string>
|
<string>0.0.1.0</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<string>true</string>
|
<string>true</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>© 2025 Voidraft. All rights reserved.</string>
|
<string>© 2025 voidraft. All rights reserved.</string>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsLocalNetworking</key>
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Voidraft</string>
|
<string>voidraft</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>Voidraft</string>
|
<string>voidraft</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>landaiqing</string>
|
<string>landaiqing</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>0.0.1.0</string>
|
<string>0.0.1.0</string>
|
||||||
<key>CFBundleGetInfoString</key>
|
<key>CFBundleGetInfoString</key>
|
||||||
<string>Voidraft</string>
|
<string>voidraft</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.0.1.0</string>
|
<string>0.0.1.0</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
@@ -22,6 +22,6 @@
|
|||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
<string>true</string>
|
<string>true</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>© 2025 Voidraft. All rights reserved.</string>
|
<string>© 2025 voidraft. All rights reserved.</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -11,9 +11,12 @@ tasks:
|
|||||||
- task: common:build:frontend
|
- task: common:build:frontend
|
||||||
- task: common:generate:icons
|
- task: common:generate:icons
|
||||||
cmds:
|
cmds:
|
||||||
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
|
- go build {{.BUILD_FLAGS}} -ldflags="{{.LDFLAGS}} {{.VERSION_FLAGS}}" -o {{.OUTPUT}}
|
||||||
vars:
|
vars:
|
||||||
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
|
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
|
||||||
|
LDFLAGS: '{{if eq .PRODUCTION "true"}}-w -s{{else}}{{end}}'
|
||||||
|
VERSION_FLAGS:
|
||||||
|
sh: 'grep "VERSION=" version.txt | cut -d"=" -f2 | xargs -I {} echo "-X voidraft/internal/version.Version={}"'
|
||||||
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
|
||||||
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -11,9 +11,12 @@ tasks:
|
|||||||
- task: common:build:frontend
|
- task: common:build:frontend
|
||||||
- task: common:generate:icons
|
- task: common:generate:icons
|
||||||
cmds:
|
cmds:
|
||||||
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
|
- go build {{.BUILD_FLAGS}} -ldflags="{{.LDFLAGS}} {{.VERSION_FLAGS}}" -o {{.BIN_DIR}}/{{.APP_NAME}}
|
||||||
vars:
|
vars:
|
||||||
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
|
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
|
||||||
|
LDFLAGS: '{{if eq .PRODUCTION "true"}}-w -s{{else}}{{end}}'
|
||||||
|
VERSION_FLAGS:
|
||||||
|
sh: 'grep "VERSION=" version.txt | cut -d"=" -f2 | xargs -I {} echo "-X voidraft/internal/version.Version={}"'
|
||||||
env:
|
env:
|
||||||
GOOS: linux
|
GOOS: linux
|
||||||
CGO_ENABLED: 1
|
CGO_ENABLED: 1
|
||||||
|
|||||||
@@ -3,26 +3,26 @@
|
|||||||
#
|
#
|
||||||
# The lines below are called `modelines`. See `:help modeline`
|
# The lines below are called `modelines`. See `:help modeline`
|
||||||
|
|
||||||
name: "Voidraft"
|
name: "voidraft"
|
||||||
arch: ${GOARCH}
|
arch: ${GOARCH}
|
||||||
platform: "linux"
|
platform: "linux"
|
||||||
version: "0.0.1.0"
|
version: "0.0.1.0"
|
||||||
section: "default"
|
section: "default"
|
||||||
priority: "extra"
|
priority: "extra"
|
||||||
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
|
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
|
||||||
description: "Voidraft"
|
description: "voidraft"
|
||||||
vendor: "Voidraft"
|
vendor: "voidraft"
|
||||||
homepage: "https://wails.io"
|
homepage: "https://voidraft.landaiqing.cn"
|
||||||
license: "MIT"
|
license: "MIT"
|
||||||
release: "1"
|
release: "1"
|
||||||
|
|
||||||
contents:
|
contents:
|
||||||
- src: "./bin/Voidraft"
|
- src: "./bin/voidraft"
|
||||||
dst: "/usr/local/bin/Voidraft"
|
dst: "/usr/local/bin/voidraft"
|
||||||
- src: "./build/appicon.png"
|
- src: "./build/appicon.png"
|
||||||
dst: "/usr/share/icons/hicolor/128x128/apps/Voidraft.png"
|
dst: "/usr/share/icons/hicolor/128x128/apps/voidraft.png"
|
||||||
- src: "./build/linux/Voidraft.desktop"
|
- src: "./build/linux/voidraft.desktop"
|
||||||
dst: "/usr/share/applications/Voidraft.desktop"
|
dst: "/usr/share/applications/voidraft.desktop"
|
||||||
|
|
||||||
depends:
|
depends:
|
||||||
- gtk3
|
- gtk3
|
||||||
|
|||||||
@@ -14,13 +14,16 @@ tasks:
|
|||||||
- task: common:generate:icons
|
- task: common:generate:icons
|
||||||
cmds:
|
cmds:
|
||||||
- task: generate:syso
|
- task: generate:syso
|
||||||
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
|
- go build {{.BUILD_FLAGS}} -ldflags="{{.LDFLAGS}} {{.VERSION_FLAGS}}" -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
|
||||||
- cmd: powershell Remove-item *.syso
|
- cmd: powershell Remove-item *.syso
|
||||||
platforms: [windows]
|
platforms: [windows]
|
||||||
- cmd: rm -f *.syso
|
- cmd: rm -f *.syso
|
||||||
platforms: [linux, darwin]
|
platforms: [linux, darwin]
|
||||||
vars:
|
vars:
|
||||||
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
|
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
|
||||||
|
LDFLAGS: '{{if eq .PRODUCTION "true"}}-w -s -H windowsgui{{else}}{{end}}'
|
||||||
|
VERSION_FLAGS:
|
||||||
|
sh: 'powershell -Command "(Get-Content version.txt) -replace ''VERSION='', ''-X voidraft/internal/version.Version=''"'
|
||||||
env:
|
env:
|
||||||
GOOS: windows
|
GOOS: windows
|
||||||
CGO_ENABLED: 1
|
CGO_ENABLED: 1
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"0000": {
|
"0000": {
|
||||||
"ProductVersion": "0.0.1.0",
|
"ProductVersion": "0.0.1.0",
|
||||||
"CompanyName": "Voidraft",
|
"CompanyName": "voidraft",
|
||||||
"FileDescription": "Voidraft",
|
"FileDescription": "voidraft",
|
||||||
"LegalCopyright": "© 2025 Voidraft. All rights reserved.",
|
"LegalCopyright": "© 2025 voidraft. All rights reserved.",
|
||||||
"ProductName": "Voidraft",
|
"ProductName": "voidraft",
|
||||||
"Comments": "Voidraft"
|
"Comments": "voidraft"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,19 +5,19 @@
|
|||||||
!include "FileFunc.nsh"
|
!include "FileFunc.nsh"
|
||||||
|
|
||||||
!ifndef INFO_PROJECTNAME
|
!ifndef INFO_PROJECTNAME
|
||||||
!define INFO_PROJECTNAME "Voidraft"
|
!define INFO_PROJECTNAME "voidraft"
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_COMPANYNAME
|
!ifndef INFO_COMPANYNAME
|
||||||
!define INFO_COMPANYNAME "Voidraft"
|
!define INFO_COMPANYNAME "voidraft"
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_PRODUCTNAME
|
!ifndef INFO_PRODUCTNAME
|
||||||
!define INFO_PRODUCTNAME "Voidraft"
|
!define INFO_PRODUCTNAME "voidraft"
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_PRODUCTVERSION
|
!ifndef INFO_PRODUCTVERSION
|
||||||
!define INFO_PRODUCTVERSION "0.0.1.0"
|
!define INFO_PRODUCTVERSION "0.0.1.0"
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_COPYRIGHT
|
!ifndef INFO_COPYRIGHT
|
||||||
!define INFO_COPYRIGHT "© 2025 Voidraft. All rights reserved."
|
!define INFO_COPYRIGHT "© 2025 voidraft. All rights reserved."
|
||||||
!endif
|
!endif
|
||||||
!ifndef PRODUCT_EXECUTABLE
|
!ifndef PRODUCT_EXECUTABLE
|
||||||
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
voidraft.landaiqing.cn
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<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">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Space+Mono&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body class="theme-dark">
|
||||||
|
<div class="container">
|
||||||
|
<!-- 主卡片 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<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>
|
||||||
|
</button>
|
||||||
|
<button id="lang-toggle" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-language"></i> 中/EN
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<!-- 导航区域 -->
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="index.html" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-home"></i> <span data-en="Home" data-zh="首页">Home</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/landaiqing/voidraft" class="btn btn-secondary">
|
||||||
|
<i class="fab fa-github"></i> <span data-en="Source Code" data-zh="源代码">Source Code</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载中提示 -->
|
||||||
|
<div id="loading" class="loading-container">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p data-en="Loading releases..." data-zh="正在加载版本信息...">Loading releases...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 更新日志内容 -->
|
||||||
|
<div id="changelog" class="changelog-container">
|
||||||
|
<!-- 通过JavaScript动态填充内容 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误信息 -->
|
||||||
|
<div id="error-message" class="error-container" style="display: none;">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<p data-en="Failed to load release information. Please try again later."
|
||||||
|
data-zh="加载版本信息失败,请稍后再试。">Failed to load release information. Please try again later.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 页脚 -->
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<a href="https://github.com/landaiqing/voidraft/releases" target="_blank" class="footer-link" data-en="Releases" data-zh="版本发布">Releases</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
<script src="js/changelog.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
/* 更新日志页面样式 */
|
||||||
|
.nav-links {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-left-color: var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .loading-spinner {
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-left-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--error-color);
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px dashed var(--error-color);
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: rgba(var(--card-bg-rgb), 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container i {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 更新日志容器 */
|
||||||
|
.changelog-container {
|
||||||
|
display: none;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
border-left: 4px solid var(--primary-color);
|
||||||
|
padding-left: 20px;
|
||||||
|
background-color: rgba(var(--card-bg-rgb), 0.5);
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-version {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-date {
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 10px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-badge.pre-release {
|
||||||
|
background-color: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-description {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-assets {
|
||||||
|
background-color: rgba(var(--light-bg-rgb), 0.7);
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-assets-title {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-list {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid rgba(128, 128, 128, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-name {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-size {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 资源下载按钮 */
|
||||||
|
.download-btn {
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn:hover {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content {
|
||||||
|
line-height: 1.8;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h1,
|
||||||
|
.markdown-content h2,
|
||||||
|
.markdown-content h3 {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ul,
|
||||||
|
.markdown-content ol {
|
||||||
|
padding-left: 20px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content li:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 2px dashed var(--border-color);
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content br {
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content code {
|
||||||
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
|
background-color: rgba(128, 128, 128, 0.1);
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre {
|
||||||
|
background-color: rgba(128, 128, 128, 0.1);
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-source {
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background-color: rgba(var(--light-bg-rgb), 0.7);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: right;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-source a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-source a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown内容样式增强 */
|
||||||
|
.markdown-content blockquote {
|
||||||
|
border-left: 4px solid var(--primary-color);
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
background-color: rgba(var(--light-bg-rgb), 0.5);
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ul,
|
||||||
|
.markdown-content ol {
|
||||||
|
padding-left: 20px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动设备响应式优化 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.release-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-assets {
|
||||||
|
padding: 12px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-item {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 12px 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-name {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-size {
|
||||||
|
margin-left: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.release {
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-icon {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-size {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保日志页面页脚样式一致 */
|
||||||
|
.footer {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-text {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iIq129k.woff2) format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1isq129k.woff2) format('woff2');
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iAq129k.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iEq129k.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'IBM Plex Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1i8q1w.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYE58RWq7.woff2) format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYE98RWq7.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYEF8RQ.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
@@ -0,0 +1,717 @@
|
|||||||
|
@import url('./space-mono-font.css');
|
||||||
|
@import url('./ibm-plex-mono-font.css');
|
||||||
|
|
||||||
|
/* 浅色主题 */
|
||||||
|
:root {
|
||||||
|
--bg-color: #fefefe;
|
||||||
|
--text-color: #000000;
|
||||||
|
--primary-color: #F08080;
|
||||||
|
--primary-color-rgb: 240, 128, 128;
|
||||||
|
--secondary-color: #ff006e;
|
||||||
|
--accent-color: #073B4C;
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--card-bg-rgb: 255, 255, 255;
|
||||||
|
--border-color: #000000;
|
||||||
|
--light-bg: #f0f0f0;
|
||||||
|
--light-bg-rgb: 240, 240, 240;
|
||||||
|
--shadow-color: rgba(240, 128, 128, 0.5);
|
||||||
|
--success-color: #27c93f;
|
||||||
|
--warning-color: #FFD166;
|
||||||
|
--error-color: #ff006e;
|
||||||
|
--info-color: #118ab2;
|
||||||
|
--code-bg: #ffffff;
|
||||||
|
--code-bg-rgb: 255, 255, 255;
|
||||||
|
--preview-header-bg: #f0f0f0;
|
||||||
|
--preview-header-bg-rgb: 240, 240, 240;
|
||||||
|
--grid-color-1: rgba(0, 0, 0, 0.08);
|
||||||
|
--grid-color-2: rgba(0, 0, 0, 0.05);
|
||||||
|
--header-title-color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题变量 */
|
||||||
|
.theme-dark {
|
||||||
|
--bg-color: #121212;
|
||||||
|
--text-color: #ffffff;
|
||||||
|
--primary-color: #F08080;
|
||||||
|
--primary-color-rgb: 240, 128, 128;
|
||||||
|
--secondary-color: #ff006e;
|
||||||
|
--accent-color: #118ab2;
|
||||||
|
--card-bg: #1e1e1e;
|
||||||
|
--card-bg-rgb: 30, 30, 30;
|
||||||
|
--border-color: #ffffff;
|
||||||
|
--light-bg: #2a2a2a;
|
||||||
|
--light-bg-rgb: 42, 42, 42;
|
||||||
|
--shadow-color: rgba(240, 128, 128, 0.5);
|
||||||
|
--success-color: #27c93f;
|
||||||
|
--warning-color: #FFD166;
|
||||||
|
--error-color: #ff006e;
|
||||||
|
--info-color: #118ab2;
|
||||||
|
--code-bg: #1e1e1e;
|
||||||
|
--code-bg-rgb: 30, 30, 30;
|
||||||
|
--preview-header-bg: #252526;
|
||||||
|
--preview-header-bg-rgb: 37, 37, 38;
|
||||||
|
--grid-color-1: rgba(255, 255, 255, 0.08);
|
||||||
|
--grid-color-2: rgba(255, 255, 255, 0.05);
|
||||||
|
--header-title-color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主题切换和语言切换的过渡效果 */
|
||||||
|
.theme-transition,
|
||||||
|
.theme-transition *,
|
||||||
|
.lang-transition,
|
||||||
|
.lang-transition * {
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gridMove {
|
||||||
|
0% {
|
||||||
|
background-position: 0px 0px, 0px 0px, 0px 0px, 0px 0px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
background-image:
|
||||||
|
linear-gradient(var(--grid-color-1) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, var(--grid-color-1) 1px, transparent 1px),
|
||||||
|
linear-gradient(var(--grid-color-2) 0.5px, transparent 0.5px),
|
||||||
|
linear-gradient(90deg, var(--grid-color-2) 0.5px, transparent 0.5px);
|
||||||
|
background-size: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
|
||||||
|
background-position: center;
|
||||||
|
animation: gridMove 40s linear infinite;
|
||||||
|
font-family: 'Space Mono', monospace;
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 20px;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片容器 */
|
||||||
|
.card {
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
background-image:
|
||||||
|
linear-gradient(var(--grid-color-1) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, var(--grid-color-1) 1px, transparent 1px),
|
||||||
|
linear-gradient(var(--grid-color-2) 0.5px, transparent 0.5px),
|
||||||
|
linear-gradient(90deg, var(--grid-color-2) 0.5px, transparent 0.5px);
|
||||||
|
background-size: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
|
||||||
|
background-position: center;
|
||||||
|
border: 4px solid var(--border-color);
|
||||||
|
box-shadow: 12px 12px 0 var(--shadow-color);
|
||||||
|
margin-bottom: 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 16px 16px 0 var(--shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片头部 */
|
||||||
|
.card-header {
|
||||||
|
background-color: rgba(var(--primary-color-rgb), 0.9);
|
||||||
|
border-bottom: 4px solid var(--border-color);
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--header-title-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: var(--secondary-color);
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 3px solid var(--border-color);
|
||||||
|
box-shadow: 4px 4px 0 var(--shadow-color);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Space Mono', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: var(--primary-color);
|
||||||
|
border: 3px solid var(--primary-color);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--light-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: var(--primary-color);
|
||||||
|
border: 3px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片内容 */
|
||||||
|
.card-content {
|
||||||
|
padding: 30px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
background-color: rgba(var(--card-bg-rgb), 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo区域 */
|
||||||
|
.logo-container {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-frame {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 4px solid var(--border-color);
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-image {
|
||||||
|
width: 130px;
|
||||||
|
height: 130px;
|
||||||
|
object-fit: contain;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 介绍区域 */
|
||||||
|
.intro-box {
|
||||||
|
border: 2px dashed var(--border-color);
|
||||||
|
padding: 20px;
|
||||||
|
background-color: rgba(var(--light-bg-rgb), 0.7);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-text {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮组 */
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 特性网格 */
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
margin: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 特性卡片 */
|
||||||
|
.feature-card {
|
||||||
|
background-color: rgba(var(--card-bg-rgb), 0.8);
|
||||||
|
border: 3px solid var(--border-color);
|
||||||
|
box-shadow: 5px 5px 0 var(--shadow-color);
|
||||||
|
padding: 20px;
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 7px 7px 0 var(--shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预览区域 */
|
||||||
|
.preview-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.preview-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预览窗口 */
|
||||||
|
.preview-window {
|
||||||
|
border: 3px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 10px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 300px;
|
||||||
|
background-color: rgba(var(--card-bg-rgb), 0.7);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 5px 5px 0 var(--shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预览头部 */
|
||||||
|
.preview-header {
|
||||||
|
background-color: rgba(var(--preview-header-bg-rgb), 0.9);
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-btn {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 0.5px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-btn:nth-child(1) {
|
||||||
|
background-color: #ff5f56;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-btn:nth-child(2) {
|
||||||
|
background-color: #ffbd2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-btn:nth-child(3) {
|
||||||
|
background-color: #27c93f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
font-size: 13px;
|
||||||
|
opacity: 0.8;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预览内容 */
|
||||||
|
.preview-content {
|
||||||
|
padding: 15px;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(var(--code-bg-rgb), 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 代码块容器 */
|
||||||
|
.code-block-wrapper {
|
||||||
|
background-color: rgba(var(--code-bg-rgb), 0.8);
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 块头部 */
|
||||||
|
.block-header {
|
||||||
|
background-color: rgba(var(--light-bg-rgb), 0.8);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-language {
|
||||||
|
color: rgba(128, 128, 128, 0.8);
|
||||||
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-language::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-right: 5px;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23888'%3E%3Cpath d='M9.7,16.7L5.3,12.3C4.9,11.9 4.9,11.1 5.3,10.7C5.7,10.3 6.3,10.3 6.7,10.7L10.5,14.5L17.3,7.7C17.7,7.3 18.3,7.3 18.7,7.7C19.1,8.1 19.1,8.7 18.7,9.1L11.3,16.7C10.9,17.1 10.1,17.1 9.7,16.7Z'/%3E%3C/svg%3E");
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
font-family: 'IBM Plex Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre;
|
||||||
|
tab-size: 4;
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .code-block-wrapper {
|
||||||
|
border-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .block-header {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
border-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .block-language {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .block-language::before {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23aaa'%3E%3Cpath d='M9.7,16.7L5.3,12.3C4.9,11.9 4.9,11.1 5.3,10.7C5.7,10.3 6.3,10.3 6.7,10.7L10.5,14.5L17.3,7.7C17.7,7.3 18.3,7.3 18.7,7.7C19.1,8.1 19.1,8.7 18.7,9.1L11.3,16.7C10.9,17.1 10.1,17.1 9.7,16.7Z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .code-block {
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 代码高亮 */
|
||||||
|
.theme-dark .keyword { color: #c586c0; }
|
||||||
|
.theme-dark .function { color: #dcdcaa; }
|
||||||
|
.theme-dark .variable { color: #9cdcfe; }
|
||||||
|
.theme-dark .string { color: #ce9178; }
|
||||||
|
.theme-dark .comment { color: #6a9955; }
|
||||||
|
.theme-dark .class { color: #4ec9b0; }
|
||||||
|
.theme-dark .parameter { color: #9cdcfe; }
|
||||||
|
.theme-dark .built-in { color: #4ec9b0; }
|
||||||
|
|
||||||
|
/* 浅色主题代码高亮 */
|
||||||
|
.keyword { color: #af00db; }
|
||||||
|
.function { color: #795e26; }
|
||||||
|
.variable { color: #001080; }
|
||||||
|
.string { color: #a31515; }
|
||||||
|
.comment { color: #008000; }
|
||||||
|
.class { color: #267f99; }
|
||||||
|
.parameter { color: #001080; }
|
||||||
|
.built-in { color: #267f99; }
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
border: none;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .light-theme-img {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .dark-theme-img {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.theme-dark) .dark-theme-img {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.theme-dark) .light-theme-img {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 技术栈列表 */
|
||||||
|
.tech-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 技术栈列表 */
|
||||||
|
.tech-item {
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
background-color: rgba(var(--light-bg-rgb), 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-icon {
|
||||||
|
margin-right: 15px;
|
||||||
|
color: var(--secondary-color);
|
||||||
|
font-size: 20px;
|
||||||
|
width: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-name {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页脚 */
|
||||||
|
.footer {
|
||||||
|
border-top: 2px solid var(--border-color);
|
||||||
|
padding: 20px 0;
|
||||||
|
margin-top: 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
background-color: transparent;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-text {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
color: var(--secondary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link:hover {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.button-group {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-controls {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-frame {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-image {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 针对移动设备的响应式优化 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-controls {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预览区域优化 */
|
||||||
|
.preview-content {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-header {
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日志界面导航链接优化 */
|
||||||
|
.nav-links {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links .btn {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
/* 特性卡片优化 */
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预览窗口优化 */
|
||||||
|
.preview-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-window {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 技术栈列表小屏幕优化 */
|
||||||
|
.tech-item {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-desc {
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 40px; /* 图标宽度+右边距 */
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日志界面资源列表项优化 */
|
||||||
|
.asset-item {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-name {
|
||||||
|
width: 100%;
|
||||||
|
word-break: break-all;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-size {
|
||||||
|
order: 2;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
order: 3;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页脚链接优化 */
|
||||||
|
.footer {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
margin-top: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
+256
@@ -0,0 +1,256 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<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">
|
||||||
|
<meta name="robots" content="index, follow">
|
||||||
|
<link rel="canonical" href="https://landaiqing.github.io/voidraft/">
|
||||||
|
|
||||||
|
<!-- Internationalization / hreflang -->
|
||||||
|
<link rel="alternate" hreflang="en" href="https://landaiqing.github.io/voidraft/">
|
||||||
|
<link rel="alternate" hreflang="zh" href="https://landaiqing.github.io/voidraft/?lang=zh">
|
||||||
|
<link rel="alternate" hreflang="x-default" href="https://landaiqing.github.io/voidraft/">
|
||||||
|
|
||||||
|
<!-- 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:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
|
||||||
|
<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:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="./css/styles.css">
|
||||||
|
<link rel="icon" href="./img/favicon.ico" type="image/x-icon">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Structured Data -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "SoftwareApplication",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"operatingSystem": ["Windows", "macOS", "Linux"],
|
||||||
|
"applicationCategory": "DeveloperApplication",
|
||||||
|
"offers": {
|
||||||
|
"@type": "Offer",
|
||||||
|
"price": "0",
|
||||||
|
"priceCurrency": "USD"
|
||||||
|
},
|
||||||
|
"screenshot": "https://landaiqing.github.io/voidraft/img/screenshot-dark.png",
|
||||||
|
"softwareVersion": "Latest",
|
||||||
|
"programmingLanguage": ["Go", "TypeScript", "Vue.js"],
|
||||||
|
"codeRepository": "https://github.com/landaiqing/voidraft"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="theme-dark">
|
||||||
|
<div class="container">
|
||||||
|
<!-- 主卡片 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<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>
|
||||||
|
</button>
|
||||||
|
<button id="lang-toggle" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-language"></i> 中/EN
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<!-- Logo和介绍 -->
|
||||||
|
<div class="logo-container">
|
||||||
|
<div class="logo-frame">
|
||||||
|
<img src="img/logo.png" alt="voidraft Logo" class="logo-image">
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="intro-box">
|
||||||
|
<p class="intro-text" data-en="Designed for developers to record, organize, and manage various text snippets anytime, anywhere." data-zh="专为开发者打造,随时随地记录、整理和管理各种文本片段。">Designed for developers to record, organize, and manage various text snippets anytime, anywhere.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-group">
|
||||||
|
<a href="https://github.com/landaiqing/voidraft/releases" class="btn" data-en="Download" data-zh="下载">
|
||||||
|
<i class="fas fa-download"></i> Download
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/landaiqing/voidraft" class="btn btn-secondary" data-en="Source Code" data-zh="源代码">
|
||||||
|
<i class="fab fa-github"></i> Source Code
|
||||||
|
</a>
|
||||||
|
<a href="changelog.html" class="btn btn-secondary" data-en="Changelog" data-zh="更新日志">
|
||||||
|
<i class="fas fa-history"></i> Changelog
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 特性部分 -->
|
||||||
|
<h2 data-en="Core Features" data-zh="核心特性">Core Features</h2>
|
||||||
|
|
||||||
|
<div class="features-grid">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title" data-en="Developer-Friendly" data-zh="开发者友好">Developer-Friendly</h3>
|
||||||
|
<p class="feature-desc" data-en="Multi-language code blocks with syntax highlighting for 30+ programming languages" data-zh="多语言代码块支持,为30+种编程语言提供语法高亮">Multi-language code blocks with syntax highlighting for 30+ programming languages</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<i class="fas fa-magic"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title" data-en="Code Formatting" data-zh="代码格式化">Code Formatting</h3>
|
||||||
|
<p class="feature-desc" data-en="Built-in Prettier support for one-click code beautification" data-zh="内置Prettier支持,一键美化代码">Built-in Prettier support for one-click code beautification</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<i class="fas fa-palette"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title" data-en="Custom Themes" data-zh="自定义主题">Custom Themes</h3>
|
||||||
|
<p class="feature-desc" data-en="Dark/Light themes with full customization options" data-zh="深色/浅色主题,支持完全自定义">Dark/Light themes with full customization options</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<i class="fas fa-clone"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title" data-en="Multi-Window" data-zh="多窗口支持">Multi-Window</h3>
|
||||||
|
<p class="feature-desc" data-en="Edit multiple documents simultaneously" data-zh="同时编辑多个文档">Edit multiple documents simultaneously</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<i class="fas fa-layer-group"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title" data-en="Block Editing" data-zh="块状编辑">Block Editing</h3>
|
||||||
|
<p class="feature-desc" data-en="Split content into independent code blocks with different language settings" data-zh="将内容分割为独立的代码块,每个块可设置不同语言">Split content into independent code blocks with different language settings</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<i class="fas fa-puzzle-piece"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="feature-title" data-en="Extensions" data-zh="丰富扩展">Extensions</h3>
|
||||||
|
<p class="feature-desc" data-en="Rainbow brackets, VSCode-style search, color picker, translation tool, and more" data-zh="彩虹括号、VSCode风格搜索、颜色选择器、翻译工具等多种扩展">Rainbow brackets, VSCode-style search, color picker, translation tool, and more</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 预览部分 -->
|
||||||
|
<h2 data-en="Preview" data-zh="预览">Preview</h2>
|
||||||
|
|
||||||
|
<div class="preview-container">
|
||||||
|
<div class="preview-window">
|
||||||
|
<div class="preview-header">
|
||||||
|
<div class="preview-controls">
|
||||||
|
<span class="preview-btn"></span>
|
||||||
|
<span class="preview-btn"></span>
|
||||||
|
<span class="preview-btn"></span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-title">voidraft</div>
|
||||||
|
</div>
|
||||||
|
<div class="preview-content">
|
||||||
|
<div class="code-block-wrapper">
|
||||||
|
<div class="block-header">
|
||||||
|
<div class="block-language">javascript</div>
|
||||||
|
</div>
|
||||||
|
<pre class="code-block">
|
||||||
|
<span class="keyword">function</span> <span class="function">createDocument</span>() {
|
||||||
|
<span class="keyword">const</span> <span class="variable">doc</span> = <span class="keyword">new</span> <span class="class">Document</span>();
|
||||||
|
|
||||||
|
<span class="variable">doc</span>.<span class="function">addCodeBlock</span>(<span class="string">'javascript'</span>, <span class="string">`
|
||||||
|
<span class="keyword">function</span> <span class="function">greeting</span>(<span class="parameter">name</span>) {
|
||||||
|
<span class="keyword">return</span> <span class="string">`Hello, </span>${<span class="parameter">name</span>}<span class="string">!`</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="built-in">console</span>.<span class="function">log</span>(<span class="function">greeting</span>(<span class="string">'World'</span>));
|
||||||
|
`</span>);
|
||||||
|
|
||||||
|
<span class="keyword">return</span> <span class="variable">doc</span>;
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="code-block-wrapper" style="margin-top: 10px;">
|
||||||
|
<div class="block-header">
|
||||||
|
<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">// 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;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 技术栈部分 -->
|
||||||
|
<h2 data-en="Technical Stack" data-zh="技术栈">Technical Stack</h2>
|
||||||
|
|
||||||
|
<ul class="tech-list">
|
||||||
|
<li class="tech-item">
|
||||||
|
<div class="tech-icon"><i class="fas fa-desktop"></i></div>
|
||||||
|
<span class="tech-name">Wails3</span>
|
||||||
|
<span class="tech-desc" data-en="Cross-platform desktop application framework" data-zh="跨平台桌面应用框架">Cross-platform desktop application framework</span>
|
||||||
|
</li>
|
||||||
|
<li class="tech-item">
|
||||||
|
<div class="tech-icon"><i class="fas fa-cogs"></i></div>
|
||||||
|
<span class="tech-name">Go 1.21+</span>
|
||||||
|
<span class="tech-desc" data-en="Fast and efficient backend language" data-zh="快速高效的后端语言">Fast and efficient backend language</span>
|
||||||
|
</li>
|
||||||
|
<li class="tech-item">
|
||||||
|
<div class="tech-icon"><i class="fab fa-vuejs"></i></div>
|
||||||
|
<span class="tech-name">Vue 3 + TypeScript</span>
|
||||||
|
<span class="tech-desc" data-en="Modern frontend framework" data-zh="现代化前端框架">Modern frontend framework</span>
|
||||||
|
</li>
|
||||||
|
<li class="tech-item">
|
||||||
|
<div class="tech-icon"><i class="fas fa-edit"></i></div>
|
||||||
|
<span class="tech-name">CodeMirror 6</span>
|
||||||
|
<span class="tech-desc" data-en="Modern code editor with extension support" data-zh="支持扩展的现代化代码编辑器">Modern code editor with extension support</span>
|
||||||
|
</li>
|
||||||
|
<li class="tech-item">
|
||||||
|
<div class="tech-icon"><i class="fas fa-database"></i></div>
|
||||||
|
<span class="tech-name">SQLite</span>
|
||||||
|
<span class="tech-desc" data-en="Lightweight database for document storage" data-zh="轻量级文档存储数据库">Lightweight database for document storage</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 页脚 -->
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<a href="https://github.com/landaiqing/voidraft/releases" target="_blank" class="footer-link" data-en="Releases" data-zh="版本发布">Releases</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,705 @@
|
|||||||
|
/**
|
||||||
|
* voidraft - Changelog Script
|
||||||
|
* 从GitHub API获取发布信息,支持Gitea备用源
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仓库配置类
|
||||||
|
*/
|
||||||
|
class RepositoryConfig {
|
||||||
|
constructor() {
|
||||||
|
this.repos = {
|
||||||
|
github: {
|
||||||
|
owner: 'landaiqing',
|
||||||
|
name: 'voidraft',
|
||||||
|
apiUrl: 'https://api.github.com/repos/landaiqing/voidraft/releases',
|
||||||
|
releasesUrl: 'https://github.com/landaiqing/voidraft/releases'
|
||||||
|
},
|
||||||
|
gitea: {
|
||||||
|
owner: 'landaiqing',
|
||||||
|
name: 'voidraft',
|
||||||
|
domain: 'git.landaiqing.cn',
|
||||||
|
apiUrl: 'https://git.landaiqing.cn/api/v1/repos/landaiqing/voidraft/releases',
|
||||||
|
releasesUrl: 'https://git.landaiqing.cn/landaiqing/voidraft/releases'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取仓库配置
|
||||||
|
* @param {string} source - 'github' 或 'gitea'
|
||||||
|
*/
|
||||||
|
getRepo(source) {
|
||||||
|
return this.repos[source];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有仓库配置
|
||||||
|
*/
|
||||||
|
getAllRepos() {
|
||||||
|
return this.repos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 国际化消息管理类
|
||||||
|
*/
|
||||||
|
class I18nMessages {
|
||||||
|
constructor() {
|
||||||
|
this.messages = {
|
||||||
|
loading: {
|
||||||
|
en: 'Loading releases...',
|
||||||
|
zh: '正在加载版本信息...'
|
||||||
|
},
|
||||||
|
noReleases: {
|
||||||
|
en: 'No release information found',
|
||||||
|
zh: '没有找到版本发布信息'
|
||||||
|
},
|
||||||
|
fetchError: {
|
||||||
|
en: 'Failed to load release information. Please try again later.',
|
||||||
|
zh: '无法获取版本信息,请稍后再试'
|
||||||
|
},
|
||||||
|
githubApiError: {
|
||||||
|
en: 'GitHub API returned an error status: ',
|
||||||
|
zh: 'GitHub API返回错误状态: '
|
||||||
|
},
|
||||||
|
giteaApiError: {
|
||||||
|
en: 'Gitea API returned an error status: ',
|
||||||
|
zh: 'Gitea API返回错误状态: '
|
||||||
|
},
|
||||||
|
dataSource: {
|
||||||
|
en: 'Data source: ',
|
||||||
|
zh: '数据来源: '
|
||||||
|
},
|
||||||
|
downloads: {
|
||||||
|
en: 'Downloads',
|
||||||
|
zh: '下载资源'
|
||||||
|
},
|
||||||
|
download: {
|
||||||
|
en: 'Download',
|
||||||
|
zh: '下载'
|
||||||
|
},
|
||||||
|
preRelease: {
|
||||||
|
en: 'Pre-release',
|
||||||
|
zh: '预发布'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息
|
||||||
|
* @param {string} key - 消息键
|
||||||
|
* @param {string} lang - 语言代码
|
||||||
|
*/
|
||||||
|
getMessage(key, lang = 'en') {
|
||||||
|
return this.messages[key] && this.messages[key][lang] || this.messages[key]['en'] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前语言
|
||||||
|
*/
|
||||||
|
getCurrentLang() {
|
||||||
|
return window.currentLang || 'en';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API客户端类
|
||||||
|
*/
|
||||||
|
class APIClient {
|
||||||
|
constructor(repositoryConfig, i18nMessages) {
|
||||||
|
this.repositoryConfig = repositoryConfig;
|
||||||
|
this.i18nMessages = i18nMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从指定源获取发布信息
|
||||||
|
* @param {string} source - 'github' 或 'gitea'
|
||||||
|
*/
|
||||||
|
async fetchReleases(source) {
|
||||||
|
const repo = this.repositoryConfig.getRepo(source);
|
||||||
|
const errorMessageKey = source === 'github' ? 'githubApiError' : 'giteaApiError';
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headers: { 'Accept': 'application/json' }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (source === 'github') {
|
||||||
|
return this.fetchFromGitHub(repo, options, errorMessageKey);
|
||||||
|
} else {
|
||||||
|
return this.fetchFromGitea(repo, options, errorMessageKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从GitHub获取数据
|
||||||
|
* @param {Object} repo - 仓库配置
|
||||||
|
* @param {Object} options - 请求选项
|
||||||
|
* @param {string} errorMessageKey - 错误消息键
|
||||||
|
*/
|
||||||
|
async fetchFromGitHub(repo, options, errorMessageKey) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||||
|
|
||||||
|
options.signal = controller.signal;
|
||||||
|
options.headers['Accept'] = 'application/vnd.github.v3+json';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(repo.apiUrl, options);
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`${this.i18nMessages.getMessage(errorMessageKey, this.i18nMessages.getCurrentLang())}${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const releases = await response.json();
|
||||||
|
|
||||||
|
if (!releases || releases.length === 0) {
|
||||||
|
throw new Error(this.i18nMessages.getMessage('noReleases', this.i18nMessages.getCurrentLang()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return releases;
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从Gitea获取数据
|
||||||
|
* @param {Object} repo - 仓库配置
|
||||||
|
* @param {Object} options - 请求选项
|
||||||
|
* @param {string} errorMessageKey - 错误消息键
|
||||||
|
*/
|
||||||
|
async fetchFromGitea(repo, options, errorMessageKey) {
|
||||||
|
const response = await fetch(repo.apiUrl, options);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`${this.i18nMessages.getMessage(errorMessageKey, this.i18nMessages.getCurrentLang())}${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const releases = await response.json();
|
||||||
|
|
||||||
|
if (!releases || releases.length === 0) {
|
||||||
|
throw new Error(this.i18nMessages.getMessage('noReleases', this.i18nMessages.getCurrentLang()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return releases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI管理类
|
||||||
|
*/
|
||||||
|
class UIManager {
|
||||||
|
constructor(i18nMessages) {
|
||||||
|
this.i18nMessages = i18nMessages;
|
||||||
|
this.elements = {
|
||||||
|
loading: document.getElementById('loading'),
|
||||||
|
changelog: document.getElementById('changelog'),
|
||||||
|
error: document.getElementById('error-message')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示加载状态
|
||||||
|
*/
|
||||||
|
showLoading() {
|
||||||
|
this.elements.loading.style.display = 'block';
|
||||||
|
this.elements.error.style.display = 'none';
|
||||||
|
this.elements.changelog.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏加载状态
|
||||||
|
*/
|
||||||
|
hideLoading() {
|
||||||
|
this.elements.loading.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示错误消息
|
||||||
|
* @param {string} message - 错误消息
|
||||||
|
*/
|
||||||
|
showError(message) {
|
||||||
|
const errorMessageElement = this.elements.error.querySelector('p');
|
||||||
|
if (errorMessageElement) {
|
||||||
|
errorMessageElement.textContent = message;
|
||||||
|
} else {
|
||||||
|
this.elements.error.textContent = message;
|
||||||
|
}
|
||||||
|
this.elements.error.style.display = 'block';
|
||||||
|
this.hideLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示发布信息
|
||||||
|
* @param {Array} releases - 发布信息数组
|
||||||
|
* @param {string} source - 数据源
|
||||||
|
*/
|
||||||
|
displayReleases(releases, source) {
|
||||||
|
this.hideLoading();
|
||||||
|
|
||||||
|
// 清除现有内容
|
||||||
|
this.elements.changelog.innerHTML = '';
|
||||||
|
|
||||||
|
// 创建数据源元素
|
||||||
|
const sourceElement = this.createSourceElement(source);
|
||||||
|
this.elements.changelog.appendChild(sourceElement);
|
||||||
|
|
||||||
|
// 创建发布信息元素
|
||||||
|
releases.forEach(release => {
|
||||||
|
const releaseElement = this.createReleaseElement(release, source);
|
||||||
|
this.elements.changelog.appendChild(releaseElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.elements.changelog.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建数据源元素
|
||||||
|
* @param {string} source - 数据源
|
||||||
|
*/
|
||||||
|
createSourceElement(source) {
|
||||||
|
const sourceElement = document.createElement('div');
|
||||||
|
sourceElement.className = 'data-source';
|
||||||
|
|
||||||
|
// 创建带有国际化支持的源标签
|
||||||
|
const sourceLabel = document.createElement('span');
|
||||||
|
sourceLabel.setAttribute('data-en', this.i18nMessages.getMessage('dataSource', 'en'));
|
||||||
|
sourceLabel.setAttribute('data-zh', this.i18nMessages.getMessage('dataSource', 'zh'));
|
||||||
|
sourceLabel.textContent = this.i18nMessages.getMessage('dataSource', this.i18nMessages.getCurrentLang());
|
||||||
|
|
||||||
|
// 创建链接
|
||||||
|
const sourceLink = document.createElement('a');
|
||||||
|
const repositoryConfig = new RepositoryConfig();
|
||||||
|
sourceLink.href = repositoryConfig.getRepo(source).releasesUrl;
|
||||||
|
sourceLink.textContent = source === 'github' ? 'GitHub' : 'Gitea';
|
||||||
|
sourceLink.target = '_blank';
|
||||||
|
|
||||||
|
// 组装元素
|
||||||
|
sourceElement.appendChild(sourceLabel);
|
||||||
|
sourceElement.appendChild(sourceLink);
|
||||||
|
|
||||||
|
return sourceElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建发布信息元素
|
||||||
|
* @param {Object} release - 发布信息对象
|
||||||
|
* @param {string} source - 数据源
|
||||||
|
*/
|
||||||
|
createReleaseElement(release, source) {
|
||||||
|
const releaseElement = document.createElement('div');
|
||||||
|
releaseElement.className = 'release';
|
||||||
|
|
||||||
|
// 格式化发布日期
|
||||||
|
const releaseDate = new Date(release.published_at || release.created_at);
|
||||||
|
const formattedDate = DateFormatter.formatDate(releaseDate);
|
||||||
|
|
||||||
|
// 创建头部
|
||||||
|
const headerElement = this.createReleaseHeader(release, formattedDate);
|
||||||
|
releaseElement.appendChild(headerElement);
|
||||||
|
|
||||||
|
// 添加发布说明
|
||||||
|
if (release.body) {
|
||||||
|
const descriptionElement = document.createElement('div');
|
||||||
|
descriptionElement.className = 'release-description markdown-content';
|
||||||
|
descriptionElement.innerHTML = MarkdownParser.parseMarkdown(release.body);
|
||||||
|
releaseElement.appendChild(descriptionElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加下载资源
|
||||||
|
const assets = AssetManager.getAssetsFromRelease(release, source);
|
||||||
|
if (assets && assets.length > 0) {
|
||||||
|
const assetsElement = this.createAssetsElement(assets);
|
||||||
|
releaseElement.appendChild(assetsElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return releaseElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建发布信息头部
|
||||||
|
*/
|
||||||
|
createReleaseHeader(release, formattedDate) {
|
||||||
|
const headerElement = document.createElement('div');
|
||||||
|
headerElement.className = 'release-header';
|
||||||
|
|
||||||
|
// 版本元素
|
||||||
|
const versionElement = document.createElement('div');
|
||||||
|
versionElement.className = 'release-version';
|
||||||
|
|
||||||
|
// 版本文本
|
||||||
|
const versionText = document.createElement('span');
|
||||||
|
versionText.textContent = release.name || release.tag_name;
|
||||||
|
versionElement.appendChild(versionText);
|
||||||
|
|
||||||
|
// 预发布标记
|
||||||
|
if (release.prerelease) {
|
||||||
|
const preReleaseTag = document.createElement('span');
|
||||||
|
preReleaseTag.className = 'release-badge pre-release';
|
||||||
|
preReleaseTag.setAttribute('data-en', this.i18nMessages.getMessage('preRelease', 'en'));
|
||||||
|
preReleaseTag.setAttribute('data-zh', this.i18nMessages.getMessage('preRelease', 'zh'));
|
||||||
|
preReleaseTag.textContent = this.i18nMessages.getMessage('preRelease', this.i18nMessages.getCurrentLang());
|
||||||
|
versionElement.appendChild(preReleaseTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日期元素
|
||||||
|
const dateElement = document.createElement('div');
|
||||||
|
dateElement.className = 'release-date';
|
||||||
|
dateElement.textContent = formattedDate;
|
||||||
|
|
||||||
|
headerElement.appendChild(versionElement);
|
||||||
|
headerElement.appendChild(dateElement);
|
||||||
|
|
||||||
|
return headerElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建资源文件元素
|
||||||
|
* @param {Array} assets - 资源文件数组
|
||||||
|
*/
|
||||||
|
createAssetsElement(assets) {
|
||||||
|
const assetsElement = document.createElement('div');
|
||||||
|
assetsElement.className = 'release-assets';
|
||||||
|
|
||||||
|
// 资源标题
|
||||||
|
const assetsTitle = document.createElement('div');
|
||||||
|
assetsTitle.className = 'release-assets-title';
|
||||||
|
assetsTitle.setAttribute('data-en', this.i18nMessages.getMessage('downloads', 'en'));
|
||||||
|
assetsTitle.setAttribute('data-zh', this.i18nMessages.getMessage('downloads', 'zh'));
|
||||||
|
assetsTitle.textContent = this.i18nMessages.getMessage('downloads', this.i18nMessages.getCurrentLang());
|
||||||
|
|
||||||
|
// 资源列表
|
||||||
|
const assetList = document.createElement('ul');
|
||||||
|
assetList.className = 'asset-list';
|
||||||
|
|
||||||
|
// 添加每个资源
|
||||||
|
assets.forEach(asset => {
|
||||||
|
const assetItem = this.createAssetItem(asset);
|
||||||
|
assetList.appendChild(assetItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
assetsElement.appendChild(assetsTitle);
|
||||||
|
assetsElement.appendChild(assetList);
|
||||||
|
|
||||||
|
return assetsElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建资源文件项
|
||||||
|
* @param {Object} asset - 资源文件对象
|
||||||
|
*/
|
||||||
|
createAssetItem(asset) {
|
||||||
|
const assetItem = document.createElement('li');
|
||||||
|
assetItem.className = 'asset-item';
|
||||||
|
|
||||||
|
// 文件图标
|
||||||
|
const iconElement = document.createElement('i');
|
||||||
|
iconElement.className = `asset-icon fas fa-${FileIconHelper.getFileIcon(asset.name)}`;
|
||||||
|
|
||||||
|
// 文件名
|
||||||
|
const nameElement = document.createElement('span');
|
||||||
|
nameElement.className = 'asset-name';
|
||||||
|
nameElement.textContent = asset.name;
|
||||||
|
|
||||||
|
// 文件大小
|
||||||
|
const sizeElement = document.createElement('span');
|
||||||
|
sizeElement.className = 'asset-size';
|
||||||
|
sizeElement.textContent = FileSizeFormatter.formatFileSize(asset.size);
|
||||||
|
|
||||||
|
// 下载链接
|
||||||
|
const downloadLink = document.createElement('a');
|
||||||
|
downloadLink.className = 'download-btn';
|
||||||
|
downloadLink.href = asset.browser_download_url;
|
||||||
|
downloadLink.target = '_blank';
|
||||||
|
downloadLink.setAttribute('data-en', this.i18nMessages.getMessage('download', 'en'));
|
||||||
|
downloadLink.setAttribute('data-zh', this.i18nMessages.getMessage('download', 'zh'));
|
||||||
|
downloadLink.textContent = this.i18nMessages.getMessage('download', this.i18nMessages.getCurrentLang());
|
||||||
|
|
||||||
|
// 组装资源项
|
||||||
|
assetItem.appendChild(iconElement);
|
||||||
|
assetItem.appendChild(nameElement);
|
||||||
|
assetItem.appendChild(sizeElement);
|
||||||
|
assetItem.appendChild(downloadLink);
|
||||||
|
|
||||||
|
return assetItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源管理器类
|
||||||
|
*/
|
||||||
|
class AssetManager {
|
||||||
|
/**
|
||||||
|
* 从发布信息中获取资源文件
|
||||||
|
* @param {Object} release - 发布信息对象
|
||||||
|
* @param {string} source - 数据源
|
||||||
|
*/
|
||||||
|
static getAssetsFromRelease(release, source) {
|
||||||
|
let assets = [];
|
||||||
|
|
||||||
|
if (source === 'github') {
|
||||||
|
assets = release.assets || [];
|
||||||
|
} else { // Gitea
|
||||||
|
assets = release.assets || [];
|
||||||
|
// 检查Gitea特定的资源结构
|
||||||
|
if (!assets.length && release.attachments) {
|
||||||
|
assets = release.attachments.map(attachment => ({
|
||||||
|
name: attachment.name,
|
||||||
|
size: attachment.size,
|
||||||
|
browser_download_url: attachment.browser_download_url
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return assets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件图标助手类
|
||||||
|
*/
|
||||||
|
class FileIconHelper {
|
||||||
|
/**
|
||||||
|
* 根据文件扩展名获取图标
|
||||||
|
* @param {string} filename - 文件名
|
||||||
|
*/
|
||||||
|
static getFileIcon(filename) {
|
||||||
|
const extension = filename.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
'exe': 'download',
|
||||||
|
'msi': 'download',
|
||||||
|
'dmg': 'download',
|
||||||
|
'pkg': 'download',
|
||||||
|
'deb': 'download',
|
||||||
|
'rpm': 'download',
|
||||||
|
'tar': 'file-archive',
|
||||||
|
'gz': 'file-archive',
|
||||||
|
'zip': 'file-archive',
|
||||||
|
'7z': 'file-archive',
|
||||||
|
'rar': 'file-archive',
|
||||||
|
'pdf': 'file-pdf',
|
||||||
|
'txt': 'file-alt',
|
||||||
|
'md': 'file-alt',
|
||||||
|
'json': 'file-code',
|
||||||
|
'xml': 'file-code',
|
||||||
|
'yml': 'file-code',
|
||||||
|
'yaml': 'file-code'
|
||||||
|
};
|
||||||
|
|
||||||
|
return iconMap[extension] || 'file';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件大小格式化器类
|
||||||
|
*/
|
||||||
|
class FileSizeFormatter {
|
||||||
|
/**
|
||||||
|
* 格式化文件大小
|
||||||
|
* @param {number} bytes - 字节数
|
||||||
|
*/
|
||||||
|
static formatFileSize(bytes) {
|
||||||
|
if (!bytes) return '';
|
||||||
|
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
|
||||||
|
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日期格式化器类
|
||||||
|
*/
|
||||||
|
class DateFormatter {
|
||||||
|
/**
|
||||||
|
* 格式化日期
|
||||||
|
* @param {Date} date - 日期对象
|
||||||
|
*/
|
||||||
|
static formatDate(date) {
|
||||||
|
const options = {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
};
|
||||||
|
|
||||||
|
const lang = window.currentLang || 'en';
|
||||||
|
const locale = lang === 'zh' ? 'zh-CN' : 'en-US';
|
||||||
|
|
||||||
|
return date.toLocaleDateString(locale, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markdown解析器类
|
||||||
|
*/
|
||||||
|
class MarkdownParser {
|
||||||
|
/**
|
||||||
|
* 简单的Markdown解析
|
||||||
|
* @param {string} markdown - Markdown文本
|
||||||
|
*/
|
||||||
|
static parseMarkdown(markdown) {
|
||||||
|
if (!markdown) return '';
|
||||||
|
|
||||||
|
// 预处理:保留原始换行符,用特殊标记替换
|
||||||
|
const preservedLineBreaks = '___LINE_BREAK___';
|
||||||
|
markdown = markdown.replace(/\n/g, preservedLineBreaks);
|
||||||
|
|
||||||
|
// 引用块 - > text
|
||||||
|
markdown = markdown.replace(/>\s*(.*?)(?=>|$)/g, '<blockquote>$1</blockquote>');
|
||||||
|
markdown = markdown.replace(/>\s*(.*?)(?=>|$)/g, '<blockquote>$1</blockquote>');
|
||||||
|
|
||||||
|
// 链接 - [text](url)
|
||||||
|
markdown = markdown.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||||
|
|
||||||
|
// 标题 - # Heading
|
||||||
|
markdown = markdown.replace(/^### (.*?)(?=___LINE_BREAK___|$)/gm, '<h3>$1</h3>');
|
||||||
|
markdown = markdown.replace(/^## (.*?)(?=___LINE_BREAK___|$)/gm, '<h2>$1</h2>');
|
||||||
|
markdown = markdown.replace(/^# (.*?)(?=___LINE_BREAK___|$)/gm, '<h1>$1</h1>');
|
||||||
|
|
||||||
|
// 粗体 - **text**
|
||||||
|
markdown = markdown.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||||
|
|
||||||
|
// 斜体 - *text*
|
||||||
|
markdown = markdown.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
||||||
|
|
||||||
|
// 代码块 - ```code```
|
||||||
|
markdown = markdown.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
||||||
|
|
||||||
|
// 行内代码 - `code`
|
||||||
|
markdown = markdown.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||||
|
|
||||||
|
// 处理列表项
|
||||||
|
// 先将每个列表项转换为HTML
|
||||||
|
markdown = markdown.replace(/- (.*?)(?=___LINE_BREAK___- |___LINE_BREAK___$|$)/g, '<li>$1</li>');
|
||||||
|
markdown = markdown.replace(/\* (.*?)(?=___LINE_BREAK___\* |___LINE_BREAK___$|$)/g, '<li>$1</li>');
|
||||||
|
markdown = markdown.replace(/\d+\. (.*?)(?=___LINE_BREAK___\d+\. |___LINE_BREAK___$|$)/g, '<li>$1</li>');
|
||||||
|
|
||||||
|
// 然后将连续的列表项包装在ul或ol中
|
||||||
|
const listItemRegex = /<li>.*?<\/li>/g;
|
||||||
|
const listItems = markdown.match(listItemRegex) || [];
|
||||||
|
|
||||||
|
if (listItems.length > 0) {
|
||||||
|
// 将连续的列表项组合在一起
|
||||||
|
let lastIndex = 0;
|
||||||
|
let result = '';
|
||||||
|
let inList = false;
|
||||||
|
|
||||||
|
listItems.forEach(item => {
|
||||||
|
const itemIndex = markdown.indexOf(item, lastIndex);
|
||||||
|
|
||||||
|
// 添加列表项之前的内容
|
||||||
|
if (itemIndex > lastIndex) {
|
||||||
|
result += markdown.substring(lastIndex, itemIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不在列表中,开始一个新列表
|
||||||
|
if (!inList) {
|
||||||
|
result += '<ul>';
|
||||||
|
inList = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加列表项
|
||||||
|
result += item;
|
||||||
|
|
||||||
|
// 更新lastIndex
|
||||||
|
lastIndex = itemIndex + item.length;
|
||||||
|
|
||||||
|
// 检查下一个内容是否是列表项
|
||||||
|
const nextItemIndex = markdown.indexOf('<li>', lastIndex);
|
||||||
|
if (nextItemIndex === -1 || nextItemIndex > lastIndex + 20) { // 如果下一个列表项不紧邻
|
||||||
|
result += '</ul>';
|
||||||
|
inList = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加剩余内容
|
||||||
|
if (lastIndex < markdown.length) {
|
||||||
|
result += markdown.substring(lastIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
markdown = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理水平分隔线
|
||||||
|
markdown = markdown.replace(/---/g, '<hr>');
|
||||||
|
|
||||||
|
// 恢复换行符
|
||||||
|
markdown = markdown.replace(/___LINE_BREAK___/g, '<br>');
|
||||||
|
|
||||||
|
// 处理段落
|
||||||
|
markdown = markdown.replace(/<br><br>/g, '</p><p>');
|
||||||
|
|
||||||
|
// 包装在段落标签中
|
||||||
|
if (!markdown.startsWith('<p>')) {
|
||||||
|
markdown = `<p>${markdown}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新日志主应用类
|
||||||
|
*/
|
||||||
|
class ChangelogApp {
|
||||||
|
constructor() {
|
||||||
|
this.repositoryConfig = new RepositoryConfig();
|
||||||
|
this.i18nMessages = new I18nMessages();
|
||||||
|
this.apiClient = new APIClient(this.repositoryConfig, this.i18nMessages);
|
||||||
|
this.uiManager = new UIManager(this.i18nMessages);
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化应用
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.uiManager.showLoading();
|
||||||
|
|
||||||
|
// 首先尝试GitHub API
|
||||||
|
this.apiClient.fetchReleases('github')
|
||||||
|
.then(releases => {
|
||||||
|
this.uiManager.displayReleases(releases, 'github');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// GitHub失败时尝试Gitea
|
||||||
|
return this.apiClient.fetchReleases('gitea')
|
||||||
|
.then(releases => {
|
||||||
|
this.uiManager.displayReleases(releases, 'gitea');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('获取发布信息失败:', error);
|
||||||
|
this.uiManager.showError(this.i18nMessages.getMessage('fetchError', this.i18nMessages.getCurrentLang()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听语言变化事件
|
||||||
|
document.addEventListener('languageChanged', () => this.updateUI());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新UI元素(当语言变化时)
|
||||||
|
*/
|
||||||
|
updateUI() {
|
||||||
|
const elementsToUpdate = document.querySelectorAll('[data-en][data-zh]');
|
||||||
|
const currentLang = this.i18nMessages.getCurrentLang();
|
||||||
|
|
||||||
|
elementsToUpdate.forEach(element => {
|
||||||
|
const text = element.getAttribute(`data-${currentLang}`);
|
||||||
|
if (text) {
|
||||||
|
element.textContent = text;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当DOM加载完成时初始化应用
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
new ChangelogApp();
|
||||||
|
});
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
/**
|
||||||
|
* voidraft - Website Script
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题管理类
|
||||||
|
*/
|
||||||
|
class ThemeManager {
|
||||||
|
constructor() {
|
||||||
|
this.themeToggle = document.getElementById('theme-toggle');
|
||||||
|
this.currentTheme = this.getInitialTheme();
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取初始主题
|
||||||
|
*/
|
||||||
|
getInitialTheme() {
|
||||||
|
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
return savedTheme || (prefersDarkScheme.matches ? 'dark' : 'light');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化主题管理器
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
if (!this.themeToggle) return;
|
||||||
|
|
||||||
|
this.setTheme(this.currentTheme);
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定事件
|
||||||
|
*/
|
||||||
|
bindEvents() {
|
||||||
|
this.themeToggle.addEventListener('click', () => {
|
||||||
|
this.toggleTheme();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换主题
|
||||||
|
*/
|
||||||
|
toggleTheme() {
|
||||||
|
document.body.classList.add('theme-transition');
|
||||||
|
|
||||||
|
const newTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
|
||||||
|
this.setTheme(newTheme);
|
||||||
|
this.saveTheme(newTheme);
|
||||||
|
|
||||||
|
setTimeout(() => document.body.classList.remove('theme-transition'), 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置主题
|
||||||
|
* @param {string} theme - 'dark' 或 'light'
|
||||||
|
*/
|
||||||
|
setTheme(theme) {
|
||||||
|
this.currentTheme = theme;
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
|
document.body.classList.toggle('theme-dark', isDark);
|
||||||
|
document.body.classList.toggle('theme-light', !isDark);
|
||||||
|
|
||||||
|
this.updateToggleIcon(isDark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新切换按钮图标
|
||||||
|
* @param {boolean} isDark - 是否为暗色主题
|
||||||
|
*/
|
||||||
|
updateToggleIcon(isDark) {
|
||||||
|
if (this.themeToggle) {
|
||||||
|
const icon = this.themeToggle.querySelector('i');
|
||||||
|
if (icon) {
|
||||||
|
icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存主题到本地存储
|
||||||
|
* @param {string} theme - 主题名称
|
||||||
|
*/
|
||||||
|
saveTheme(theme) {
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语言管理类
|
||||||
|
*/
|
||||||
|
class LanguageManager {
|
||||||
|
constructor() {
|
||||||
|
this.langToggle = document.getElementById('lang-toggle');
|
||||||
|
this.currentLang = this.getInitialLanguage();
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取初始语言
|
||||||
|
*/
|
||||||
|
getInitialLanguage() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const urlLang = urlParams.get('lang');
|
||||||
|
const savedLang = localStorage.getItem('lang');
|
||||||
|
const browserLang = navigator.language.startsWith('zh') ? 'zh' : 'en';
|
||||||
|
return urlLang || savedLang || browserLang;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化语言管理器
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
if (!this.langToggle) return;
|
||||||
|
|
||||||
|
window.currentLang = this.currentLang;
|
||||||
|
this.setLanguage(this.currentLang);
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定事件
|
||||||
|
*/
|
||||||
|
bindEvents() {
|
||||||
|
this.langToggle.addEventListener('click', () => {
|
||||||
|
this.toggleLanguage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换语言
|
||||||
|
*/
|
||||||
|
toggleLanguage() {
|
||||||
|
document.body.classList.add('lang-transition');
|
||||||
|
|
||||||
|
const newLang = this.currentLang === 'zh' ? 'en' : 'zh';
|
||||||
|
this.setLanguage(newLang);
|
||||||
|
this.saveLanguage(newLang);
|
||||||
|
this.updateURL(newLang);
|
||||||
|
this.notifyLanguageChange(newLang);
|
||||||
|
|
||||||
|
setTimeout(() => document.body.classList.remove('lang-transition'), 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置页面语言
|
||||||
|
* @param {string} lang - 'zh' 或 'en'
|
||||||
|
*/
|
||||||
|
setLanguage(lang) {
|
||||||
|
this.currentLang = lang;
|
||||||
|
window.currentLang = lang;
|
||||||
|
|
||||||
|
this.updatePageElements(lang);
|
||||||
|
this.updateHTMLLang(lang);
|
||||||
|
this.updateToggleButton(lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新页面元素文本
|
||||||
|
* @param {string} lang - 语言代码
|
||||||
|
*/
|
||||||
|
updatePageElements(lang) {
|
||||||
|
document.querySelectorAll('[data-zh][data-en]').forEach(el => {
|
||||||
|
el.textContent = el.getAttribute(`data-${lang}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新HTML语言属性
|
||||||
|
* @param {string} lang - 语言代码
|
||||||
|
*/
|
||||||
|
updateHTMLLang(lang) {
|
||||||
|
document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新切换按钮文本
|
||||||
|
* @param {string} lang - 语言代码
|
||||||
|
*/
|
||||||
|
updateToggleButton(lang) {
|
||||||
|
if (this.langToggle) {
|
||||||
|
const text = lang === 'zh' ? 'EN/中' : '中/EN';
|
||||||
|
this.langToggle.innerHTML = `<i class="fas fa-language"></i> ${text}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存语言到本地存储
|
||||||
|
* @param {string} lang - 语言代码
|
||||||
|
*/
|
||||||
|
saveLanguage(lang) {
|
||||||
|
localStorage.setItem('lang', lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新URL参数
|
||||||
|
* @param {string} lang - 语言代码
|
||||||
|
*/
|
||||||
|
updateURL(lang) {
|
||||||
|
const newUrl = new URL(window.location);
|
||||||
|
if (lang === 'zh') {
|
||||||
|
newUrl.searchParams.set('lang', 'zh');
|
||||||
|
} else {
|
||||||
|
newUrl.searchParams.delete('lang');
|
||||||
|
}
|
||||||
|
window.history.replaceState({}, '', newUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知语言变更
|
||||||
|
* @param {string} lang - 语言代码
|
||||||
|
*/
|
||||||
|
notifyLanguageChange(lang) {
|
||||||
|
window.dispatchEvent(new CustomEvent('languageChanged', { detail: { lang } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前语言
|
||||||
|
*/
|
||||||
|
getCurrentLanguage() {
|
||||||
|
return this.currentLang;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SEO管理类
|
||||||
|
*/
|
||||||
|
class SEOManager {
|
||||||
|
constructor(languageManager) {
|
||||||
|
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'
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
description: 'voidraft 是专为开发者打造的优雅文本片段记录工具。支持多语言代码块、语法高亮、代码格式化、自定义主题等功能。',
|
||||||
|
title: 'voidraft - 专为开发者打造的优雅文本片段记录工具',
|
||||||
|
ogTitle: 'voidraft - 专为开发者打造的优雅文本片段记录工具'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化SEO管理器
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.bindEvents();
|
||||||
|
this.updateMetaTags(this.languageManager.getCurrentLanguage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定事件
|
||||||
|
*/
|
||||||
|
bindEvents() {
|
||||||
|
window.addEventListener('languageChanged', (event) => {
|
||||||
|
this.updateMetaTags(event.detail.lang);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新SEO元标签
|
||||||
|
* @param {string} lang - 当前语言
|
||||||
|
*/
|
||||||
|
updateMetaTags(lang) {
|
||||||
|
const texts = this.metaTexts[lang];
|
||||||
|
|
||||||
|
this.updateMetaDescription(texts.description);
|
||||||
|
this.updateOpenGraphTags(texts.ogTitle, texts.description);
|
||||||
|
this.updateTwitterCardTags(texts.ogTitle, texts.description);
|
||||||
|
this.updatePageTitle(texts.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新meta描述
|
||||||
|
* @param {string} description - 描述文本
|
||||||
|
*/
|
||||||
|
updateMetaDescription(description) {
|
||||||
|
const metaDesc = document.querySelector('meta[name="description"]');
|
||||||
|
if (metaDesc) {
|
||||||
|
metaDesc.content = description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新Open Graph标签
|
||||||
|
* @param {string} title - 标题
|
||||||
|
* @param {string} description - 描述
|
||||||
|
*/
|
||||||
|
updateOpenGraphTags(title, description) {
|
||||||
|
const ogTitle = document.querySelector('meta[property="og:title"]');
|
||||||
|
const ogDesc = document.querySelector('meta[property="og:description"]');
|
||||||
|
|
||||||
|
if (ogTitle) ogTitle.content = title;
|
||||||
|
if (ogDesc) ogDesc.content = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新Twitter Card标签
|
||||||
|
* @param {string} title - 标题
|
||||||
|
* @param {string} description - 描述
|
||||||
|
*/
|
||||||
|
updateTwitterCardTags(title, description) {
|
||||||
|
const twitterTitle = document.querySelector('meta[property="twitter:title"]');
|
||||||
|
const twitterDesc = document.querySelector('meta[property="twitter:description"]');
|
||||||
|
|
||||||
|
if (twitterTitle) twitterTitle.content = title;
|
||||||
|
if (twitterDesc) twitterDesc.content = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新页面标题
|
||||||
|
* @param {string} title - 标题
|
||||||
|
*/
|
||||||
|
updatePageTitle(title) {
|
||||||
|
document.title = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI效果管理类
|
||||||
|
*/
|
||||||
|
class UIEffects {
|
||||||
|
constructor() {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化UI效果
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.initCardEffects();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化卡片悬停效果
|
||||||
|
*/
|
||||||
|
initCardEffects() {
|
||||||
|
const cards = document.querySelectorAll('.feature-card');
|
||||||
|
|
||||||
|
cards.forEach(card => {
|
||||||
|
card.addEventListener('mouseenter', () => {
|
||||||
|
this.animateCardHover(card, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('mouseleave', () => {
|
||||||
|
this.animateCardHover(card, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卡片悬停动画
|
||||||
|
* @param {Element} card - 卡片元素
|
||||||
|
* @param {boolean} isHover - 是否悬停
|
||||||
|
*/
|
||||||
|
animateCardHover(card, isHover) {
|
||||||
|
if (isHover) {
|
||||||
|
card.style.transform = 'translateY(-8px)';
|
||||||
|
card.style.boxShadow = '7px 7px 0 var(--shadow-color)';
|
||||||
|
} else {
|
||||||
|
card.style.transform = 'translateY(0)';
|
||||||
|
card.style.boxShadow = '5px 5px 0 var(--shadow-color)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* voidraft主应用类
|
||||||
|
*/
|
||||||
|
class voidraftApp {
|
||||||
|
constructor() {
|
||||||
|
this.themeManager = null;
|
||||||
|
this.languageManager = null;
|
||||||
|
this.seoManager = null;
|
||||||
|
this.uiEffects = null;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化应用
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.initializeManagers();
|
||||||
|
this.showConsoleBranding();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化各个管理器
|
||||||
|
*/
|
||||||
|
initializeManagers() {
|
||||||
|
this.themeManager = new ThemeManager();
|
||||||
|
this.languageManager = new LanguageManager();
|
||||||
|
this.seoManager = new SEOManager(this.languageManager);
|
||||||
|
this.uiEffects = new UIEffects();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示控制台品牌信息
|
||||||
|
*/
|
||||||
|
showConsoleBranding() {
|
||||||
|
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;');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取主题管理器
|
||||||
|
*/
|
||||||
|
getThemeManager() {
|
||||||
|
return this.themeManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取语言管理器
|
||||||
|
*/
|
||||||
|
getLanguageManager() {
|
||||||
|
return this.languageManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取SEO管理器
|
||||||
|
*/
|
||||||
|
getSEOManager() {
|
||||||
|
return this.seoManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取UI效果管理器
|
||||||
|
*/
|
||||||
|
getUIEffects() {
|
||||||
|
return this.uiEffects;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当DOM加载完成时初始化应用
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.voidRaftApp = new voidraftApp();
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
import * as Service from "./service.js";
|
import * as BadgeService from "./badgeservice.js";
|
||||||
export {
|
export {
|
||||||
Service
|
BadgeService
|
||||||
};
|
};
|
||||||
|
|
||||||
export * from "./models.js";
|
export * from "./models.js";
|
||||||
@@ -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;
|
||||||
@@ -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";
|
||||||
@@ -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);
|
||||||
+110
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// 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";
|
|
||||||
|
|
||||||
export class Config {
|
|
||||||
/**
|
|
||||||
* DBSource is the database URI to use.
|
|
||||||
* The string ":memory:" can be used to create an in-memory database.
|
|
||||||
* The sqlite driver can be configured through query parameters.
|
|
||||||
* For more details see https://pkg.go.dev/modernc.org/sqlite#Driver.Open
|
|
||||||
*/
|
|
||||||
"DBSource": string;
|
|
||||||
|
|
||||||
/** Creates a new Config instance. */
|
|
||||||
constructor($$source: Partial<Config> = {}) {
|
|
||||||
if (!("DBSource" in $$source)) {
|
|
||||||
this["DBSource"] = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(this, $$source);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new Config instance from a string or object.
|
|
||||||
*/
|
|
||||||
static createFrom($$source: any = {}): Config {
|
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
|
||||||
return new Config($$parsedSource as Partial<Config>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Row holds a single row in the result of a query.
|
|
||||||
* It is a key-value map where keys are column names.
|
|
||||||
*/
|
|
||||||
export type Row = { [_: string]: any };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rows holds the result of a query
|
|
||||||
* as an array of key-value maps where keys are column names.
|
|
||||||
*/
|
|
||||||
export type Rows = Row[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stmt wraps a prepared sql statement pointer.
|
|
||||||
* It provides the same methods as the [sql.Stmt] type.
|
|
||||||
*/
|
|
||||||
export type Stmt = string;
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
// 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 {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 {
|
|
||||||
ExecContext as Execute,
|
|
||||||
QueryContext as Query
|
|
||||||
};
|
|
||||||
|
|
||||||
import { Stmt } from "./stmt.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepare creates a prepared statement for later queries or executions.
|
|
||||||
* Multiple queries or executions may be run concurrently from the returned statement.
|
|
||||||
*
|
|
||||||
* The caller must call the statement's Close method when it is no longer needed.
|
|
||||||
* Statements are closed automatically
|
|
||||||
* when the connection they are associated with is closed.
|
|
||||||
*
|
|
||||||
* Prepare supports early cancellation.
|
|
||||||
*/
|
|
||||||
export function Prepare(query: string): Promise<Stmt | null> & { cancel(): void } {
|
|
||||||
const promise = PrepareContext(query);
|
|
||||||
const wrapper: any = (promise.then(function (id) {
|
|
||||||
return id == null ? null : new Stmt(
|
|
||||||
ClosePrepared.bind(null, id),
|
|
||||||
ExecPrepared.bind(null, id),
|
|
||||||
QueryPrepared.bind(null, id));
|
|
||||||
}));
|
|
||||||
wrapper.cancel = promise.cancel;
|
|
||||||
return wrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close closes the current database connection if one is open, otherwise has no effect.
|
|
||||||
* Additionally, Close closes all open prepared statements associated to the connection.
|
|
||||||
*
|
|
||||||
* Even when a non-nil error is returned,
|
|
||||||
* the database service is left in a consistent state,
|
|
||||||
* ready for a call to [Service.Open].
|
|
||||||
*/
|
|
||||||
export function Close(): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(1888105376) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ClosePrepared closes a prepared statement
|
|
||||||
* obtained with [Service.Prepare] or [Service.PrepareContext].
|
|
||||||
* ClosePrepared is idempotent:
|
|
||||||
* it has no effect on prepared statements that are already closed.
|
|
||||||
*/
|
|
||||||
function ClosePrepared(stmt: $models.Stmt | null): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(2526200629, stmt) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure changes the database service configuration.
|
|
||||||
* The connection state at call time is preserved.
|
|
||||||
* Consumers will need to call [Service.Open] manually after Configure
|
|
||||||
* in order to reconnect with the new configuration.
|
|
||||||
*
|
|
||||||
* See [NewWithConfig] for details on configuration.
|
|
||||||
*/
|
|
||||||
export function Configure(config: $models.Config | null): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(1939578712, config) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ExecContext executes a query without returning any rows.
|
|
||||||
* It supports early cancellation.
|
|
||||||
*/
|
|
||||||
function ExecContext(query: string, ...args: any[]): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(674944556, query, args) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ExecPrepared executes a prepared statement
|
|
||||||
* obtained with [Service.Prepare] or [Service.PrepareContext]
|
|
||||||
* without returning any rows.
|
|
||||||
* It supports early cancellation.
|
|
||||||
*/
|
|
||||||
function ExecPrepared(stmt: $models.Stmt | null, ...args: any[]): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(2086877656, stmt, args) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute executes a query without returning any rows.
|
|
||||||
*/
|
|
||||||
export function Execute(query: string, ...args: any[]): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(3811930203, query, args) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open validates the current configuration,
|
|
||||||
* closes the current connection if one is present,
|
|
||||||
* then opens and validates a new connection.
|
|
||||||
*
|
|
||||||
* Even when a non-nil error is returned,
|
|
||||||
* the database service is left in a consistent state,
|
|
||||||
* ready for a new call to Open.
|
|
||||||
*/
|
|
||||||
export function Open(): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(2012175612) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepare creates a prepared statement for later queries or executions.
|
|
||||||
* Multiple queries or executions may be run concurrently from the returned statement.
|
|
||||||
*
|
|
||||||
* The caller should call the statement's Close method when it is no longer needed.
|
|
||||||
* Statements are closed automatically
|
|
||||||
* when the connection they are associated with is closed.
|
|
||||||
*/
|
|
||||||
export function Prepare(query: string): Promise<$models.Stmt | null> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(1801965143, query) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PrepareContext creates a prepared statement for later queries or executions.
|
|
||||||
* Multiple queries or executions may be run concurrently from the returned statement.
|
|
||||||
*
|
|
||||||
* The caller must call the statement's Close method when it is no longer needed.
|
|
||||||
* Statements are closed automatically
|
|
||||||
* when the connection they are associated with is closed.
|
|
||||||
*
|
|
||||||
* PrepareContext supports early cancellation.
|
|
||||||
*/
|
|
||||||
function PrepareContext(query: string): Promise<$models.Stmt | null> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(570941694, query) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query executes a query and returns a slice of key-value records,
|
|
||||||
* one per row, with column names as keys.
|
|
||||||
*/
|
|
||||||
export function Query(query: string, ...args: any[]): Promise<$models.Rows> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(860757720, query, args) as any;
|
|
||||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
|
||||||
return $$createType1($result);
|
|
||||||
}) as any;
|
|
||||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
|
||||||
return $typingPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* QueryContext executes a query and returns a slice of key-value records,
|
|
||||||
* one per row, with column names as keys.
|
|
||||||
* It supports early cancellation, returning the slice of results fetched so far.
|
|
||||||
*/
|
|
||||||
function QueryContext(query: string, ...args: any[]): Promise<$models.Rows> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(4115542347, query, args) as any;
|
|
||||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
|
||||||
return $$createType1($result);
|
|
||||||
}) as any;
|
|
||||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
|
||||||
return $typingPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* QueryPrepared executes a prepared statement
|
|
||||||
* obtained with [Service.Prepare] or [Service.PrepareContext]
|
|
||||||
* and returns a slice of key-value records, one per row, with column names as keys.
|
|
||||||
* It supports early cancellation, returning the slice of results fetched so far.
|
|
||||||
*/
|
|
||||||
function QueryPrepared(stmt: $models.Stmt | null, ...args: any[]): Promise<$models.Rows> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(3885083725, stmt, args) as any;
|
|
||||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
|
||||||
return $$createType1($result);
|
|
||||||
}) as any;
|
|
||||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
|
||||||
return $typingPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ServiceName returns the name of the plugin.
|
|
||||||
* You should use the go module format e.g. github.com/myuser/myplugin
|
|
||||||
*/
|
|
||||||
export function ServiceName(): Promise<string> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(1637123084) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ServiceShutdown closes the database connection.
|
|
||||||
* It returns a non-nil error in case of failures.
|
|
||||||
*/
|
|
||||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(3650435925) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ServiceStartup opens the database connection.
|
|
||||||
* It returns a non-nil error in case of failures.
|
|
||||||
*/
|
|
||||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(1113159936, options) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private type creation functions
|
|
||||||
const $$createType0 = $Create.Map($Create.Any, $Create.Any);
|
|
||||||
const $$createType1 = $Create.Array($$createType0);
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
//@ts-check
|
|
||||||
|
|
||||||
//@ts-ignore: Unused imports
|
|
||||||
import * as $models from "./models.js";
|
|
||||||
|
|
||||||
const execSymbol = Symbol("exec"),
|
|
||||||
querySymbol = Symbol("query"),
|
|
||||||
closeSymbol = Symbol("close");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stmt represents a prepared statement for later queries or executions.
|
|
||||||
* Multiple queries or executions may be run concurrently on the same statement.
|
|
||||||
*
|
|
||||||
* The caller must call the statement's Close method when it is no longer needed.
|
|
||||||
* Statements are closed automatically
|
|
||||||
* when the connection they are associated with is closed.
|
|
||||||
*/
|
|
||||||
export class Stmt {
|
|
||||||
/**
|
|
||||||
* Constructs a new prepared statement instance.
|
|
||||||
* @param {(...args: any[]) => Promise<void>} close
|
|
||||||
* @param {(...args: any[]) => Promise<void> & { cancel(): void }} exec
|
|
||||||
* @param {(...args: any[]) => Promise<$models.Rows> & { cancel(): void }} query
|
|
||||||
*/
|
|
||||||
constructor(close, exec, query) {
|
|
||||||
/**
|
|
||||||
* @member
|
|
||||||
* @private
|
|
||||||
* @type {typeof close}
|
|
||||||
*/
|
|
||||||
this[closeSymbol] = close;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @member
|
|
||||||
* @private
|
|
||||||
* @type {typeof exec}
|
|
||||||
*/
|
|
||||||
this[execSymbol] = exec;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @member
|
|
||||||
* @private
|
|
||||||
* @type {typeof query}
|
|
||||||
*/
|
|
||||||
this[querySymbol] = query;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the prepared statement.
|
|
||||||
* It has no effect when the statement is already closed.
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
Close() {
|
|
||||||
return this[closeSymbol]();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the prepared statement without returning any rows.
|
|
||||||
* It supports early cancellation.
|
|
||||||
*
|
|
||||||
* @param {any[]} args
|
|
||||||
* @returns {Promise<void> & { cancel(): void }}
|
|
||||||
*/
|
|
||||||
Exec(...args) {
|
|
||||||
return this[execSymbol](...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the prepared statement
|
|
||||||
* and returns a slice of key-value records, one per row, with column names as keys.
|
|
||||||
* It supports early cancellation, returning the array of results fetched so far.
|
|
||||||
*
|
|
||||||
* @param {any[]} args
|
|
||||||
* @returns {Promise<$models.Rows> & { cancel(): void }}
|
|
||||||
*/
|
|
||||||
Query(...args) {
|
|
||||||
return this[querySymbol](...args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export * from "./models.js";
|
||||||
@@ -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>);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,11 @@ export class AppConfig {
|
|||||||
*/
|
*/
|
||||||
"updates": UpdatesConfig;
|
"updates": UpdatesConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git备份设置
|
||||||
|
*/
|
||||||
|
"backup": GitBackupConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配置元数据
|
* 配置元数据
|
||||||
*/
|
*/
|
||||||
@@ -52,6 +57,9 @@ export class AppConfig {
|
|||||||
if (!("updates" in $$source)) {
|
if (!("updates" in $$source)) {
|
||||||
this["updates"] = (new UpdatesConfig());
|
this["updates"] = (new UpdatesConfig());
|
||||||
}
|
}
|
||||||
|
if (!("backup" in $$source)) {
|
||||||
|
this["backup"] = (new GitBackupConfig());
|
||||||
|
}
|
||||||
if (!("metadata" in $$source)) {
|
if (!("metadata" in $$source)) {
|
||||||
this["metadata"] = (new ConfigMetadata());
|
this["metadata"] = (new ConfigMetadata());
|
||||||
}
|
}
|
||||||
@@ -68,6 +76,7 @@ export class AppConfig {
|
|||||||
const $$createField2_0 = $$createType2;
|
const $$createField2_0 = $$createType2;
|
||||||
const $$createField3_0 = $$createType3;
|
const $$createField3_0 = $$createType3;
|
||||||
const $$createField4_0 = $$createType4;
|
const $$createField4_0 = $$createType4;
|
||||||
|
const $$createField5_0 = $$createType5;
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
if ("general" in $$parsedSource) {
|
if ("general" in $$parsedSource) {
|
||||||
$$parsedSource["general"] = $$createField0_0($$parsedSource["general"]);
|
$$parsedSource["general"] = $$createField0_0($$parsedSource["general"]);
|
||||||
@@ -81,8 +90,11 @@ export class AppConfig {
|
|||||||
if ("updates" in $$parsedSource) {
|
if ("updates" in $$parsedSource) {
|
||||||
$$parsedSource["updates"] = $$createField3_0($$parsedSource["updates"]);
|
$$parsedSource["updates"] = $$createField3_0($$parsedSource["updates"]);
|
||||||
}
|
}
|
||||||
|
if ("backup" in $$parsedSource) {
|
||||||
|
$$parsedSource["backup"] = $$createField4_0($$parsedSource["backup"]);
|
||||||
|
}
|
||||||
if ("metadata" in $$parsedSource) {
|
if ("metadata" in $$parsedSource) {
|
||||||
$$parsedSource["metadata"] = $$createField4_0($$parsedSource["metadata"]);
|
$$parsedSource["metadata"] = $$createField5_0($$parsedSource["metadata"]);
|
||||||
}
|
}
|
||||||
return new AppConfig($$parsedSource as Partial<AppConfig>);
|
return new AppConfig($$parsedSource as Partial<AppConfig>);
|
||||||
}
|
}
|
||||||
@@ -102,11 +114,6 @@ export class AppearanceConfig {
|
|||||||
*/
|
*/
|
||||||
"systemTheme": SystemThemeType;
|
"systemTheme": SystemThemeType;
|
||||||
|
|
||||||
/**
|
|
||||||
* 自定义主题配置
|
|
||||||
*/
|
|
||||||
"customTheme": CustomThemeConfig;
|
|
||||||
|
|
||||||
/** Creates a new AppearanceConfig instance. */
|
/** Creates a new AppearanceConfig instance. */
|
||||||
constructor($$source: Partial<AppearanceConfig> = {}) {
|
constructor($$source: Partial<AppearanceConfig> = {}) {
|
||||||
if (!("language" in $$source)) {
|
if (!("language" in $$source)) {
|
||||||
@@ -115,9 +122,6 @@ export class AppearanceConfig {
|
|||||||
if (!("systemTheme" in $$source)) {
|
if (!("systemTheme" in $$source)) {
|
||||||
this["systemTheme"] = ("" as SystemThemeType);
|
this["systemTheme"] = ("" as SystemThemeType);
|
||||||
}
|
}
|
||||||
if (!("customTheme" in $$source)) {
|
|
||||||
this["customTheme"] = (new CustomThemeConfig());
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(this, $$source);
|
Object.assign(this, $$source);
|
||||||
}
|
}
|
||||||
@@ -126,15 +130,30 @@ export class AppearanceConfig {
|
|||||||
* Creates a new AppearanceConfig instance from a string or object.
|
* Creates a new AppearanceConfig instance from a string or object.
|
||||||
*/
|
*/
|
||||||
static createFrom($$source: any = {}): AppearanceConfig {
|
static createFrom($$source: any = {}): AppearanceConfig {
|
||||||
const $$createField2_0 = $$createType5;
|
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
if ("customTheme" in $$parsedSource) {
|
|
||||||
$$parsedSource["customTheme"] = $$createField2_0($$parsedSource["customTheme"]);
|
|
||||||
}
|
|
||||||
return new AppearanceConfig($$parsedSource as Partial<AppearanceConfig>);
|
return new AppearanceConfig($$parsedSource as Partial<AppearanceConfig>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git备份相关类型定义
|
||||||
|
*
|
||||||
|
* AuthMethod 定义Git认证方式
|
||||||
|
*/
|
||||||
|
export enum AuthMethod {
|
||||||
|
/**
|
||||||
|
* The Go zero value for the underlying type of the enum.
|
||||||
|
*/
|
||||||
|
$zero = "",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证方式
|
||||||
|
*/
|
||||||
|
Token = "token",
|
||||||
|
SSHKey = "ssh_key",
|
||||||
|
UserPass = "user_pass",
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConfigMetadata 配置元数据
|
* ConfigMetadata 配置元数据
|
||||||
*/
|
*/
|
||||||
@@ -170,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 = $$createType6;
|
|
||||||
const $$createField1_0 = $$createType6;
|
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
|
||||||
if ("darkTheme" in $$parsedSource) {
|
|
||||||
$$parsedSource["darkTheme"] = $$createField0_0($$parsedSource["darkTheme"]);
|
|
||||||
}
|
|
||||||
if ("lightTheme" in $$parsedSource) {
|
|
||||||
$$parsedSource["lightTheme"] = $$createField1_0($$parsedSource["lightTheme"]);
|
|
||||||
}
|
|
||||||
return new CustomThemeConfig($$parsedSource as Partial<CustomThemeConfig>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Document represents a document in the system
|
* Document represents a document in the system
|
||||||
*/
|
*/
|
||||||
@@ -224,6 +200,11 @@ export class Document {
|
|||||||
"updatedAt": time$0.Time;
|
"updatedAt": time$0.Time;
|
||||||
"is_deleted": boolean;
|
"is_deleted": boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 锁定标志,锁定的文档无法被删除
|
||||||
|
*/
|
||||||
|
"is_locked": boolean;
|
||||||
|
|
||||||
/** Creates a new Document instance. */
|
/** Creates a new Document instance. */
|
||||||
constructor($$source: Partial<Document> = {}) {
|
constructor($$source: Partial<Document> = {}) {
|
||||||
if (!("id" in $$source)) {
|
if (!("id" in $$source)) {
|
||||||
@@ -244,6 +225,9 @@ export class Document {
|
|||||||
if (!("is_deleted" in $$source)) {
|
if (!("is_deleted" in $$source)) {
|
||||||
this["is_deleted"] = false;
|
this["is_deleted"] = false;
|
||||||
}
|
}
|
||||||
|
if (!("is_locked" in $$source)) {
|
||||||
|
this["is_locked"] = false;
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(this, $$source);
|
Object.assign(this, $$source);
|
||||||
}
|
}
|
||||||
@@ -389,7 +373,7 @@ export class Extension {
|
|||||||
* Creates a new Extension instance from a string or object.
|
* Creates a new Extension instance from a string or object.
|
||||||
*/
|
*/
|
||||||
static createFrom($$source: any = {}): Extension {
|
static createFrom($$source: any = {}): Extension {
|
||||||
const $$createField3_0 = $$createType7;
|
const $$createField3_0 = $$createType6;
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
if ("config" in $$parsedSource) {
|
if ("config" in $$parsedSource) {
|
||||||
$$parsedSource["config"] = $$createField3_0($$parsedSource["config"]);
|
$$parsedSource["config"] = $$createField3_0($$parsedSource["config"]);
|
||||||
@@ -483,6 +467,12 @@ export class GeneralConfig {
|
|||||||
*/
|
*/
|
||||||
"startAtLogin": boolean;
|
"startAtLogin": boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 窗口吸附设置
|
||||||
|
* 是否启用窗口吸附功能(阈值现在是自适应的)
|
||||||
|
*/
|
||||||
|
"enableWindowSnap": boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局热键设置
|
* 全局热键设置
|
||||||
* 是否启用全局热键
|
* 是否启用全局热键
|
||||||
@@ -508,6 +498,9 @@ export class GeneralConfig {
|
|||||||
if (!("startAtLogin" in $$source)) {
|
if (!("startAtLogin" in $$source)) {
|
||||||
this["startAtLogin"] = false;
|
this["startAtLogin"] = false;
|
||||||
}
|
}
|
||||||
|
if (!("enableWindowSnap" in $$source)) {
|
||||||
|
this["enableWindowSnap"] = false;
|
||||||
|
}
|
||||||
if (!("enableGlobalHotkey" in $$source)) {
|
if (!("enableGlobalHotkey" in $$source)) {
|
||||||
this["enableGlobalHotkey"] = false;
|
this["enableGlobalHotkey"] = false;
|
||||||
}
|
}
|
||||||
@@ -522,15 +515,64 @@ export class GeneralConfig {
|
|||||||
* Creates a new GeneralConfig instance from a string or object.
|
* Creates a new GeneralConfig instance from a string or object.
|
||||||
*/
|
*/
|
||||||
static createFrom($$source: any = {}): GeneralConfig {
|
static createFrom($$source: any = {}): GeneralConfig {
|
||||||
const $$createField5_0 = $$createType9;
|
const $$createField6_0 = $$createType8;
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
if ("globalHotkey" in $$parsedSource) {
|
if ("globalHotkey" in $$parsedSource) {
|
||||||
$$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]);
|
$$parsedSource["globalHotkey"] = $$createField6_0($$parsedSource["globalHotkey"]);
|
||||||
}
|
}
|
||||||
return new GeneralConfig($$parsedSource as Partial<GeneralConfig>);
|
return new GeneralConfig($$parsedSource as Partial<GeneralConfig>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitBackupConfig Git备份配置
|
||||||
|
*/
|
||||||
|
export class GitBackupConfig {
|
||||||
|
"enabled": boolean;
|
||||||
|
"repo_url": string;
|
||||||
|
"auth_method": AuthMethod;
|
||||||
|
"username"?: string;
|
||||||
|
"password"?: string;
|
||||||
|
"token"?: string;
|
||||||
|
"ssh_key_path"?: string;
|
||||||
|
"ssh_key_passphrase"?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分钟
|
||||||
|
*/
|
||||||
|
"backup_interval": number;
|
||||||
|
"auto_backup": boolean;
|
||||||
|
|
||||||
|
/** Creates a new GitBackupConfig instance. */
|
||||||
|
constructor($$source: Partial<GitBackupConfig> = {}) {
|
||||||
|
if (!("enabled" in $$source)) {
|
||||||
|
this["enabled"] = false;
|
||||||
|
}
|
||||||
|
if (!("repo_url" in $$source)) {
|
||||||
|
this["repo_url"] = "";
|
||||||
|
}
|
||||||
|
if (!("auth_method" in $$source)) {
|
||||||
|
this["auth_method"] = ("" as AuthMethod);
|
||||||
|
}
|
||||||
|
if (!("backup_interval" in $$source)) {
|
||||||
|
this["backup_interval"] = 0;
|
||||||
|
}
|
||||||
|
if (!("auto_backup" in $$source)) {
|
||||||
|
this["auto_backup"] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new GitBackupConfig instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): GitBackupConfig {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new GitBackupConfig($$parsedSource as Partial<GitBackupConfig>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GiteaConfig Gitea配置
|
* GiteaConfig Gitea配置
|
||||||
*/
|
*/
|
||||||
@@ -1083,6 +1125,58 @@ export enum TabType {
|
|||||||
TabTypeTab = "tab",
|
TabTypeTab = "tab",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme 主题数据库模型
|
||||||
|
*/
|
||||||
|
export class Theme {
|
||||||
|
"id": number;
|
||||||
|
"name": string;
|
||||||
|
"type": ThemeType;
|
||||||
|
"colors": ThemeColorConfig;
|
||||||
|
"isDefault": boolean;
|
||||||
|
"createdAt": time$0.Time;
|
||||||
|
"updatedAt": time$0.Time;
|
||||||
|
|
||||||
|
/** Creates a new Theme instance. */
|
||||||
|
constructor($$source: Partial<Theme> = {}) {
|
||||||
|
if (!("id" in $$source)) {
|
||||||
|
this["id"] = 0;
|
||||||
|
}
|
||||||
|
if (!("name" in $$source)) {
|
||||||
|
this["name"] = "";
|
||||||
|
}
|
||||||
|
if (!("type" in $$source)) {
|
||||||
|
this["type"] = ("" as ThemeType);
|
||||||
|
}
|
||||||
|
if (!("colors" in $$source)) {
|
||||||
|
this["colors"] = (new ThemeColorConfig());
|
||||||
|
}
|
||||||
|
if (!("isDefault" in $$source)) {
|
||||||
|
this["isDefault"] = false;
|
||||||
|
}
|
||||||
|
if (!("createdAt" in $$source)) {
|
||||||
|
this["createdAt"] = null;
|
||||||
|
}
|
||||||
|
if (!("updatedAt" in $$source)) {
|
||||||
|
this["updatedAt"] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Theme instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): Theme {
|
||||||
|
const $$createField3_0 = $$createType9;
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
if ("colors" in $$parsedSource) {
|
||||||
|
$$parsedSource["colors"] = $$createField3_0($$parsedSource["colors"]);
|
||||||
|
}
|
||||||
|
return new Theme($$parsedSource as Partial<Theme>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ThemeColorConfig 主题颜色配置
|
* ThemeColorConfig 主题颜色配置
|
||||||
*/
|
*/
|
||||||
@@ -1291,6 +1385,19 @@ export class ThemeColorConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeType 主题类型枚举
|
||||||
|
*/
|
||||||
|
export enum ThemeType {
|
||||||
|
/**
|
||||||
|
* The Go zero value for the underlying type of the enum.
|
||||||
|
*/
|
||||||
|
$zero = "",
|
||||||
|
|
||||||
|
ThemeTypeDark = "dark",
|
||||||
|
ThemeTypeLight = "light",
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UpdateSourceType 更新源类型
|
* UpdateSourceType 更新源类型
|
||||||
*/
|
*/
|
||||||
@@ -1407,16 +1514,16 @@ const $$createType0 = GeneralConfig.createFrom;
|
|||||||
const $$createType1 = EditingConfig.createFrom;
|
const $$createType1 = EditingConfig.createFrom;
|
||||||
const $$createType2 = AppearanceConfig.createFrom;
|
const $$createType2 = AppearanceConfig.createFrom;
|
||||||
const $$createType3 = UpdatesConfig.createFrom;
|
const $$createType3 = UpdatesConfig.createFrom;
|
||||||
const $$createType4 = ConfigMetadata.createFrom;
|
const $$createType4 = GitBackupConfig.createFrom;
|
||||||
const $$createType5 = CustomThemeConfig.createFrom;
|
const $$createType5 = ConfigMetadata.createFrom;
|
||||||
const $$createType6 = ThemeColorConfig.createFrom;
|
var $$createType6 = (function $$initCreateType6(...args): any {
|
||||||
var $$createType7 = (function $$initCreateType7(...args): any {
|
if ($$createType6 === $$initCreateType6) {
|
||||||
if ($$createType7 === $$initCreateType7) {
|
$$createType6 = $$createType7;
|
||||||
$$createType7 = $$createType8;
|
|
||||||
}
|
}
|
||||||
return $$createType7(...args);
|
return $$createType6(...args);
|
||||||
});
|
});
|
||||||
const $$createType8 = $Create.Map($Create.Any, $Create.Any);
|
const $$createType7 = $Create.Map($Create.Any, $Create.Any);
|
||||||
const $$createType9 = HotkeyCombo.createFrom;
|
const $$createType8 = HotkeyCombo.createFrom;
|
||||||
|
const $$createType9 = ThemeColorConfig.createFrom;
|
||||||
const $$createType10 = GithubConfig.createFrom;
|
const $$createType10 = GithubConfig.createFrom;
|
||||||
const $$createType11 = GiteaConfig.createFrom;
|
const $$createType11 = GiteaConfig.createFrom;
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BackupService 提供基于Git的备份功能
|
||||||
|
* @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 models$0 from "../models/models.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HandleConfigChange 处理备份配置变更
|
||||||
|
*/
|
||||||
|
export function HandleConfigChange(config: models$0.GitBackupConfig | null): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(395287784, config) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize 初始化备份服务
|
||||||
|
*/
|
||||||
|
export function Initialize(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(1052437974) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PushToRemote 推送本地更改到远程仓库
|
||||||
|
*/
|
||||||
|
export function PushToRemote(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(262644139) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reinitialize 重新初始化备份服务,用于响应配置变更
|
||||||
|
*/
|
||||||
|
export function Reinitialize(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(301562543) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ServiceShutdown 服务关闭时的清理工作
|
||||||
|
*/
|
||||||
|
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(422131801) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StartAutoBackup 启动自动备份定时器
|
||||||
|
*/
|
||||||
|
export function StartAutoBackup(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(3035755449) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StopAutoBackup 停止自动备份
|
||||||
|
*/
|
||||||
|
export function StopAutoBackup(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2641894021) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
@@ -34,6 +34,30 @@ export function GetConfig(): Promise<models$0.AppConfig | null> & { cancel(): vo
|
|||||||
return $typingPromise;
|
return $typingPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetConfigDir 获取配置目录
|
||||||
|
*/
|
||||||
|
export function GetConfigDir(): Promise<string> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2275626561) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetSettingsPath 获取设置文件路径
|
||||||
|
*/
|
||||||
|
export function GetSettingsPath(): Promise<string> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2175583370) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MigrateConfig 执行配置迁移
|
||||||
|
*/
|
||||||
|
export function MigrateConfig(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(434292783) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ResetConfig 强制重置所有配置为默认值
|
* ResetConfig 强制重置所有配置为默认值
|
||||||
*/
|
*/
|
||||||
@@ -58,6 +82,14 @@ export function Set(key: string, value: any): Promise<void> & { cancel(): void }
|
|||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SetBackupConfigChangeCallback 设置备份配置变更回调
|
||||||
|
*/
|
||||||
|
export function SetBackupConfigChangeCallback(callback: any): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(3264871659, callback) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SetDataPathChangeCallback 设置数据路径配置变更回调
|
* SetDataPathChangeCallback 设置数据路径配置变更回调
|
||||||
*/
|
*/
|
||||||
@@ -74,6 +106,14 @@ export function SetHotkeyChangeCallback(callback: any): Promise<void> & { cancel
|
|||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SetWindowSnapConfigChangeCallback 设置窗口吸附配置变更回调
|
||||||
|
*/
|
||||||
|
export function SetWindowSnapConfigChangeCallback(callback: any): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2324961653, callback) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
// Private type creation functions
|
// Private type creation functions
|
||||||
const $$createType0 = models$0.AppConfig.createFrom;
|
const $$createType0 = models$0.AppConfig.createFrom;
|
||||||
const $$createType1 = $Create.Nullable($$createType0);
|
const $$createType1 = $Create.Nullable($$createType0);
|
||||||
@@ -22,6 +22,14 @@ export function OnDataPathChanged(): Promise<void> & { cancel(): void } {
|
|||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RegisterModel 注册模型与表的映射关系
|
||||||
|
*/
|
||||||
|
export function RegisterModel(tableName: string, model: any): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(175397515, tableName, model) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ServiceShutdown shuts down the service when the application closes
|
* ServiceShutdown shuts down the service when the application closes
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ export function SelectDirectory(): Promise<string> & { cancel(): void } {
|
|||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SelectFile 打开文件选择对话框
|
||||||
|
*/
|
||||||
|
export function SelectFile(): Promise<string> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(37302920) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SetWindow 设置绑定的窗口
|
* SetWindow 设置绑定的窗口
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -81,6 +81,14 @@ export function ListDeletedDocumentsMeta(): Promise<(models$0.Document | null)[]
|
|||||||
return $typingPromise;
|
return $typingPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LockDocument 锁定文档,防止删除
|
||||||
|
*/
|
||||||
|
export function LockDocument(id: number): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(1889494473, id) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RestoreDocument restores a deleted document
|
* RestoreDocument restores a deleted document
|
||||||
*/
|
*/
|
||||||
@@ -97,6 +105,14 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
|
|||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UnlockDocument 解锁文档
|
||||||
|
*/
|
||||||
|
export function UnlockDocument(id: number): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(222307930, id) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UpdateDocumentContent updates the content of a document
|
* UpdateDocumentContent updates the content of a document
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ export function GetCurrentHotkey(): Promise<models$0.HotkeyCombo | null> & { can
|
|||||||
/**
|
/**
|
||||||
* Initialize 初始化热键服务
|
* Initialize 初始化热键服务
|
||||||
*/
|
*/
|
||||||
export function Initialize(app: application$0.App | null): Promise<void> & { cancel(): void } {
|
export function Initialize(app: application$0.App | null, mainWindow: application$0.WebviewWindow | null): Promise<void> & { cancel(): void } {
|
||||||
let $resultPromise = $Call.ByID(3671360458, app) as any;
|
let $resultPromise = $Call.ByID(3671360458, app, mainWindow) as any;
|
||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
import * as BackupService from "./backupservice.js";
|
||||||
import * as ConfigService from "./configservice.js";
|
import * as ConfigService from "./configservice.js";
|
||||||
import * as DatabaseService from "./databaseservice.js";
|
import * as DatabaseService from "./databaseservice.js";
|
||||||
import * as DialogService from "./dialogservice.js";
|
import * as DialogService from "./dialogservice.js";
|
||||||
@@ -12,10 +13,14 @@ import * as MigrationService from "./migrationservice.js";
|
|||||||
import * as SelfUpdateService from "./selfupdateservice.js";
|
import * as SelfUpdateService from "./selfupdateservice.js";
|
||||||
import * as StartupService from "./startupservice.js";
|
import * as StartupService from "./startupservice.js";
|
||||||
import * as SystemService from "./systemservice.js";
|
import * as SystemService from "./systemservice.js";
|
||||||
|
import * as TestService from "./testservice.js";
|
||||||
|
import * as ThemeService from "./themeservice.js";
|
||||||
import * as TranslationService from "./translationservice.js";
|
import * as TranslationService from "./translationservice.js";
|
||||||
import * as TrayService from "./trayservice.js";
|
import * as TrayService from "./trayservice.js";
|
||||||
import * as WindowService from "./windowservice.js";
|
import * as WindowService from "./windowservice.js";
|
||||||
|
import * as WindowSnapService from "./windowsnapservice.js";
|
||||||
export {
|
export {
|
||||||
|
BackupService,
|
||||||
ConfigService,
|
ConfigService,
|
||||||
DatabaseService,
|
DatabaseService,
|
||||||
DialogService,
|
DialogService,
|
||||||
@@ -27,9 +32,12 @@ export {
|
|||||||
SelfUpdateService,
|
SelfUpdateService,
|
||||||
StartupService,
|
StartupService,
|
||||||
SystemService,
|
SystemService,
|
||||||
|
TestService,
|
||||||
|
ThemeService,
|
||||||
TranslationService,
|
TranslationService,
|
||||||
TrayService,
|
TrayService,
|
||||||
WindowService
|
WindowService,
|
||||||
|
WindowSnapService
|
||||||
};
|
};
|
||||||
|
|
||||||
export * from "./models.js";
|
export * from "./models.js";
|
||||||
@@ -203,7 +203,7 @@ export class SelfUpdateResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WindowInfo 窗口信息
|
* WindowInfo 窗口信息(简化版)
|
||||||
*/
|
*/
|
||||||
export class WindowInfo {
|
export class WindowInfo {
|
||||||
"Window": application$0.WebviewWindow | null;
|
"Window": application$0.WebviewWindow | null;
|
||||||
@@ -238,6 +238,26 @@ export class WindowInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WindowSnapService 窗口吸附服务
|
||||||
|
*/
|
||||||
|
export class WindowSnapService {
|
||||||
|
|
||||||
|
/** Creates a new WindowSnapService instance. */
|
||||||
|
constructor($$source: Partial<WindowSnapService> = {}) {
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new WindowSnapService instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): WindowSnapService {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new WindowSnapService($$parsedSource as Partial<WindowSnapService>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Private type creation functions
|
// Private type creation functions
|
||||||
const $$createType0 = application$0.WebviewWindow.createFrom;
|
const $$createType0 = application$0.WebviewWindow.createFrom;
|
||||||
const $$createType1 = $Create.Nullable($$createType0);
|
const $$createType1 = $Create.Nullable($$createType0);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WindowService 窗口管理服务
|
* WindowService 窗口管理服务(专注于窗口生命周期管理)
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -46,6 +46,14 @@ export function OpenDocumentWindow(documentID: number): Promise<void> & { cancel
|
|||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ServiceShutdown 实现服务关闭接口
|
||||||
|
*/
|
||||||
|
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(202192783) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SetAppReferences 设置应用和主窗口引用
|
* SetAppReferences 设置应用和主窗口引用
|
||||||
*/
|
*/
|
||||||
@@ -54,6 +62,14 @@ export function SetAppReferences(app: application$0.App | null, mainWindow: appl
|
|||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SetWindowSnapService 设置窗口吸附服务引用
|
||||||
|
*/
|
||||||
|
export function SetWindowSnapService(snapService: $models.WindowSnapService | null): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(1105193745, snapService) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
// Private type creation functions
|
// Private type creation functions
|
||||||
const $$createType0 = $models.WindowInfo.createFrom;
|
const $$createType0 = $models.WindowInfo.createFrom;
|
||||||
const $$createType1 = $Create.Array($$createType0);
|
const $$createType1 = $Create.Array($$createType0);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Generated
+366
-395
File diff suppressed because it is too large.
Load diff
+19
-20
@@ -24,24 +24,24 @@
|
|||||||
"@codemirror/lang-json": "^6.0.2",
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
"@codemirror/lang-less": "^6.0.2",
|
"@codemirror/lang-less": "^6.0.2",
|
||||||
"@codemirror/lang-lezer": "^6.0.2",
|
"@codemirror/lang-lezer": "^6.0.2",
|
||||||
"@codemirror/lang-liquid": "^6.2.3",
|
"@codemirror/lang-liquid": "^6.3.0",
|
||||||
"@codemirror/lang-markdown": "^6.3.3",
|
"@codemirror/lang-markdown": "^6.3.4",
|
||||||
"@codemirror/lang-php": "^6.0.2",
|
"@codemirror/lang-php": "^6.0.2",
|
||||||
"@codemirror/lang-python": "^6.2.1",
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
"@codemirror/lang-rust": "^6.0.2",
|
"@codemirror/lang-rust": "^6.0.2",
|
||||||
"@codemirror/lang-sass": "^6.0.2",
|
"@codemirror/lang-sass": "^6.0.2",
|
||||||
"@codemirror/lang-sql": "^6.9.0",
|
"@codemirror/lang-sql": "^6.9.1",
|
||||||
"@codemirror/lang-vue": "^0.1.3",
|
"@codemirror/lang-vue": "^0.1.3",
|
||||||
"@codemirror/lang-wast": "^6.0.2",
|
"@codemirror/lang-wast": "^6.0.2",
|
||||||
"@codemirror/lang-xml": "^6.1.0",
|
"@codemirror/lang-xml": "^6.1.0",
|
||||||
"@codemirror/lang-yaml": "^6.1.2",
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
"@codemirror/language": "^6.11.2",
|
"@codemirror/language": "^6.11.3",
|
||||||
"@codemirror/language-data": "^6.5.1",
|
"@codemirror/language-data": "^6.5.1",
|
||||||
"@codemirror/legacy-modes": "^6.5.1",
|
"@codemirror/legacy-modes": "^6.5.1",
|
||||||
"@codemirror/lint": "^6.8.5",
|
"@codemirror/lint": "^6.8.5",
|
||||||
"@codemirror/search": "^6.5.11",
|
"@codemirror/search": "^6.5.11",
|
||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/view": "^6.38.0",
|
"@codemirror/view": "^6.38.1",
|
||||||
"@lezer/highlight": "^1.2.1",
|
"@lezer/highlight": "^1.2.1",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
@@ -52,31 +52,30 @@
|
|||||||
"hsl-matcher": "^1.2.4",
|
"hsl-matcher": "^1.2.4",
|
||||||
"lezer": "^0.13.5",
|
"lezer": "^0.13.5",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.4.1",
|
"pinia-plugin-persistedstate": "^4.5.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"remarkable": "^2.0.1",
|
"remarkable": "^2.0.1",
|
||||||
"sass": "^1.89.2",
|
"sass": "^1.90.0",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.19",
|
||||||
"vue-i18n": "^11.1.9",
|
"vue-i18n": "^11.1.11",
|
||||||
"vue-pick-colors": "^1.8.0",
|
"vue-pick-colors": "^1.8.0",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.30.1",
|
"@eslint/js": "^9.34.0",
|
||||||
"@lezer/generator": "^1.8.0",
|
"@lezer/generator": "^1.8.0",
|
||||||
"@types/lodash": "^4.17.20",
|
"@types/node": "^24.3.0",
|
||||||
"@types/node": "^24.0.12",
|
|
||||||
"@types/remarkable": "^2.0.8",
|
"@types/remarkable": "^2.0.8",
|
||||||
"@vitejs/plugin-vue": "^6.0.0",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@wailsio/runtime": "latest",
|
"@wailsio/runtime": "latest",
|
||||||
"eslint": "^9.30.1",
|
"eslint": "^9.34.0",
|
||||||
"eslint-plugin-vue": "^10.3.0",
|
"eslint-plugin-vue": "^10.4.0",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.9.2",
|
||||||
"typescript-eslint": "^8.36.0",
|
"typescript-eslint": "^8.40.0",
|
||||||
"unplugin-vue-components": "^28.8.0",
|
"unplugin-vue-components": "^29.0.0",
|
||||||
"vite": "^7.0.3",
|
"vite": "^7.1.3",
|
||||||
"vue-eslint-parser": "^10.2.0",
|
"vue-eslint-parser": "^10.2.0",
|
||||||
"vue-tsc": "^3.0.1"
|
"vue-tsc": "^3.0.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import { useSystemStore } from '@/stores/systemStore';
|
|||||||
import {useKeybindingStore} from '@/stores/keybindingStore';
|
import {useKeybindingStore} from '@/stores/keybindingStore';
|
||||||
import {useThemeStore} from '@/stores/themeStore';
|
import {useThemeStore} from '@/stores/themeStore';
|
||||||
import {useUpdateStore} from '@/stores/updateStore';
|
import {useUpdateStore} from '@/stores/updateStore';
|
||||||
|
import {useBackupStore} from '@/stores/backupStore';
|
||||||
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
|
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
|
||||||
|
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
@@ -12,6 +13,7 @@ const systemStore = useSystemStore();
|
|||||||
const keybindingStore = useKeybindingStore();
|
const keybindingStore = useKeybindingStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const updateStore = useUpdateStore();
|
const updateStore = useUpdateStore();
|
||||||
|
const backupStore = useBackupStore();
|
||||||
|
|
||||||
// 应用启动时加载配置和初始化系统信息
|
// 应用启动时加载配置和初始化系统信息
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -26,6 +28,9 @@ onMounted(async () => {
|
|||||||
await configStore.initializeLanguage();
|
await configStore.initializeLanguage();
|
||||||
themeStore.initializeTheme();
|
themeStore.initializeTheme();
|
||||||
|
|
||||||
|
// 初始化备份服务
|
||||||
|
await backupStore.initialize();
|
||||||
|
|
||||||
// 启动时检查更新
|
// 启动时检查更新
|
||||||
await updateStore.checkOnStartup();
|
await updateStore.checkOnStartup();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,85 +46,56 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
import {computed, onMounted, ref} from 'vue';
|
||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
import { useWindowStore } from '@/stores/windowStore';
|
|
||||||
import {useDocumentStore} from '@/stores/documentStore';
|
import {useDocumentStore} from '@/stores/documentStore';
|
||||||
|
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
const isMaximized = ref(false);
|
const isMaximized = ref(false);
|
||||||
const documentStore = useDocumentStore();
|
const documentStore = useDocumentStore();
|
||||||
|
|
||||||
const minimizeWindow = async () => {
|
|
||||||
try {
|
|
||||||
await runtime.Window.Minimise();
|
|
||||||
} catch (error) {
|
|
||||||
// Error handling
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleMaximize = async () => {
|
|
||||||
try {
|
|
||||||
const newState = !isMaximized.value;
|
|
||||||
isMaximized.value = newState;
|
|
||||||
|
|
||||||
if (newState) {
|
|
||||||
await runtime.Window.Maximise();
|
|
||||||
} else {
|
|
||||||
await runtime.Window.UnMaximise();
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
await checkMaximizedState();
|
|
||||||
}, 100);
|
|
||||||
} catch (error) {
|
|
||||||
isMaximized.value = !isMaximized.value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeWindow = async () => {
|
|
||||||
try {
|
|
||||||
await runtime.Window.Close();
|
|
||||||
} catch (error) {
|
|
||||||
// Error handling
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkMaximizedState = async () => {
|
|
||||||
try {
|
|
||||||
isMaximized.value = await runtime.Window.IsMaximised();
|
|
||||||
} catch (error) {
|
|
||||||
// Error handling
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 计算标题文本
|
// 计算标题文本
|
||||||
const titleText = computed(() => {
|
const titleText = computed(() => {
|
||||||
const currentDoc = documentStore.currentDocument;
|
const currentDoc = documentStore.currentDocument;
|
||||||
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
|
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const minimizeWindow = async () => {
|
||||||
|
try {
|
||||||
|
await runtime.Window.Minimise();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMaximize = async () => {
|
||||||
|
try {
|
||||||
|
await runtime.Window.ToggleMaximise();
|
||||||
|
await checkMaximizedState();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeWindow = async () => {
|
||||||
|
try {
|
||||||
|
await runtime.Window.Close();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkMaximizedState = async () => {
|
||||||
|
try {
|
||||||
|
isMaximized.value = await runtime.Window.IsMaximised();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await checkMaximizedState();
|
await checkMaximizedState();
|
||||||
|
|
||||||
runtime.Events.On('window:maximised', () => {
|
|
||||||
isMaximized.value = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
runtime.Events.On('window:unmaximised', () => {
|
|
||||||
isMaximized.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
runtime.Events.On('window:focus', async () => {
|
|
||||||
await checkMaximizedState();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
runtime.Events.Off('window:maximised');
|
|
||||||
runtime.Events.Off('window:unmaximised');
|
|
||||||
runtime.Events.Off('window:focus');
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,6 @@
|
|||||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
import { useWindowStore } from '@/stores/windowStore';
|
|
||||||
import { useDocumentStore } from '@/stores/documentStore';
|
import { useDocumentStore } from '@/stores/documentStore';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -65,26 +64,16 @@ const minimizeWindow = async () => {
|
|||||||
try {
|
try {
|
||||||
await runtime.Window.Minimise();
|
await runtime.Window.Minimise();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error handling
|
console.error(error)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleMaximize = async () => {
|
const toggleMaximize = async () => {
|
||||||
try {
|
try {
|
||||||
const newState = !isMaximized.value;
|
await runtime.Window.ToggleMaximise();
|
||||||
isMaximized.value = newState;
|
|
||||||
|
|
||||||
if (newState) {
|
|
||||||
await runtime.Window.Maximise();
|
|
||||||
} else {
|
|
||||||
await runtime.Window.UnMaximise();
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
await checkMaximizedState();
|
await checkMaximizedState();
|
||||||
}, 100);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
isMaximized.value = !isMaximized.value;
|
console.error(error)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,7 +81,7 @@ const closeWindow = async () => {
|
|||||||
try {
|
try {
|
||||||
await runtime.Window.Close();
|
await runtime.Window.Close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error handling
|
console.error(error)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,7 +89,7 @@ const checkMaximizedState = async () => {
|
|||||||
try {
|
try {
|
||||||
isMaximized.value = await runtime.Window.IsMaximised();
|
isMaximized.value = await runtime.Window.IsMaximised();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error handling
|
console.error(error)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,24 +101,6 @@ const titleText = computed(() => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await checkMaximizedState();
|
await checkMaximizedState();
|
||||||
|
|
||||||
runtime.Events.On('window:maximised', () => {
|
|
||||||
isMaximized.value = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
runtime.Events.On('window:unmaximised', () => {
|
|
||||||
isMaximized.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
runtime.Events.On('window:focus', async () => {
|
|
||||||
await checkMaximizedState();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
runtime.Events.Off('window:maximised');
|
|
||||||
runtime.Events.Off('window:unmaximised');
|
|
||||||
runtime.Events.Off('window:focus');
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="windows-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
|
<div class="windows-titlebar" style="--wails-draggable:drag">
|
||||||
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
|
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
|
||||||
<div class="titlebar-icon">
|
<div class="titlebar-icon">
|
||||||
<img src="/appicon.png" alt="voidraft"/>
|
<img src="/appicon.png" alt="voidraft"/>
|
||||||
@@ -36,10 +36,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, onMounted, onUnmounted, ref} from 'vue';
|
import {computed, onMounted, ref} from 'vue';
|
||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
import { useWindowStore } from '@/stores/windowStore';
|
|
||||||
import {useDocumentStore} from '@/stores/documentStore';
|
import {useDocumentStore} from '@/stores/documentStore';
|
||||||
|
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
@@ -59,30 +58,16 @@ const minimizeWindow = async () => {
|
|||||||
try {
|
try {
|
||||||
await runtime.Window.Minimise();
|
await runtime.Window.Minimise();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error handling
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleMaximize = async () => {
|
const toggleMaximize = async () => {
|
||||||
try {
|
try {
|
||||||
// 立即更新UI状态,提供即时反馈
|
await runtime.Window.ToggleMaximise();
|
||||||
const newState = !isMaximized.value;
|
|
||||||
isMaximized.value = newState;
|
|
||||||
|
|
||||||
// 然后执行实际操作
|
|
||||||
if (newState) {
|
|
||||||
await runtime.Window.Maximise();
|
|
||||||
} else {
|
|
||||||
await runtime.Window.UnMaximise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 操作完成后再次确认状态(防止操作失败时状态不一致)
|
|
||||||
setTimeout(async () => {
|
|
||||||
await checkMaximizedState();
|
await checkMaximizedState();
|
||||||
}, 100);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 如果操作失败,恢复原状态
|
console.error(error);
|
||||||
isMaximized.value = !isMaximized.value;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,7 +75,7 @@ const closeWindow = async () => {
|
|||||||
try {
|
try {
|
||||||
await runtime.Window.Close();
|
await runtime.Window.Close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error handling
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,31 +83,12 @@ const checkMaximizedState = async () => {
|
|||||||
try {
|
try {
|
||||||
isMaximized.value = await runtime.Window.IsMaximised();
|
isMaximized.value = await runtime.Window.IsMaximised();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error handling
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await checkMaximizedState();
|
await checkMaximizedState();
|
||||||
|
|
||||||
runtime.Events.On('window:maximised', () => {
|
|
||||||
isMaximized.value = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
runtime.Events.On('window:unmaximised', () => {
|
|
||||||
isMaximized.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
runtime.Events.On('window:focus', async () => {
|
|
||||||
await checkMaximizedState();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
runtime.Events.Off('window:maximised');
|
|
||||||
runtime.Events.Off('window:unmaximised');
|
|
||||||
runtime.Events.Off('window:focus');
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,12 @@ const selectItem = async (item: any) => {
|
|||||||
// 选择文档
|
// 选择文档
|
||||||
const selectDoc = async (doc: Document) => {
|
const selectDoc = async (doc: Document) => {
|
||||||
try {
|
try {
|
||||||
|
// 如果选择的就是当前文档,直接关闭菜单
|
||||||
|
if (documentStore.currentDocument?.id === doc.id) {
|
||||||
|
closeMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
||||||
if (hasOpen) {
|
if (hasOpen) {
|
||||||
// 设置错误状态并启动定时器
|
// 设置错误状态并启动定时器
|
||||||
@@ -208,16 +214,41 @@ const openInNewWindow = async (doc: Document, event: Event) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理删除 - 简化确认机制
|
// 处理删除
|
||||||
const handleDelete = async (doc: Document, event: Event) => {
|
const handleDelete = async (doc: Document, event: Event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (deleteConfirmId.value === doc.id) {
|
if (deleteConfirmId.value === doc.id) {
|
||||||
// 确认删除
|
// 确认删除前检查文档是否在其他窗口打开
|
||||||
try {
|
try {
|
||||||
await documentStore.deleteDocument(doc.id);
|
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
||||||
await documentStore.updateDocuments();
|
if (hasOpen) {
|
||||||
|
// 设置错误状态并启动定时器
|
||||||
|
alreadyOpenDocId.value = doc.id;
|
||||||
|
|
||||||
|
// 清除之前的定时器(如果存在)
|
||||||
|
if (errorMessageTimer.value) {
|
||||||
|
clearTimeout(errorMessageTimer.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置新的定时器,3秒后清除错误信息
|
||||||
|
errorMessageTimer.value = window.setTimeout(() => {
|
||||||
|
alreadyOpenDocId.value = null;
|
||||||
|
errorMessageTimer.value = null;
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
// 取消删除确认状态
|
||||||
|
deleteConfirmId.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSuccess = await documentStore.deleteDocument(doc.id);
|
||||||
|
|
||||||
|
if (!deleteSuccess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await documentStore.updateDocuments();
|
||||||
// 如果删除的是当前文档,切换到第一个文档
|
// 如果删除的是当前文档,切换到第一个文档
|
||||||
if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) {
|
if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) {
|
||||||
const firstDoc = documentStore.documentList[0];
|
const firstDoc = documentStore.documentList[0];
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import {onMounted, onUnmounted, ref, watch, computed} from 'vue';
|
import {computed, onMounted, onUnmounted, ref, watch} from 'vue';
|
||||||
import {useConfigStore} from '@/stores/configStore';
|
import {useConfigStore} from '@/stores/configStore';
|
||||||
import {useEditorStore} from '@/stores/editorStore';
|
import {useEditorStore} from '@/stores/editorStore';
|
||||||
import {useUpdateStore} from '@/stores/updateStore';
|
import {useUpdateStore} from '@/stores/updateStore';
|
||||||
import {useWindowStore} from '@/stores/windowStore';
|
import {useWindowStore} from '@/stores/windowStore';
|
||||||
|
import {useSystemStore} from '@/stores/systemStore';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
import {useRouter} from 'vue-router';
|
import {useRouter} from 'vue-router';
|
||||||
import BlockLanguageSelector from './BlockLanguageSelector.vue';
|
import BlockLanguageSelector from './BlockLanguageSelector.vue';
|
||||||
@@ -17,22 +18,32 @@ const editorStore = useEditorStore();
|
|||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const updateStore = useUpdateStore();
|
const updateStore = useUpdateStore();
|
||||||
const windowStore = useWindowStore();
|
const windowStore = useWindowStore();
|
||||||
|
const systemStore = useSystemStore();
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// 当前块是否支持格式化的响应式状态
|
// 当前块是否支持格式化的响应式状态
|
||||||
const canFormatCurrentBlock = ref(false);
|
const canFormatCurrentBlock = ref(false);
|
||||||
|
|
||||||
// 窗口置顶状态管理(仅当前窗口,不同步到配置文件)
|
// 窗口置顶状态 - 合并配置和临时状态
|
||||||
const isCurrentWindowOnTop = ref(false);
|
const isCurrentWindowOnTop = computed(() => {
|
||||||
|
return configStore.config.general.alwaysOnTop || systemStore.isWindowOnTop;
|
||||||
const setWindowAlwaysOnTop = async (isTop: boolean) => {
|
});
|
||||||
await runtime.Window.SetAlwaysOnTop(isTop);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// 切换窗口置顶状态
|
||||||
const toggleAlwaysOnTop = async () => {
|
const toggleAlwaysOnTop = async () => {
|
||||||
isCurrentWindowOnTop.value = !isCurrentWindowOnTop.value;
|
const currentlyOnTop = isCurrentWindowOnTop.value;
|
||||||
await runtime.Window.SetAlwaysOnTop(isCurrentWindowOnTop.value);
|
|
||||||
|
if (currentlyOnTop) {
|
||||||
|
// 如果当前是置顶状态,彻底关闭所有置顶
|
||||||
|
if (configStore.config.general.alwaysOnTop) {
|
||||||
|
await configStore.setAlwaysOnTop(false);
|
||||||
|
}
|
||||||
|
await systemStore.setWindowOnTop(false);
|
||||||
|
} else {
|
||||||
|
// 如果当前不是置顶状态,开启临时置顶
|
||||||
|
await systemStore.setWindowOnTop(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 跳转到设置页面
|
// 跳转到设置页面
|
||||||
@@ -80,7 +91,7 @@ const debouncedUpdateFormatButton = (() => {
|
|||||||
timeout = window.setTimeout(() => {
|
timeout = window.setTimeout(() => {
|
||||||
updateFormatButtonState();
|
updateFormatButtonState();
|
||||||
timeout = null;
|
timeout = null;
|
||||||
}, 300);
|
}, 1000);
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -143,12 +154,28 @@ onUnmounted(() => {
|
|||||||
// 组件加载后初始化置顶状态
|
// 组件加载后初始化置顶状态
|
||||||
watch(isLoaded, async (loaded) => {
|
watch(isLoaded, async (loaded) => {
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
// 初始化时从配置文件读取置顶状态
|
// 应用合并后的置顶状态
|
||||||
isCurrentWindowOnTop.value = configStore.config.general.alwaysOnTop;
|
const shouldBeOnTop = configStore.config.general.alwaysOnTop || systemStore.isWindowOnTop;
|
||||||
await setWindowAlwaysOnTop(isCurrentWindowOnTop.value);
|
try {
|
||||||
|
await runtime.Window.SetAlwaysOnTop(shouldBeOnTop);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to apply window pin state:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 监听配置变化,同步窗口状态
|
||||||
|
watch(
|
||||||
|
() => isCurrentWindowOnTop.value,
|
||||||
|
async (shouldBeOnTop) => {
|
||||||
|
try {
|
||||||
|
await runtime.Window.SetAlwaysOnTop(shouldBeOnTop);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to sync window pin state:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const handleUpdateButtonClick = async () => {
|
const handleUpdateButtonClick = async () => {
|
||||||
if (updateStore.hasUpdate && !updateStore.isUpdating && !updateStore.updateSuccess) {
|
if (updateStore.hasUpdate && !updateStore.isUpdating && !updateStore.updateSuccess) {
|
||||||
// 开始下载更新
|
// 开始下载更新
|
||||||
@@ -230,20 +257,23 @@ const updateButtonTitle = computed(() => {
|
|||||||
@click="handleUpdateButtonClick"
|
@click="handleUpdateButtonClick"
|
||||||
>
|
>
|
||||||
<!-- 检查更新中 -->
|
<!-- 检查更新中 -->
|
||||||
<svg v-if="updateStore.isChecking" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
|
<svg v-if="updateStore.isChecking" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotating">
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotating">
|
||||||
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
|
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<!-- 下载更新中 -->
|
<!-- 下载更新中 -->
|
||||||
<svg v-else-if="updateStore.isUpdating" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
|
<svg v-else-if="updateStore.isUpdating" xmlns="http://www.w3.org/2000/svg" width="14" height="14"
|
||||||
|
viewBox="0 0 24 24" fill="none"
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotating">
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotating">
|
||||||
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
|
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
|
||||||
<path d="M12 2a10 10 0 1 0 10 10"></path>
|
<path d="M12 2a10 10 0 1 0 10 10"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<!-- 更新成功,等待重启 -->
|
<!-- 更新成功,等待重启 -->
|
||||||
<svg v-else-if="updateStore.updateSuccess" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
|
<svg v-else-if="updateStore.updateSuccess" xmlns="http://www.w3.org/2000/svg" width="14" height="14"
|
||||||
|
viewBox="0 0 24 24" fill="none"
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pulsing">
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pulsing">
|
||||||
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path>
|
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path>
|
||||||
<line x1="12" y1="2" x2="12" y2="12"></line>
|
<line x1="12" y1="2" x2="12" y2="12"></line>
|
||||||
@@ -252,7 +282,8 @@ const updateButtonTitle = computed(() => {
|
|||||||
<!-- 有更新可用 -->
|
<!-- 有更新可用 -->
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
|
<svg v-else xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
<path
|
||||||
|
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||||
<polyline points="7.5,10.5 12,15 16.5,10.5"/>
|
<polyline points="7.5,10.5 12,15 16.5,10.5"/>
|
||||||
<polyline points="12,15 12,3"/>
|
<polyline points="12,15 12,3"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export default {
|
|||||||
general: 'General',
|
general: 'General',
|
||||||
editing: 'Editor',
|
editing: 'Editor',
|
||||||
appearance: 'Appearance',
|
appearance: 'Appearance',
|
||||||
|
backupPage: 'Backup',
|
||||||
keyBindings: 'Key Bindings',
|
keyBindings: 'Key Bindings',
|
||||||
updates: 'Updates',
|
updates: 'Updates',
|
||||||
reset: 'Reset',
|
reset: 'Reset',
|
||||||
@@ -133,6 +134,7 @@ export default {
|
|||||||
showInSystemTray: 'Show in System Tray',
|
showInSystemTray: 'Show in System Tray',
|
||||||
enableSystemTray: 'Enable System Tray',
|
enableSystemTray: 'Enable System Tray',
|
||||||
alwaysOnTop: 'Always on Top',
|
alwaysOnTop: 'Always on Top',
|
||||||
|
enableWindowSnap: 'Enable Window Snapping',
|
||||||
startup: 'Startup Settings',
|
startup: 'Startup Settings',
|
||||||
startAtLogin: 'Start at Login',
|
startAtLogin: 'Start at Login',
|
||||||
dataStorage: 'Data Storage',
|
dataStorage: 'Data Storage',
|
||||||
@@ -242,6 +244,49 @@ export default {
|
|||||||
restartNow: 'Restart Now',
|
restartNow: 'Restart Now',
|
||||||
hotkeyPreview: 'Preview:',
|
hotkeyPreview: 'Preview:',
|
||||||
none: 'None',
|
none: 'None',
|
||||||
|
backup: {
|
||||||
|
basicSettings: 'Basic Settings',
|
||||||
|
enableBackup: 'Enable Git Backup',
|
||||||
|
autoBackup: 'Auto Backup',
|
||||||
|
backupInterval: 'Backup Interval',
|
||||||
|
intervals: {
|
||||||
|
'5min': '5 minutes',
|
||||||
|
'10min': '10 minutes',
|
||||||
|
'15min': '15 minutes',
|
||||||
|
'30min': '30 minutes',
|
||||||
|
'1hour': '1 hour'
|
||||||
|
},
|
||||||
|
repositoryConfig: 'Repository Configuration',
|
||||||
|
repoUrl: 'Repository URL',
|
||||||
|
repoUrlPlaceholder: 'Enter Git repository URL',
|
||||||
|
authConfig: 'Authentication Configuration',
|
||||||
|
authMethod: 'Authentication Method',
|
||||||
|
authMethods: {
|
||||||
|
token: 'Access Token',
|
||||||
|
sshKey: 'SSH Key',
|
||||||
|
userPass: 'Username/Password'
|
||||||
|
},
|
||||||
|
username: 'Username',
|
||||||
|
usernamePlaceholder: 'Enter username',
|
||||||
|
password: 'Password',
|
||||||
|
passwordPlaceholder: 'Enter password',
|
||||||
|
token: 'Access Token',
|
||||||
|
tokenPlaceholder: 'Enter access token',
|
||||||
|
sshKeyPath: 'SSH Key Path',
|
||||||
|
sshKeyPathPlaceholder: 'Select SSH key file',
|
||||||
|
sshKeyPassphrase: 'SSH Key Passphrase',
|
||||||
|
sshKeyPassphrasePlaceholder: 'Enter SSH key passphrase',
|
||||||
|
backupOperations: 'Backup Operations',
|
||||||
|
pushToRemote: 'Push to Remote',
|
||||||
|
pushing: 'Pushing...',
|
||||||
|
actions: {
|
||||||
|
push: 'Push',
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
success: 'Success',
|
||||||
|
failed: 'Failed'
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extensions: {
|
extensions: {
|
||||||
rainbowBrackets: {
|
rainbowBrackets: {
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export default {
|
|||||||
general: '常规',
|
general: '常规',
|
||||||
editing: '编辑器',
|
editing: '编辑器',
|
||||||
appearance: '外观',
|
appearance: '外观',
|
||||||
|
backupPage: '备份',
|
||||||
extensions: '扩展',
|
extensions: '扩展',
|
||||||
keyBindings: '快捷键',
|
keyBindings: '快捷键',
|
||||||
updates: '更新',
|
updates: '更新',
|
||||||
@@ -134,6 +135,7 @@ export default {
|
|||||||
showInSystemTray: '在系统托盘中显示',
|
showInSystemTray: '在系统托盘中显示',
|
||||||
enableSystemTray: '启用系统托盘',
|
enableSystemTray: '启用系统托盘',
|
||||||
alwaysOnTop: '窗口始终置顶',
|
alwaysOnTop: '窗口始终置顶',
|
||||||
|
enableWindowSnap: '启用窗口吸附',
|
||||||
startup: '启动设置',
|
startup: '启动设置',
|
||||||
startAtLogin: '开机自启动',
|
startAtLogin: '开机自启动',
|
||||||
dataStorage: '数据存储',
|
dataStorage: '数据存储',
|
||||||
@@ -243,6 +245,49 @@ export default {
|
|||||||
},
|
},
|
||||||
hotkeyPreview: '预览:',
|
hotkeyPreview: '预览:',
|
||||||
none: '无',
|
none: '无',
|
||||||
|
backup: {
|
||||||
|
basicSettings: '基本设置',
|
||||||
|
enableBackup: '启用备份',
|
||||||
|
autoBackup: '自动备份',
|
||||||
|
backupInterval: '备份间隔',
|
||||||
|
intervals: {
|
||||||
|
'5min': '5分钟',
|
||||||
|
'10min': '10分钟',
|
||||||
|
'15min': '15分钟',
|
||||||
|
'30min': '30分钟',
|
||||||
|
'1hour': '1小时'
|
||||||
|
},
|
||||||
|
repositoryConfig: '仓库配置',
|
||||||
|
repoUrl: '仓库地址',
|
||||||
|
repoUrlPlaceholder: '请输入Git仓库地址',
|
||||||
|
authConfig: '认证配置',
|
||||||
|
authMethod: '认证方式',
|
||||||
|
authMethods: {
|
||||||
|
token: '访问令牌',
|
||||||
|
sshKey: 'SSH密钥',
|
||||||
|
userPass: '用户名密码'
|
||||||
|
},
|
||||||
|
username: '用户名',
|
||||||
|
usernamePlaceholder: '请输入用户名',
|
||||||
|
password: '密码',
|
||||||
|
passwordPlaceholder: '请输入密码',
|
||||||
|
token: '访问令牌',
|
||||||
|
tokenPlaceholder: '请输入访问令牌',
|
||||||
|
sshKeyPath: 'SSH密钥路径',
|
||||||
|
sshKeyPathPlaceholder: '请选择SSH密钥文件',
|
||||||
|
sshKeyPassphrase: 'SSH密钥密码',
|
||||||
|
sshKeyPassphrasePlaceholder: '请输入SSH密钥密码',
|
||||||
|
backupOperations: '备份操作',
|
||||||
|
pushToRemote: '推送到远程',
|
||||||
|
pushing: '推送中...',
|
||||||
|
actions: {
|
||||||
|
push: '推送',
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
success: '成功',
|
||||||
|
failed: '失败'
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extensions: {
|
extensions: {
|
||||||
rainbowBrackets: {
|
rainbowBrackets: {
|
||||||
|
|||||||
@@ -7,19 +7,12 @@ import AppearancePage from '@/views/settings/pages/AppearancePage.vue';
|
|||||||
import KeyBindingsPage from '@/views/settings/pages/KeyBindingsPage.vue';
|
import KeyBindingsPage from '@/views/settings/pages/KeyBindingsPage.vue';
|
||||||
import UpdatesPage from '@/views/settings/pages/UpdatesPage.vue';
|
import UpdatesPage from '@/views/settings/pages/UpdatesPage.vue';
|
||||||
import ExtensionsPage from '@/views/settings/pages/ExtensionsPage.vue';
|
import ExtensionsPage from '@/views/settings/pages/ExtensionsPage.vue';
|
||||||
|
import BackupPage from '@/views/settings/pages/BackupPage.vue';
|
||||||
|
// 测试页面
|
||||||
|
import TestPage from '@/views/settings/pages/TestPage.vue';
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
// 基础设置子路由
|
||||||
{
|
const settingsChildren: RouteRecordRaw[] = [
|
||||||
path: '/',
|
|
||||||
name: 'Editor',
|
|
||||||
component: Editor
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/settings',
|
|
||||||
name: 'Settings',
|
|
||||||
redirect: '/settings/general',
|
|
||||||
component: Settings,
|
|
||||||
children: [
|
|
||||||
{
|
{
|
||||||
path: 'general',
|
path: 'general',
|
||||||
name: 'SettingsGeneral',
|
name: 'SettingsGeneral',
|
||||||
@@ -49,8 +42,35 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: 'updates',
|
path: 'updates',
|
||||||
name: 'SettingsUpdates',
|
name: 'SettingsUpdates',
|
||||||
component: UpdatesPage
|
component: UpdatesPage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'backup',
|
||||||
|
name: 'SettingsBackup',
|
||||||
|
component: BackupPage
|
||||||
}
|
}
|
||||||
]
|
];
|
||||||
|
|
||||||
|
// 仅在开发环境添加测试页面路由
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
settingsChildren.push({
|
||||||
|
path: 'test',
|
||||||
|
name: 'SettingsTest',
|
||||||
|
component: TestPage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Editor',
|
||||||
|
component: Editor
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
name: 'Settings',
|
||||||
|
redirect: '/settings/general',
|
||||||
|
component: Settings,
|
||||||
|
children: settingsChildren
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import {defineStore} from 'pinia'
|
||||||
|
import {computed, readonly, ref} from 'vue'
|
||||||
|
import type {GitBackupConfig} from '@/../bindings/voidraft/internal/models'
|
||||||
|
import {BackupService} from '@/../bindings/voidraft/internal/services'
|
||||||
|
import {useConfigStore} from '@/stores/configStore'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimalist Backup Store
|
||||||
|
*/
|
||||||
|
export const useBackupStore = defineStore('backup', () => {
|
||||||
|
// Core state
|
||||||
|
const config = ref<GitBackupConfig | null>(null)
|
||||||
|
const isPushing = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
const isInitialized = ref(false)
|
||||||
|
|
||||||
|
// Backup result states
|
||||||
|
const pushSuccess = ref(false)
|
||||||
|
const pushError = ref(false)
|
||||||
|
|
||||||
|
// Timers for auto-hiding status icons and error messages
|
||||||
|
let pushStatusTimer: number | null = null
|
||||||
|
let errorTimer: number | null = null
|
||||||
|
|
||||||
|
// 获取configStore
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
|
// Computed properties
|
||||||
|
const isEnabled = computed(() => configStore.config.backup.enabled)
|
||||||
|
const isConfigured = computed(() => configStore.config.backup.repo_url)
|
||||||
|
|
||||||
|
// 清除状态显示
|
||||||
|
const clearPushStatus = () => {
|
||||||
|
if (pushStatusTimer !== null) {
|
||||||
|
window.clearTimeout(pushStatusTimer)
|
||||||
|
pushStatusTimer = null
|
||||||
|
}
|
||||||
|
pushSuccess.value = false
|
||||||
|
pushError.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除错误信息和错误图标
|
||||||
|
const clearError = () => {
|
||||||
|
if (errorTimer !== null) {
|
||||||
|
window.clearTimeout(errorTimer)
|
||||||
|
errorTimer = null
|
||||||
|
}
|
||||||
|
error.value = null
|
||||||
|
pushError.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置错误信息和错误图标并自动清除
|
||||||
|
const setErrorWithAutoHide = (errorMessage: string, hideAfter: number = 3000) => {
|
||||||
|
clearError()
|
||||||
|
clearPushStatus()
|
||||||
|
error.value = errorMessage
|
||||||
|
pushError.value = true
|
||||||
|
errorTimer = window.setTimeout(() => {
|
||||||
|
error.value = null
|
||||||
|
pushError.value = false
|
||||||
|
errorTimer = null
|
||||||
|
}, hideAfter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push to remote repository
|
||||||
|
const pushToRemote = async () => {
|
||||||
|
if (isPushing.value || !isConfigured.value) return
|
||||||
|
|
||||||
|
isPushing.value = true
|
||||||
|
clearError() // 清除之前的错误信息
|
||||||
|
clearPushStatus()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await BackupService.PushToRemote()
|
||||||
|
// 显示成功状态,并设置3秒后自动消失
|
||||||
|
pushSuccess.value = true
|
||||||
|
pushStatusTimer = window.setTimeout(() => {
|
||||||
|
pushSuccess.value = false
|
||||||
|
pushStatusTimer = null
|
||||||
|
}, 3000)
|
||||||
|
} catch (err: any) {
|
||||||
|
setErrorWithAutoHide(err?.message || 'Backup operation failed')
|
||||||
|
} finally {
|
||||||
|
isPushing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化备份服务
|
||||||
|
const initialize = async () => {
|
||||||
|
if (!isEnabled.value) return
|
||||||
|
|
||||||
|
// 避免重复初始化
|
||||||
|
if (isInitialized.value) return
|
||||||
|
|
||||||
|
clearError() // 清除之前的错误信息
|
||||||
|
try {
|
||||||
|
await BackupService.Initialize()
|
||||||
|
isInitialized.value = true
|
||||||
|
} catch (err: any) {
|
||||||
|
setErrorWithAutoHide(err?.message || 'Failed to initialize backup service')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
config: readonly(config),
|
||||||
|
isPushing: readonly(isPushing),
|
||||||
|
error: readonly(error),
|
||||||
|
isInitialized: readonly(isInitialized),
|
||||||
|
pushSuccess: readonly(pushSuccess),
|
||||||
|
pushError: readonly(pushError),
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
isEnabled,
|
||||||
|
isConfigured,
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
pushToRemote,
|
||||||
|
initialize,
|
||||||
|
clearError
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -11,10 +11,11 @@ import {
|
|||||||
TabType,
|
TabType,
|
||||||
UpdatesConfig,
|
UpdatesConfig,
|
||||||
UpdateSourceType,
|
UpdateSourceType,
|
||||||
|
GitBackupConfig,
|
||||||
|
AuthMethod
|
||||||
} from '@/../bindings/voidraft/internal/models/models';
|
} from '@/../bindings/voidraft/internal/models/models';
|
||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import {ConfigUtils} from '@/utils/configUtils';
|
import {ConfigUtils} from '@/utils/configUtils';
|
||||||
import {WindowController} from '@/utils/windowController';
|
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
// 国际化相关导入
|
// 国际化相关导入
|
||||||
export type SupportedLocaleType = 'zh-CN' | 'en-US';
|
export type SupportedLocaleType = 'zh-CN' | 'en-US';
|
||||||
@@ -48,6 +49,10 @@ type UpdatesConfigKeyMap = {
|
|||||||
readonly [K in keyof UpdatesConfig]: string;
|
readonly [K in keyof UpdatesConfig]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BackupConfigKeyMap = {
|
||||||
|
readonly [K in keyof GitBackupConfig]: string;
|
||||||
|
};
|
||||||
|
|
||||||
type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight';
|
type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight';
|
||||||
|
|
||||||
// 配置键映射
|
// 配置键映射
|
||||||
@@ -57,7 +62,8 @@ const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
|
|||||||
enableSystemTray: 'general.enableSystemTray',
|
enableSystemTray: 'general.enableSystemTray',
|
||||||
startAtLogin: 'general.startAtLogin',
|
startAtLogin: 'general.startAtLogin',
|
||||||
enableGlobalHotkey: 'general.enableGlobalHotkey',
|
enableGlobalHotkey: 'general.enableGlobalHotkey',
|
||||||
globalHotkey: 'general.globalHotkey'
|
globalHotkey: 'general.globalHotkey',
|
||||||
|
enableWindowSnap: 'general.enableWindowSnap',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
|
const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
|
||||||
@@ -73,8 +79,7 @@ const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
|
|||||||
|
|
||||||
const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
|
const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
|
||||||
language: 'appearance.language',
|
language: 'appearance.language',
|
||||||
systemTheme: 'appearance.systemTheme',
|
systemTheme: 'appearance.systemTheme'
|
||||||
customTheme: 'appearance.customTheme'
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
|
const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
|
||||||
@@ -88,6 +93,20 @@ const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
|
|||||||
gitea: 'updates.gitea'
|
gitea: 'updates.gitea'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const BACKUP_CONFIG_KEY_MAP: BackupConfigKeyMap = {
|
||||||
|
enabled: 'backup.enabled',
|
||||||
|
repo_url: 'backup.repo_url',
|
||||||
|
auth_method: 'backup.auth_method',
|
||||||
|
username: 'backup.username',
|
||||||
|
password: 'backup.password',
|
||||||
|
token: 'backup.token',
|
||||||
|
ssh_key_path: 'backup.ssh_key_path',
|
||||||
|
ssh_key_passphrase: 'backup.ssh_key_passphrase',
|
||||||
|
backup_interval: 'backup.backup_interval',
|
||||||
|
auto_backup: 'backup.auto_backup',
|
||||||
|
|
||||||
|
} as const;
|
||||||
|
|
||||||
// 配置限制
|
// 配置限制
|
||||||
const CONFIG_LIMITS = {
|
const CONFIG_LIMITS = {
|
||||||
fontSize: {min: 12, max: 28, default: 13},
|
fontSize: {min: 12, max: 28, default: 13},
|
||||||
@@ -158,7 +177,8 @@ const DEFAULT_CONFIG: AppConfig = {
|
|||||||
alt: true,
|
alt: true,
|
||||||
win: false,
|
win: false,
|
||||||
key: 'X'
|
key: 'X'
|
||||||
}
|
},
|
||||||
|
enableWindowSnap: true,
|
||||||
},
|
},
|
||||||
editing: {
|
editing: {
|
||||||
fontSize: CONFIG_LIMITS.fontSize.default,
|
fontSize: CONFIG_LIMITS.fontSize.default,
|
||||||
@@ -172,77 +192,7 @@ const DEFAULT_CONFIG: AppConfig = {
|
|||||||
},
|
},
|
||||||
appearance: {
|
appearance: {
|
||||||
language: LanguageType.LangZhCN,
|
language: LanguageType.LangZhCN,
|
||||||
systemTheme: SystemThemeType.SystemThemeAuto,
|
systemTheme: SystemThemeType.SystemThemeAuto
|
||||||
customTheme: {
|
|
||||||
darkTheme: {
|
|
||||||
// 基础色调
|
|
||||||
background: '#252B37',
|
|
||||||
backgroundSecondary: '#213644',
|
|
||||||
surface: '#474747',
|
|
||||||
foreground: '#9BB586',
|
|
||||||
foregroundSecondary: '#9c9c9c',
|
|
||||||
|
|
||||||
// 语法高亮
|
|
||||||
comment: '#6272a4',
|
|
||||||
keyword: '#ff79c6',
|
|
||||||
string: '#f1fa8c',
|
|
||||||
function: '#50fa7b',
|
|
||||||
number: '#bd93f9',
|
|
||||||
operator: '#ff79c6',
|
|
||||||
variable: '#8fbcbb',
|
|
||||||
type: '#8be9fd',
|
|
||||||
|
|
||||||
// 界面元素
|
|
||||||
cursor: '#fff',
|
|
||||||
selection: '#0865a9aa',
|
|
||||||
selectionBlur: '#225377aa',
|
|
||||||
activeLine: 'rgba(255,255,255,0.04)',
|
|
||||||
lineNumber: 'rgba(255,255,255, 0.15)',
|
|
||||||
activeLineNumber: 'rgba(255,255,255, 0.6)',
|
|
||||||
|
|
||||||
// 边框分割线
|
|
||||||
borderColor: '#1e222a',
|
|
||||||
borderLight: 'rgba(255,255,255, 0.1)',
|
|
||||||
|
|
||||||
// 搜索匹配
|
|
||||||
searchMatch: '#8fbcbb',
|
|
||||||
matchingBracket: 'rgba(255,255,255,0.1)'
|
|
||||||
},
|
|
||||||
lightTheme: {
|
|
||||||
// 基础色调
|
|
||||||
background: '#ffffff',
|
|
||||||
backgroundSecondary: '#f1faf1',
|
|
||||||
surface: '#f5f5f5',
|
|
||||||
foreground: '#444d56',
|
|
||||||
foregroundSecondary: '#6a737d',
|
|
||||||
|
|
||||||
// 语法高亮
|
|
||||||
comment: '#6a737d',
|
|
||||||
keyword: '#d73a49',
|
|
||||||
string: '#032f62',
|
|
||||||
function: '#005cc5',
|
|
||||||
number: '#005cc5',
|
|
||||||
operator: '#d73a49',
|
|
||||||
variable: '#24292e',
|
|
||||||
type: '#6f42c1',
|
|
||||||
|
|
||||||
// 界面元素
|
|
||||||
cursor: '#000',
|
|
||||||
selection: '#77baff8c',
|
|
||||||
selectionBlur: '#b2c2ca85',
|
|
||||||
activeLine: '#000000',
|
|
||||||
lineNumber: '#000000',
|
|
||||||
activeLineNumber: '#000000',
|
|
||||||
|
|
||||||
// 边框分割线
|
|
||||||
borderColor: '#dfdfdf',
|
|
||||||
borderLight: '#0000000C',
|
|
||||||
|
|
||||||
// 搜索匹配
|
|
||||||
searchMatch: '#005cc5',
|
|
||||||
matchingBracket: 'rgba(0,0,0,0.1)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
updates: {
|
updates: {
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
@@ -261,6 +211,18 @@ const DEFAULT_CONFIG: AppConfig = {
|
|||||||
repo: "voidraft",
|
repo: "voidraft",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
backup: {
|
||||||
|
enabled: false,
|
||||||
|
repo_url: "",
|
||||||
|
auth_method: AuthMethod.UserPass,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
token: "",
|
||||||
|
ssh_key_path: "",
|
||||||
|
ssh_key_passphrase: "",
|
||||||
|
backup_interval: 60,
|
||||||
|
auto_backup: true,
|
||||||
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
lastUpdated: new Date().toString(),
|
lastUpdated: new Date().toString(),
|
||||||
@@ -348,6 +310,21 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
state.config.updates[key] = value;
|
state.config.updates[key] = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateBackupConfig = async <K extends keyof GitBackupConfig>(key: K, value: GitBackupConfig[K]): Promise<void> => {
|
||||||
|
// 确保配置已加载
|
||||||
|
if (!state.configLoaded && !state.isLoading) {
|
||||||
|
await initConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
const backendKey = BACKUP_CONFIG_KEY_MAP[key];
|
||||||
|
if (!backendKey) {
|
||||||
|
throw new Error(`No backend key mapping found for backup.${key.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ConfigService.Set(backendKey, value);
|
||||||
|
state.config.backup[key] = value;
|
||||||
|
};
|
||||||
|
|
||||||
// 加载配置
|
// 加载配置
|
||||||
const initConfig = async (): Promise<void> => {
|
const initConfig = async (): Promise<void> => {
|
||||||
if (state.isLoading) return;
|
if (state.isLoading) return;
|
||||||
@@ -362,14 +339,12 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
if (appConfig.editing) Object.assign(state.config.editing, appConfig.editing);
|
if (appConfig.editing) Object.assign(state.config.editing, appConfig.editing);
|
||||||
if (appConfig.appearance) Object.assign(state.config.appearance, appConfig.appearance);
|
if (appConfig.appearance) Object.assign(state.config.appearance, appConfig.appearance);
|
||||||
if (appConfig.updates) Object.assign(state.config.updates, appConfig.updates);
|
if (appConfig.updates) Object.assign(state.config.updates, appConfig.updates);
|
||||||
|
if (appConfig.backup) Object.assign(state.config.backup, appConfig.backup);
|
||||||
if (appConfig.metadata) Object.assign(state.config.metadata, appConfig.metadata);
|
if (appConfig.metadata) Object.assign(state.config.metadata, appConfig.metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.configLoaded = true;
|
state.configLoaded = true;
|
||||||
|
|
||||||
// 初始化热键监听器
|
|
||||||
const windowController = WindowController.getInstance();
|
|
||||||
await windowController.initializeHotkeyListener();
|
|
||||||
} finally {
|
} finally {
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
}
|
}
|
||||||
@@ -434,50 +409,7 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
await updateAppearanceConfig('systemTheme', systemTheme);
|
await updateAppearanceConfig('systemTheme', systemTheme);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新自定义主题方法
|
|
||||||
const updateCustomTheme = async (themeType: 'darkTheme' | 'lightTheme', colorKey: string, colorValue: string): Promise<void> => {
|
|
||||||
// 确保配置已加载
|
|
||||||
if (!state.configLoaded && !state.isLoading) {
|
|
||||||
await initConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 深拷贝当前配置
|
|
||||||
const customTheme = JSON.parse(JSON.stringify(state.config.appearance.customTheme));
|
|
||||||
|
|
||||||
// 更新对应主题的颜色值
|
|
||||||
customTheme[themeType][colorKey] = colorValue;
|
|
||||||
|
|
||||||
// 更新整个自定义主题配置到后端
|
|
||||||
await ConfigService.Set(APPEARANCE_CONFIG_KEY_MAP.customTheme, customTheme);
|
|
||||||
|
|
||||||
// 更新前端状态
|
|
||||||
state.config.appearance.customTheme = customTheme;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 设置整个自定义主题配置
|
|
||||||
const setCustomTheme = async (customTheme: any): Promise<void> => {
|
|
||||||
// 确保配置已加载
|
|
||||||
if (!state.configLoaded && !state.isLoading) {
|
|
||||||
await initConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 更新整个自定义主题配置到后端
|
|
||||||
await ConfigService.Set(APPEARANCE_CONFIG_KEY_MAP.customTheme, customTheme);
|
|
||||||
|
|
||||||
// 更新前端状态
|
|
||||||
state.config.appearance.customTheme = customTheme;
|
|
||||||
|
|
||||||
// 确保Vue能检测到变化
|
|
||||||
state.config.appearance = { ...state.config.appearance };
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化语言设置
|
// 初始化语言设置
|
||||||
const initializeLanguage = async (): Promise<void> => {
|
const initializeLanguage = async (): Promise<void> => {
|
||||||
@@ -542,8 +474,6 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
|
|
||||||
// 主题相关方法
|
// 主题相关方法
|
||||||
setSystemTheme,
|
setSystemTheme,
|
||||||
updateCustomTheme,
|
|
||||||
setCustomTheme,
|
|
||||||
|
|
||||||
// 字体大小操作
|
// 字体大小操作
|
||||||
...adjusters.fontSize,
|
...adjusters.fontSize,
|
||||||
@@ -593,7 +523,22 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
await StartupService.SetEnabled(value);
|
await StartupService.SetEnabled(value);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 窗口吸附配置相关方法
|
||||||
|
setEnableWindowSnap: async (value: boolean) => await updateGeneralConfig('enableWindowSnap', value),
|
||||||
|
|
||||||
// 更新配置相关方法
|
// 更新配置相关方法
|
||||||
setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value)
|
setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value),
|
||||||
|
|
||||||
|
// 备份配置相关方法
|
||||||
|
setEnableBackup: async (value: boolean) => {await updateBackupConfig('enabled', value);},
|
||||||
|
setAutoBackup: async (value: boolean) => {await updateBackupConfig('auto_backup', value);},
|
||||||
|
setRepoUrl: async (value: string) => await updateBackupConfig('repo_url', value),
|
||||||
|
setAuthMethod: async (value: AuthMethod) => await updateBackupConfig('auth_method', value),
|
||||||
|
setUsername: async (value: string) => await updateBackupConfig('username', value),
|
||||||
|
setPassword: async (value: string) => await updateBackupConfig('password', value),
|
||||||
|
setToken: async (value: string) => await updateBackupConfig('token', value),
|
||||||
|
setSshKeyPath: async (value: string) => await updateBackupConfig('ssh_key_path', value),
|
||||||
|
setSshKeyPassphrase: async (value: string) => await updateBackupConfig('ssh_key_passphrase', value),
|
||||||
|
setBackupInterval: async (value: number) => await updateBackupConfig('backup_interval', value),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -3,13 +3,15 @@ import {computed, ref} from 'vue';
|
|||||||
import {DocumentService} from '@/../bindings/voidraft/internal/services';
|
import {DocumentService} from '@/../bindings/voidraft/internal/services';
|
||||||
import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice';
|
import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice';
|
||||||
import {Document} from '@/../bindings/voidraft/internal/models/models';
|
import {Document} from '@/../bindings/voidraft/internal/models/models';
|
||||||
|
import {useSystemStore} from './systemStore';
|
||||||
|
|
||||||
const SCRATCH_DOCUMENT_ID = 1; // 默认草稿文档ID
|
|
||||||
|
|
||||||
export const useDocumentStore = defineStore('document', () => {
|
export const useDocumentStore = defineStore('document', () => {
|
||||||
|
|
||||||
|
const DEFAULT_DOCUMENT_ID = ref<number>(1); // 默认草稿文档ID
|
||||||
// === 核心状态 ===
|
// === 核心状态 ===
|
||||||
const documents = ref<Record<number, Document>>({});
|
const documents = ref<Record<number, Document>>({});
|
||||||
const recentDocumentIds = ref<number[]>([SCRATCH_DOCUMENT_ID]);
|
const recentDocumentIds = ref<number[]>([DEFAULT_DOCUMENT_ID.value]);
|
||||||
const currentDocumentId = ref<number | null>(null);
|
const currentDocumentId = ref<number | null>(null);
|
||||||
const currentDocument = ref<Document | null>(null);
|
const currentDocument = ref<Document | null>(null);
|
||||||
|
|
||||||
@@ -159,7 +161,7 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
const deleteDocument = async (docId: number): Promise<boolean> => {
|
const deleteDocument = async (docId: number): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
// 检查是否是默认文档(使用ID判断)
|
// 检查是否是默认文档(使用ID判断)
|
||||||
if (docId === SCRATCH_DOCUMENT_ID) {
|
if (docId === DEFAULT_DOCUMENT_ID.value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,6 +223,7 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
DEFAULT_DOCUMENT_ID,
|
||||||
// 状态
|
// 状态
|
||||||
documents,
|
documents,
|
||||||
documentList,
|
documentList,
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ export const useSystemStore = defineStore('system', () => {
|
|||||||
const environment = ref<SystemEnvironment | null>(null);
|
const environment = ref<SystemEnvironment | null>(null);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
// 窗口置顶状态管理
|
||||||
|
const isWindowOnTop = ref<boolean>(false);
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const isWindows = computed(() => environment.value?.OS === 'windows');
|
const isWindows = computed(() => environment.value?.OS === 'windows');
|
||||||
const isMacOS = computed(() => environment.value?.OS === 'darwin');
|
const isMacOS = computed(() => environment.value?.OS === 'darwin');
|
||||||
const isLinux = computed(() => environment.value?.OS === 'linux');
|
const isLinux = computed(() => environment.value?.OS === 'linux');
|
||||||
|
|
||||||
|
|
||||||
// 获取标题栏高度
|
// 获取标题栏高度
|
||||||
const titleBarHeight = computed(() => {
|
const titleBarHeight = computed(() => {
|
||||||
if (isWindows.value) return '32px';
|
if (isWindows.value) return '32px';
|
||||||
@@ -49,10 +50,31 @@ export const useSystemStore = defineStore('system', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 设置窗口置顶状态
|
||||||
|
const setWindowOnTop = async (isPinned: boolean): Promise<void> => {
|
||||||
|
isWindowOnTop.value = isPinned;
|
||||||
|
try {
|
||||||
|
await runtime.Window.SetAlwaysOnTop(isPinned);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set window always on top:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换窗口置顶状态
|
||||||
|
const toggleWindowOnTop = async (): Promise<void> => {
|
||||||
|
await setWindowOnTop(!isWindowOnTop.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置临时置顶状态(不调用系统API)
|
||||||
|
const resetWindowOnTop = (): void => {
|
||||||
|
isWindowOnTop.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 状态
|
// 状态
|
||||||
environment,
|
environment,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isWindowOnTop,
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
isWindows,
|
isWindows,
|
||||||
@@ -62,5 +84,14 @@ export const useSystemStore = defineStore('system', () => {
|
|||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
initializeSystemInfo,
|
initializeSystemInfo,
|
||||||
|
setWindowOnTop,
|
||||||
|
toggleWindowOnTop,
|
||||||
|
resetWindowOnTop,
|
||||||
};
|
};
|
||||||
|
}, {
|
||||||
|
persist: {
|
||||||
|
key: 'voidraft-system',
|
||||||
|
storage: localStorage,
|
||||||
|
pick: ['isWindowOnTop']
|
||||||
|
}
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {defineStore} from 'pinia';
|
import {defineStore} from 'pinia';
|
||||||
import {computed, reactive} from 'vue';
|
import {computed, reactive} from 'vue';
|
||||||
import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
|
import {SystemThemeType, ThemeType, ThemeColorConfig} from '@/../bindings/voidraft/internal/models/models';
|
||||||
|
import {ThemeService} from '@/../bindings/voidraft/internal/services';
|
||||||
import {useConfigStore} from './configStore';
|
import {useConfigStore} from './configStore';
|
||||||
import {useEditorStore} from './editorStore';
|
import {useEditorStore} from './editorStore';
|
||||||
import {defaultDarkColors} from '@/views/editor/theme/dark';
|
import {defaultDarkColors} from '@/views/editor/theme/dark';
|
||||||
@@ -24,16 +25,21 @@ export const useThemeStore = defineStore('theme', () => {
|
|||||||
configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
|
configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
|
||||||
);
|
);
|
||||||
|
|
||||||
// 初始化主题颜色 - 从配置加载
|
// 初始化主题颜色 - 从数据库加载
|
||||||
const initializeThemeColors = () => {
|
const initializeThemeColors = async () => {
|
||||||
const customTheme = configStore.config?.appearance?.customTheme;
|
try {
|
||||||
if (customTheme) {
|
const themes = await ThemeService.GetDefaultThemes();
|
||||||
if (customTheme.darkTheme) {
|
if (themes.dark) {
|
||||||
Object.assign(themeColors.darkTheme, customTheme.darkTheme);
|
Object.assign(themeColors.darkTheme, themes.dark.colors);
|
||||||
}
|
}
|
||||||
if (customTheme.lightTheme) {
|
if (themes.light) {
|
||||||
Object.assign(themeColors.lightTheme, customTheme.lightTheme);
|
Object.assign(themeColors.lightTheme, themes.light.colors);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load themes from database, using defaults:', error);
|
||||||
|
// 如果数据库加载失败,使用默认主题
|
||||||
|
Object.assign(themeColors.darkTheme, defaultDarkColors);
|
||||||
|
Object.assign(themeColors.lightTheme, defaultLightColors);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,10 +52,10 @@ export const useThemeStore = defineStore('theme', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 初始化主题
|
// 初始化主题
|
||||||
const initializeTheme = () => {
|
const initializeTheme = async () => {
|
||||||
const theme = configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto;
|
const theme = configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto;
|
||||||
applyThemeToDOM(theme);
|
applyThemeToDOM(theme);
|
||||||
initializeThemeColors();
|
await initializeThemeColors();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设置主题
|
// 设置主题
|
||||||
@@ -84,31 +90,35 @@ export const useThemeStore = defineStore('theme', () => {
|
|||||||
return hasChanges;
|
return hasChanges;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存主题颜色到配置
|
// 保存主题颜色到数据库
|
||||||
const saveThemeColors = async () => {
|
const saveThemeColors = async () => {
|
||||||
const customTheme = {
|
try {
|
||||||
darkTheme: { ...themeColors.darkTheme },
|
const darkColors = ThemeColorConfig.createFrom(themeColors.darkTheme);
|
||||||
lightTheme: { ...themeColors.lightTheme }
|
const lightColors = ThemeColorConfig.createFrom(themeColors.lightTheme);
|
||||||
};
|
|
||||||
|
|
||||||
await configStore.setCustomTheme(customTheme);
|
await ThemeService.UpdateThemeColors(ThemeType.ThemeTypeDark, darkColors);
|
||||||
|
await ThemeService.UpdateThemeColors(ThemeType.ThemeTypeLight, lightColors);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save theme colors:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 重置主题颜色
|
// 重置主题颜色
|
||||||
const resetThemeColors = async (themeType: 'darkTheme' | 'lightTheme') => {
|
const resetThemeColors = async (themeType: 'darkTheme' | 'lightTheme') => {
|
||||||
try {
|
try {
|
||||||
// 1. 更新内存中的颜色状态
|
const dbThemeType = themeType === 'darkTheme' ? ThemeType.ThemeTypeDark : ThemeType.ThemeTypeLight;
|
||||||
|
|
||||||
|
// 1. 调用后端重置服务
|
||||||
|
await ThemeService.ResetThemeColors(dbThemeType);
|
||||||
|
|
||||||
|
// 2. 更新内存中的颜色状态
|
||||||
if (themeType === 'darkTheme') {
|
if (themeType === 'darkTheme') {
|
||||||
Object.assign(themeColors.darkTheme, defaultDarkColors);
|
Object.assign(themeColors.darkTheme, defaultDarkColors);
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if (themeType === 'lightTheme') {
|
|
||||||
Object.assign(themeColors.lightTheme, defaultLightColors);
|
Object.assign(themeColors.lightTheme, defaultLightColors);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 保存到配置
|
|
||||||
await saveThemeColors();
|
|
||||||
|
|
||||||
// 3. 刷新编辑器主题
|
// 3. 刷新编辑器主题
|
||||||
refreshEditorTheme();
|
refreshEditorTheme();
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,11 +13,17 @@ const navItems = [
|
|||||||
{ id: 'general', icon: '⚙️', route: '/settings/general' },
|
{ id: 'general', icon: '⚙️', route: '/settings/general' },
|
||||||
{ id: 'editing', icon: '✏️', route: '/settings/editing' },
|
{ id: 'editing', icon: '✏️', route: '/settings/editing' },
|
||||||
{ id: 'appearance', icon: '🎨', route: '/settings/appearance' },
|
{ id: 'appearance', icon: '🎨', route: '/settings/appearance' },
|
||||||
|
{ id: 'backupPage', icon: '🔗', route: '/settings/backup' },
|
||||||
{ id: 'extensions', icon: '🧩', route: '/settings/extensions' },
|
{ id: 'extensions', icon: '🧩', route: '/settings/extensions' },
|
||||||
{ id: 'keyBindings', icon: '⌨️', route: '/settings/key-bindings' },
|
{ id: 'keyBindings', icon: '⌨️', route: '/settings/key-bindings' },
|
||||||
{ id: 'updates', icon: '🔄', route: '/settings/updates' }
|
{ id: 'updates', icon: '🔄', route: '/settings/updates' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 仅在开发环境添加测试页面导航
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
navItems.push({ id: 'test', icon: '🧪', route: '/settings/test' });
|
||||||
|
}
|
||||||
|
|
||||||
const activeNavItem = ref(route.path.split('/').pop() || 'general');
|
const activeNavItem = ref(route.path.split('/').pop() || 'general');
|
||||||
|
|
||||||
// 处理导航点击
|
// 处理导航点击
|
||||||
@@ -55,7 +61,7 @@ const goBackToEditor = async () => {
|
|||||||
@click="handleNavClick(item)"
|
@click="handleNavClick(item)"
|
||||||
>
|
>
|
||||||
<span class="nav-icon">{{ item.icon }}</span>
|
<span class="nav-icon">{{ item.icon }}</span>
|
||||||
<span class="nav-text">{{ t(`settings.${item.id}`) }}</span>
|
<span class="nav-text">{{ item.id === 'test' ? 'Test' : t(`settings.${item.id}`) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-footer">
|
<div class="settings-footer">
|
||||||
@@ -194,8 +200,8 @@ const goBackToEditor = async () => {
|
|||||||
.settings-content {
|
.settings-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 20px;
|
padding: 0 20px;
|
||||||
overflow-y: auto;
|
overflow-y: scroll;
|
||||||
background-color: var(--settings-bg);
|
background-color: var(--settings-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ const themeStore = useThemeStore();
|
|||||||
|
|
||||||
// 添加临时颜色状态
|
// 添加临时颜色状态
|
||||||
const tempColors = ref({
|
const tempColors = ref({
|
||||||
darkTheme: { ...configStore.config.appearance.customTheme?.darkTheme || defaultDarkColors },
|
darkTheme: { ...defaultDarkColors },
|
||||||
lightTheme: { ...configStore.config.appearance.customTheme?.lightTheme || defaultLightColors }
|
lightTheme: { ...defaultLightColors }
|
||||||
});
|
});
|
||||||
|
|
||||||
// 标记是否有未保存的更改
|
// 标记是否有未保存的更改
|
||||||
@@ -71,9 +71,9 @@ const currentThemeMode = computed(() => {
|
|||||||
return isDark ? 'dark' : 'light';
|
return isDark ? 'dark' : 'light';
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听配置变更,更新临时颜色
|
// 监听主题颜色变更,更新临时颜色
|
||||||
watch(
|
watch(
|
||||||
() => configStore.config.appearance.customTheme,
|
() => themeStore.themeColors,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
if (!hasUnsavedChanges.value) {
|
if (!hasUnsavedChanges.value) {
|
||||||
tempColors.value = {
|
tempColors.value = {
|
||||||
|
|||||||
@@ -0,0 +1,503 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {useConfigStore} from '@/stores/configStore';
|
||||||
|
import {useBackupStore} from '@/stores/backupStore';
|
||||||
|
import {useI18n} from 'vue-i18n';
|
||||||
|
import {computed, onMounted, onUnmounted} from 'vue';
|
||||||
|
import SettingSection from '../components/SettingSection.vue';
|
||||||
|
import SettingItem from '../components/SettingItem.vue';
|
||||||
|
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||||
|
import {AuthMethod} from '@/../bindings/voidraft/internal/models/models';
|
||||||
|
import {DialogService} from '@/../bindings/voidraft/internal/services';
|
||||||
|
|
||||||
|
const {t} = useI18n();
|
||||||
|
const configStore = useConfigStore();
|
||||||
|
const backupStore = useBackupStore();
|
||||||
|
|
||||||
|
// 确保配置已加载
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!configStore.configLoaded) {
|
||||||
|
await configStore.initConfig();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
backupStore.clearError();
|
||||||
|
})
|
||||||
|
|
||||||
|
// 认证方式选项
|
||||||
|
const authMethodOptions = computed(() => [
|
||||||
|
{value: AuthMethod.Token, label: t('settings.backup.authMethods.token')},
|
||||||
|
{value: AuthMethod.SSHKey, label: t('settings.backup.authMethods.sshKey')},
|
||||||
|
{value: AuthMethod.UserPass, label: t('settings.backup.authMethods.userPass')}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 备份间隔选项(分钟)
|
||||||
|
const backupIntervalOptions = computed(() => [
|
||||||
|
{value: 5, label: t('settings.backup.intervals.5min')},
|
||||||
|
{value: 10, label: t('settings.backup.intervals.10min')},
|
||||||
|
{value: 15, label: t('settings.backup.intervals.15min')},
|
||||||
|
{value: 30, label: t('settings.backup.intervals.30min')},
|
||||||
|
{value: 60, label: t('settings.backup.intervals.1hour')}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 计算属性 - 启用备份
|
||||||
|
const enableBackup = computed({
|
||||||
|
get: () => configStore.config.backup.enabled,
|
||||||
|
set: (value: boolean) => configStore.setEnableBackup(value)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性 - 自动备份
|
||||||
|
const autoBackup = computed({
|
||||||
|
get: () => configStore.config.backup.auto_backup,
|
||||||
|
set: (value: boolean) => configStore.setAutoBackup(value)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 仓库URL
|
||||||
|
const repoUrl = computed({
|
||||||
|
get: () => configStore.config.backup.repo_url,
|
||||||
|
set: (value: string) => configStore.setRepoUrl(value)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 认证方式
|
||||||
|
const authMethod = computed({
|
||||||
|
get: () => configStore.config.backup.auth_method,
|
||||||
|
set: (value: AuthMethod) => configStore.setAuthMethod(value)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 备份间隔
|
||||||
|
const backupInterval = computed({
|
||||||
|
get: () => configStore.config.backup.backup_interval,
|
||||||
|
set: (value: number) => configStore.setBackupInterval(value)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 用户名
|
||||||
|
const username = computed({
|
||||||
|
get: () => configStore.config.backup.username,
|
||||||
|
set: (value: string) => configStore.setUsername(value)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 密码
|
||||||
|
const password = computed({
|
||||||
|
get: () => configStore.config.backup.password,
|
||||||
|
set: (value: string) => configStore.setPassword(value)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 访问令牌
|
||||||
|
const token = computed({
|
||||||
|
get: () => configStore.config.backup.token,
|
||||||
|
set: (value: string) => configStore.setToken(value)
|
||||||
|
});
|
||||||
|
|
||||||
|
// SSH密钥路径
|
||||||
|
const sshKeyPath = computed({
|
||||||
|
get: () => configStore.config.backup.ssh_key_path,
|
||||||
|
set: (value: string) => configStore.setSshKeyPath(value)
|
||||||
|
});
|
||||||
|
|
||||||
|
// SSH密钥密码
|
||||||
|
const sshKeyPassphrase = computed({
|
||||||
|
get: () => configStore.config.backup.ssh_key_passphrase,
|
||||||
|
set: (value: string) => configStore.setSshKeyPassphrase(value)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理输入变化
|
||||||
|
const handleRepoUrlChange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
repoUrl.value = target.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleUsernameChange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
username.value = target.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordChange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
password.value = target.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTokenChange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
token.value = target.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSshKeyPassphraseChange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
sshKeyPassphrase.value = target.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAuthMethodChange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLSelectElement;
|
||||||
|
authMethod.value = target.value as AuthMethod;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackupIntervalChange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLSelectElement;
|
||||||
|
backupInterval.value = parseInt(target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 推送到远程
|
||||||
|
const pushToRemote = async () => {
|
||||||
|
await backupStore.pushToRemote();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选择SSH密钥文件
|
||||||
|
const selectSshKeyFile = async () => {
|
||||||
|
// 使用DialogService选择文件
|
||||||
|
const selectedPath = await DialogService.SelectFile();
|
||||||
|
// 检查用户是否取消了选择或路径为空
|
||||||
|
if (!selectedPath.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 更新SSH密钥路径
|
||||||
|
sshKeyPath.value = selectedPath.trim();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="settings-page">
|
||||||
|
<!-- 基本设置 -->
|
||||||
|
<SettingSection :title="t('settings.backup.basicSettings')">
|
||||||
|
<SettingItem
|
||||||
|
:title="t('settings.backup.enableBackup')"
|
||||||
|
>
|
||||||
|
<ToggleSwitch v-model="enableBackup"/>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem
|
||||||
|
:title="t('settings.backup.autoBackup')"
|
||||||
|
:class="{ 'disabled-setting': !enableBackup }"
|
||||||
|
>
|
||||||
|
<ToggleSwitch v-model="autoBackup" :disabled="!enableBackup"/>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem
|
||||||
|
:title="t('settings.backup.backupInterval')"
|
||||||
|
:class="{ 'disabled-setting': !enableBackup || !autoBackup }"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
class="backup-interval-select"
|
||||||
|
:value="backupInterval"
|
||||||
|
@change="handleBackupIntervalChange"
|
||||||
|
:disabled="!enableBackup || !autoBackup"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="option in backupIntervalOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</SettingItem>
|
||||||
|
</SettingSection>
|
||||||
|
|
||||||
|
<!-- 仓库配置 -->
|
||||||
|
<SettingSection :title="t('settings.backup.repositoryConfig')">
|
||||||
|
<SettingItem
|
||||||
|
:title="t('settings.backup.repoUrl')"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="repo-url-input"
|
||||||
|
:value="repoUrl"
|
||||||
|
@input="handleRepoUrlChange"
|
||||||
|
:placeholder="t('settings.backup.repoUrlPlaceholder')"
|
||||||
|
:disabled="!enableBackup"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
|
||||||
|
</SettingSection>
|
||||||
|
|
||||||
|
<!-- 认证配置 -->
|
||||||
|
<SettingSection :title="t('settings.backup.authConfig')">
|
||||||
|
<SettingItem
|
||||||
|
:title="t('settings.backup.authMethod')"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
class="auth-method-select"
|
||||||
|
:value="authMethod"
|
||||||
|
@change="handleAuthMethodChange"
|
||||||
|
:disabled="!enableBackup"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="option in authMethodOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
<!-- 用户名密码认证 -->
|
||||||
|
<template v-if="authMethod === AuthMethod.UserPass">
|
||||||
|
<SettingItem :title="t('settings.backup.username')">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="username-input"
|
||||||
|
:value="username"
|
||||||
|
@input="handleUsernameChange"
|
||||||
|
:placeholder="t('settings.backup.usernamePlaceholder')"
|
||||||
|
:disabled="!enableBackup"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem :title="t('settings.backup.password')">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="password-input"
|
||||||
|
:value="password"
|
||||||
|
@input="handlePasswordChange"
|
||||||
|
:placeholder="t('settings.backup.passwordPlaceholder')"
|
||||||
|
:disabled="!enableBackup"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 访问令牌认证 -->
|
||||||
|
<template v-if="authMethod === AuthMethod.Token">
|
||||||
|
<SettingItem
|
||||||
|
:title="t('settings.backup.token')"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="token-input"
|
||||||
|
:value="token"
|
||||||
|
@input="handleTokenChange"
|
||||||
|
:placeholder="t('settings.backup.tokenPlaceholder')"
|
||||||
|
:disabled="!enableBackup"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- SSH密钥认证 -->
|
||||||
|
<template v-if="authMethod === AuthMethod.SSHKey">
|
||||||
|
<SettingItem
|
||||||
|
:title="t('settings.backup.sshKeyPath')"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="ssh-key-path-input"
|
||||||
|
:value="sshKeyPath"
|
||||||
|
:placeholder="t('settings.backup.sshKeyPathPlaceholder')"
|
||||||
|
:disabled="!enableBackup"
|
||||||
|
readonly
|
||||||
|
@click="enableBackup && selectSshKeyFile()"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem
|
||||||
|
:title="t('settings.backup.sshKeyPassphrase')"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="ssh-passphrase-input"
|
||||||
|
:value="sshKeyPassphrase"
|
||||||
|
@input="handleSshKeyPassphraseChange"
|
||||||
|
:placeholder="t('settings.backup.sshKeyPassphrasePlaceholder')"
|
||||||
|
:disabled="!enableBackup"
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
</template>
|
||||||
|
</SettingSection>
|
||||||
|
|
||||||
|
<!-- 备份操作 -->
|
||||||
|
<SettingSection :title="t('settings.backup.backupOperations')">
|
||||||
|
<SettingItem
|
||||||
|
:title="t('settings.backup.pushToRemote')"
|
||||||
|
>
|
||||||
|
<div class="backup-operation-container">
|
||||||
|
<div class="backup-status-icons">
|
||||||
|
<span v-if="backupStore.pushSuccess" class="success-icon">✓</span>
|
||||||
|
<span v-if="backupStore.pushError" class="error-icon">✗</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="push-button"
|
||||||
|
@click="() => pushToRemote()"
|
||||||
|
:disabled="!enableBackup || !repoUrl || backupStore.isPushing"
|
||||||
|
:class="{ 'backing-up': backupStore.isPushing }"
|
||||||
|
>
|
||||||
|
<span v-if="backupStore.isPushing" class="loading-spinner"></span>
|
||||||
|
{{ backupStore.isPushing ? t('settings.backup.pushing') : t('settings.backup.actions.push') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</SettingItem>
|
||||||
|
<div v-if="backupStore.error" class="error-message-row">
|
||||||
|
{{ backupStore.error }}
|
||||||
|
</div>
|
||||||
|
</SettingSection>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.settings-page {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的输入控件样式
|
||||||
|
.repo-url-input,
|
||||||
|
.branch-input,
|
||||||
|
.username-input,
|
||||||
|
.password-input,
|
||||||
|
.token-input,
|
||||||
|
.ssh-key-path-input,
|
||||||
|
.ssh-passphrase-input,
|
||||||
|
.backup-interval-select,
|
||||||
|
.auth-method-select {
|
||||||
|
width: 50%;
|
||||||
|
min-width: 200px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background-color: var(--settings-input-bg);
|
||||||
|
border: 1px solid var(--settings-input-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--settings-text);
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: var(--settings-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--settings-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[readonly]:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--settings-hover);
|
||||||
|
background-color: var(--settings-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择框特有样式
|
||||||
|
.backup-interval-select,
|
||||||
|
.auth-method-select {
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23999999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 8px center;
|
||||||
|
background-size: 16px;
|
||||||
|
padding-right: 30px;
|
||||||
|
|
||||||
|
option {
|
||||||
|
background-color: var(--settings-input-bg);
|
||||||
|
color: var(--settings-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 备份操作容器
|
||||||
|
.backup-operation-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 备份状态图标
|
||||||
|
.backup-status-icons {
|
||||||
|
width: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 成功和错误图标
|
||||||
|
.success-icon {
|
||||||
|
color: #4caf50;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
color: #f44336;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按钮样式
|
||||||
|
.push-button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: var(--settings-input-bg);
|
||||||
|
border: 1px solid var(--settings-input-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--settings-text);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background-color: var(--settings-hover);
|
||||||
|
border-color: var(--settings-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: var(--settings-text);
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.backing-up {
|
||||||
|
background-color: #2196f3;
|
||||||
|
border-color: #2196f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误信息行样式
|
||||||
|
.error-message-row {
|
||||||
|
color: #f44336;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-wrap: break-word;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: rgba(244, 67, 54, 0.1);
|
||||||
|
border-left: 3px solid #f44336;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用状态
|
||||||
|
.disabled-setting {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载动画
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -159,6 +159,12 @@ const enableSystemTray = computed({
|
|||||||
set: (value: boolean) => configStore.setEnableSystemTray(value)
|
set: (value: boolean) => configStore.setEnableSystemTray(value)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 计算属性 - 启用窗口吸附
|
||||||
|
const enableWindowSnap = computed({
|
||||||
|
get: () => configStore.config.general.enableWindowSnap,
|
||||||
|
set: (value: boolean) => configStore.setEnableWindowSnap(value)
|
||||||
|
});
|
||||||
|
|
||||||
// 计算属性 - 开机启动
|
// 计算属性 - 开机启动
|
||||||
const startAtLogin = computed({
|
const startAtLogin = computed({
|
||||||
get: () => configStore.config.general.startAtLogin,
|
get: () => configStore.config.general.startAtLogin,
|
||||||
@@ -334,6 +340,9 @@ onUnmounted(() => {
|
|||||||
<SettingItem :title="t('settings.enableSystemTray')">
|
<SettingItem :title="t('settings.enableSystemTray')">
|
||||||
<ToggleSwitch v-model="enableSystemTray"/>
|
<ToggleSwitch v-model="enableSystemTray"/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
<SettingItem :title="t('settings.enableWindowSnap')">
|
||||||
|
<ToggleSwitch v-model="enableWindowSnap"/>
|
||||||
|
</SettingItem>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|
||||||
<SettingSection :title="t('settings.startup')">
|
<SettingSection :title="t('settings.startup')">
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -266,6 +266,7 @@ const currentVersion = computed(() => {
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
border-top: 1px solid var(--settings-border, rgba(0,0,0,0.1));
|
border-top: 1px solid var(--settings-border, rgba(0,0,0,0.1));
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
.notes-title {
|
.notes-title {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -278,24 +279,76 @@ const currentVersion = computed(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--settings-text);
|
color: var(--settings-text);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
/* Markdown内容样式 */
|
/* Markdown内容样式 */
|
||||||
:deep(p) {
|
:deep(p) {
|
||||||
margin: 0 0 6px 0;
|
margin: 0 0 6px 0;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(ul), :deep(ol) {
|
:deep(ul), :deep(ol) {
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(li) {
|
:deep(li) {
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
|
:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
|
||||||
margin: 10px 0 6px 0;
|
margin: 10px 0 6px 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(pre), :deep(code) {
|
||||||
|
background-color: var(--settings-code-bg, rgba(0,0,0,0.05));
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(pre) {
|
||||||
|
padding: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(blockquote) {
|
||||||
|
border-left: 3px solid var(--settings-border, rgba(0,0,0,0.1));
|
||||||
|
margin: 6px 0;
|
||||||
|
padding-left: 10px;
|
||||||
|
color: var(--settings-text-secondary, #757575);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(a) {
|
||||||
|
color: var(--theme-primary, #2196f3);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(table) {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 6px 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(th), :deep(td) {
|
||||||
|
border: 1px solid var(--settings-border, rgba(0,0,0,0.1));
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(th) {
|
||||||
|
background-color: var(--settings-table-header-bg, rgba(0,0,0,0.02));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,29 +3,34 @@ module voidraft
|
|||||||
go 1.24.4
|
go 1.24.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/semver/v3 v3.4.0
|
|
||||||
github.com/creativeprojects/go-selfupdate v1.5.0
|
github.com/creativeprojects/go-selfupdate v1.5.0
|
||||||
|
github.com/go-git/go-git/v5 v5.16.2
|
||||||
github.com/knadh/koanf/parsers/json v1.0.0
|
github.com/knadh/koanf/parsers/json v1.0.0
|
||||||
github.com/knadh/koanf/providers/file v1.2.0
|
github.com/knadh/koanf/providers/file v1.2.0
|
||||||
github.com/knadh/koanf/providers/structs v1.0.0
|
github.com/knadh/koanf/providers/structs v1.0.0
|
||||||
github.com/knadh/koanf/v2 v2.2.1
|
github.com/knadh/koanf/v2 v2.2.2
|
||||||
github.com/robertkrimen/otto v0.5.1
|
github.com/robertkrimen/otto v0.5.1
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.10
|
github.com/stretchr/testify v1.10.0
|
||||||
golang.org/x/net v0.41.0
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.25
|
||||||
golang.org/x/sys v0.33.0
|
golang.org/x/net v0.43.0
|
||||||
golang.org/x/text v0.26.0
|
golang.org/x/sys v0.35.0
|
||||||
|
golang.org/x/text v0.28.0
|
||||||
|
modernc.org/sqlite v1.38.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.gitea.io/sdk/gitea v0.21.0 // indirect
|
code.gitea.io/sdk/gitea v0.21.0 // indirect
|
||||||
dario.cat/mergo v1.0.2 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||||
github.com/42wim/httpsig v1.2.3 // indirect
|
github.com/42wim/httpsig v1.2.3 // indirect
|
||||||
|
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||||
github.com/adrg/xdg v0.5.3 // indirect
|
github.com/adrg/xdg v0.5.3 // indirect
|
||||||
github.com/bep/debounce v1.2.1 // indirect
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/ebitengine/purego v0.8.4 // indirect
|
github.com/ebitengine/purego v0.8.4 // indirect
|
||||||
@@ -35,9 +40,8 @@ require (
|
|||||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||||
github.com/go-git/go-git/v5 v5.16.2 // indirect
|
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/google/go-github/v30 v30.1.0 // indirect
|
github.com/google/go-github/v30 v30.1.0 // indirect
|
||||||
@@ -61,26 +65,27 @@ require (
|
|||||||
github.com/pjbgf/sha1cd v0.4.0 // indirect
|
github.com/pjbgf/sha1cd v0.4.0 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/samber/lo v1.51.0 // indirect
|
github.com/samber/lo v1.51.0 // indirect
|
||||||
github.com/sergi/go-diff v1.4.0 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
github.com/ulikunitz/xz v0.5.14 // indirect
|
||||||
github.com/wailsapp/go-webview2 v1.0.21 // indirect
|
github.com/wailsapp/go-webview2 v1.0.21 // indirect
|
||||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
github.com/xanzy/go-gitlab v0.115.0 // indirect
|
github.com/xanzy/go-gitlab v0.115.0 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
golang.org/x/crypto v0.39.0 // indirect
|
golang.org/x/crypto v0.41.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||||
|
golang.org/x/image v0.24.0 // indirect
|
||||||
golang.org/x/oauth2 v0.30.0 // indirect
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
golang.org/x/time v0.12.0 // indirect
|
golang.org/x/time v0.12.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.66.3 // indirect
|
modernc.org/libc v1.66.7 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.38.0 // indirect
|
|
||||||
)
|
)
|
||||||
@@ -2,6 +2,8 @@ code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
|
|||||||
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
|
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
|
||||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||||
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
||||||
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
|
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||||
@@ -58,8 +60,8 @@ github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77
|
|||||||
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
@@ -99,8 +101,8 @@ github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmY
|
|||||||
github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
|
github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
|
||||||
github.com/knadh/koanf/providers/structs v1.0.0 h1:DznjB7NQykhqCar2LvNug3MuxEQsZ5KvfgMbio+23u4=
|
github.com/knadh/koanf/providers/structs v1.0.0 h1:DznjB7NQykhqCar2LvNug3MuxEQsZ5KvfgMbio+23u4=
|
||||||
github.com/knadh/koanf/providers/structs v1.0.0/go.mod h1:kjo5TFtgpaZORlpoJqcbeLowM2cINodv8kX+oFAeQ1w=
|
github.com/knadh/koanf/providers/structs v1.0.0/go.mod h1:kjo5TFtgpaZORlpoJqcbeLowM2cINodv8kX+oFAeQ1w=
|
||||||
github.com/knadh/koanf/v2 v2.2.1 h1:jaleChtw85y3UdBnI0wCqcg1sj1gPoz6D3caGNHtrNE=
|
github.com/knadh/koanf/v2 v2.2.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A=
|
||||||
github.com/knadh/koanf/v2 v2.2.1/go.mod h1:PSFru3ufQgTsI7IF+95rf9s8XA1+aHxKuO/W+dPoHEY=
|
github.com/knadh/koanf/v2 v2.2.2/go.mod h1:abWQc0cBXLSF/PSOMCB/SK+T13NXDsPvOksbpi5e/9Q=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
@@ -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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
|
||||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA=
|
github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA=
|
||||||
github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.10 h1:SrxwhkBcdtaSxQ/zujJuifJN5q8hxyba5UKv5oaM/X4=
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.25 h1:o05zUiPEvmrq2lqqCs4wqnrnAjGmhryYHRhjQmtkvk8=
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.10/go.mod h1:4LCCW7s9e4PuSmu7l9OTvfWIGMO8TaSiftSeR5NpBIc=
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.25/go.mod h1:UZpnhYuju4saspCJrIHAvC0H5XjtKnqd26FRxJLrQ0M=
|
||||||
github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
|
github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
|
||||||
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
|
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
|
||||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
@@ -174,24 +176,26 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -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.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -234,18 +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.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
|
||||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
|
||||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
|
||||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
@@ -254,8 +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/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
|
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||||
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
|
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// Git备份相关类型定义
|
||||||
|
type (
|
||||||
|
// AuthMethod 定义Git认证方式
|
||||||
|
AuthMethod string
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 认证方式
|
||||||
|
Token AuthMethod = "token"
|
||||||
|
SSHKey AuthMethod = "ssh_key"
|
||||||
|
UserPass AuthMethod = "user_pass"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GitBackupConfig Git备份配置
|
||||||
|
type GitBackupConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
RepoURL string `json:"repo_url"`
|
||||||
|
AuthMethod AuthMethod `json:"auth_method"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
SSHKeyPath string `json:"ssh_key_path,omitempty"`
|
||||||
|
SSHKeyPass string `json:"ssh_key_passphrase,omitempty"`
|
||||||
|
BackupInterval int `json:"backup_interval"` // 分钟
|
||||||
|
AutoBackup bool `json:"auto_backup"`
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
"voidraft/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TabType 定义了制表符类型
|
// TabType 定义了制表符类型
|
||||||
@@ -68,6 +69,9 @@ type GeneralConfig struct {
|
|||||||
EnableSystemTray bool `json:"enableSystemTray"` // 是否启用系统托盘
|
EnableSystemTray bool `json:"enableSystemTray"` // 是否启用系统托盘
|
||||||
StartAtLogin bool `json:"startAtLogin"` // 开机启动设置
|
StartAtLogin bool `json:"startAtLogin"` // 开机启动设置
|
||||||
|
|
||||||
|
// 窗口吸附设置
|
||||||
|
EnableWindowSnap bool `json:"enableWindowSnap"` // 是否启用窗口吸附功能(阈值现在是自适应的)
|
||||||
|
|
||||||
// 全局热键设置
|
// 全局热键设置
|
||||||
EnableGlobalHotkey bool `json:"enableGlobalHotkey"` // 是否启用全局热键
|
EnableGlobalHotkey bool `json:"enableGlobalHotkey"` // 是否启用全局热键
|
||||||
GlobalHotkey HotkeyCombo `json:"globalHotkey"` // 全局热键组合
|
GlobalHotkey HotkeyCombo `json:"globalHotkey"` // 全局热键组合
|
||||||
@@ -103,7 +107,6 @@ type EditingConfig struct {
|
|||||||
type AppearanceConfig struct {
|
type AppearanceConfig struct {
|
||||||
Language LanguageType `json:"language"` // 界面语言
|
Language LanguageType `json:"language"` // 界面语言
|
||||||
SystemTheme SystemThemeType `json:"systemTheme"` // 系统界面主题
|
SystemTheme SystemThemeType `json:"systemTheme"` // 系统界面主题
|
||||||
CustomTheme CustomThemeConfig `json:"customTheme"` // 自定义主题配置
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdatesConfig 更新设置配置
|
// UpdatesConfig 更新设置配置
|
||||||
@@ -124,6 +127,7 @@ type AppConfig struct {
|
|||||||
Editing EditingConfig `json:"editing"` // 编辑设置
|
Editing EditingConfig `json:"editing"` // 编辑设置
|
||||||
Appearance AppearanceConfig `json:"appearance"` // 外观设置
|
Appearance AppearanceConfig `json:"appearance"` // 外观设置
|
||||||
Updates UpdatesConfig `json:"updates"` // 更新设置
|
Updates UpdatesConfig `json:"updates"` // 更新设置
|
||||||
|
Backup GitBackupConfig `json:"backup"` // Git备份设置
|
||||||
Metadata ConfigMetadata `json:"metadata"` // 配置元数据
|
Metadata ConfigMetadata `json:"metadata"` // 配置元数据
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +149,7 @@ func NewDefaultAppConfig() *AppConfig {
|
|||||||
DataPath: dataDir,
|
DataPath: dataDir,
|
||||||
EnableSystemTray: true,
|
EnableSystemTray: true,
|
||||||
StartAtLogin: false,
|
StartAtLogin: false,
|
||||||
|
EnableWindowSnap: true, // 默认启用窗口吸附
|
||||||
EnableGlobalHotkey: false,
|
EnableGlobalHotkey: false,
|
||||||
GlobalHotkey: HotkeyCombo{
|
GlobalHotkey: HotkeyCombo{
|
||||||
Ctrl: false,
|
Ctrl: false,
|
||||||
@@ -170,13 +175,12 @@ func NewDefaultAppConfig() *AppConfig {
|
|||||||
Appearance: AppearanceConfig{
|
Appearance: AppearanceConfig{
|
||||||
Language: LangEnUS,
|
Language: LangEnUS,
|
||||||
SystemTheme: SystemThemeAuto,
|
SystemTheme: SystemThemeAuto,
|
||||||
CustomTheme: *NewDefaultCustomThemeConfig(),
|
|
||||||
},
|
},
|
||||||
Updates: UpdatesConfig{
|
Updates: UpdatesConfig{
|
||||||
Version: "1.0.0",
|
Version: version.Version,
|
||||||
AutoUpdate: true,
|
AutoUpdate: true,
|
||||||
PrimarySource: UpdateSourceGithub,
|
PrimarySource: UpdateSourceGitea,
|
||||||
BackupSource: UpdateSourceGitea,
|
BackupSource: UpdateSourceGithub,
|
||||||
BackupBeforeUpdate: true,
|
BackupBeforeUpdate: true,
|
||||||
UpdateTimeout: 30,
|
UpdateTimeout: 30,
|
||||||
Github: GithubConfig{
|
Github: GithubConfig{
|
||||||
@@ -189,9 +193,20 @@ func NewDefaultAppConfig() *AppConfig {
|
|||||||
Repo: "voidraft",
|
Repo: "voidraft",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Backup: GitBackupConfig{
|
||||||
|
Enabled: false,
|
||||||
|
RepoURL: "",
|
||||||
|
AuthMethod: UserPass,
|
||||||
|
Username: "",
|
||||||
|
Password: "",
|
||||||
|
Token: "",
|
||||||
|
SSHKeyPath: "",
|
||||||
|
BackupInterval: 60,
|
||||||
|
AutoBackup: false,
|
||||||
|
},
|
||||||
Metadata: ConfigMetadata{
|
Metadata: ConfigMetadata{
|
||||||
LastUpdated: time.Now().Format(time.RFC3339),
|
LastUpdated: time.Now().Format(time.RFC3339),
|
||||||
Version: "1.0.0",
|
Version: version.Version,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ type Document struct {
|
|||||||
Content string `json:"content" db:"content"`
|
Content string `json:"content" db:"content"`
|
||||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||||
IsDeleted bool `json:"is_deleted"`
|
IsDeleted bool `json:"is_deleted" db:"is_deleted"`
|
||||||
|
IsLocked bool `json:"is_locked" db:"is_locked"` // 锁定标志,锁定的文档无法被删除
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDocument 创建新文档(不需要传ID,由数据库自增)
|
// NewDocument 创建新文档
|
||||||
func NewDocument(title, content string) *Document {
|
func NewDocument(title, content string) *Document {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
return &Document{
|
return &Document{
|
||||||
@@ -23,6 +24,7 @@ func NewDocument(title, content string) *Document {
|
|||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
IsDeleted: false,
|
IsDeleted: false,
|
||||||
|
IsLocked: false, // 默认不锁定
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import "time"
|
|||||||
|
|
||||||
// Extension 单个扩展配置
|
// Extension 单个扩展配置
|
||||||
type Extension struct {
|
type Extension struct {
|
||||||
ID ExtensionID `json:"id"` // 扩展唯一标识
|
ID ExtensionID `json:"id" db:"id"` // 扩展唯一标识
|
||||||
Enabled bool `json:"enabled"` // 是否启用
|
Enabled bool `json:"enabled" db:"enabled"` // 是否启用
|
||||||
IsDefault bool `json:"isDefault"` // 是否为默认扩展
|
IsDefault bool `json:"isDefault" db:"is_default"` // 是否为默认扩展
|
||||||
Config ExtensionConfig `json:"config"` // 扩展配置项
|
Config ExtensionConfig `json:"config" db:"config"` // 扩展配置项
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtensionID 扩展标识符
|
// ExtensionID 扩展标识符
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import "time"
|
|||||||
|
|
||||||
// KeyBinding 单个快捷键绑定
|
// KeyBinding 单个快捷键绑定
|
||||||
type KeyBinding struct {
|
type KeyBinding struct {
|
||||||
Command KeyBindingCommand `json:"command"` // 快捷键动作
|
Command KeyBindingCommand `json:"command" db:"command"` // 快捷键动作
|
||||||
Extension ExtensionID `json:"extension"` // 所属扩展
|
Extension ExtensionID `json:"extension" db:"extension"` // 所属扩展
|
||||||
Key string `json:"key"` // 快捷键组合(如 "Mod-f", "Ctrl-Shift-p")
|
Key string `json:"key" db:"key"` // 快捷键组合(如 "Mod-f", "Ctrl-Shift-p")
|
||||||
Enabled bool `json:"enabled"` // 是否启用
|
Enabled bool `json:"enabled" db:"enabled"` // 是否启用
|
||||||
IsDefault bool `json:"isDefault"` // 是否为默认快捷键
|
IsDefault bool `json:"isDefault" db:"is_default"` // 是否为默认快捷键
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyBindingCommand 快捷键命令
|
// KeyBindingCommand 快捷键命令
|
||||||
|
|||||||
+60
-24
@@ -1,5 +1,20 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ThemeType 主题类型枚举
|
||||||
|
type ThemeType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ThemeTypeDark ThemeType = "dark"
|
||||||
|
ThemeTypeLight ThemeType = "light"
|
||||||
|
)
|
||||||
|
|
||||||
// ThemeColorConfig 主题颜色配置
|
// ThemeColorConfig 主题颜色配置
|
||||||
type ThemeColorConfig struct {
|
type ThemeColorConfig struct {
|
||||||
// 基础色调
|
// 基础色调
|
||||||
@@ -36,15 +51,44 @@ type ThemeColorConfig struct {
|
|||||||
MatchingBracket string `json:"matchingBracket"` // 匹配括号
|
MatchingBracket string `json:"matchingBracket"` // 匹配括号
|
||||||
}
|
}
|
||||||
|
|
||||||
// CustomThemeConfig 自定义主题配置
|
// Theme 主题数据库模型
|
||||||
type CustomThemeConfig struct {
|
type Theme struct {
|
||||||
DarkTheme ThemeColorConfig `json:"darkTheme"` // 深色主题配置
|
ID int `db:"id" json:"id"`
|
||||||
LightTheme ThemeColorConfig `json:"lightTheme"` // 浅色主题配置
|
Name string `db:"name" json:"name"`
|
||||||
|
Type ThemeType `db:"type" json:"type"`
|
||||||
|
Colors ThemeColorConfig `db:"colors" json:"colors"`
|
||||||
|
IsDefault bool `db:"is_default" json:"isDefault"`
|
||||||
|
CreatedAt time.Time `db:"created_at" json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value 实现 driver.Valuer 接口,用于将 ThemeColorConfig 存储到数据库
|
||||||
|
func (tc ThemeColorConfig) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(tc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan 实现 sql.Scanner 接口,用于从数据库读取 ThemeColorConfig
|
||||||
|
func (tc *ThemeColorConfig) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes []byte
|
||||||
|
switch v := value.(type) {
|
||||||
|
case []byte:
|
||||||
|
bytes = v
|
||||||
|
case string:
|
||||||
|
bytes = []byte(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("cannot scan %T into ThemeColorConfig", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(bytes, tc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDefaultDarkTheme 创建默认深色主题配置
|
// NewDefaultDarkTheme 创建默认深色主题配置
|
||||||
func NewDefaultDarkTheme() ThemeColorConfig {
|
func NewDefaultDarkTheme() *ThemeColorConfig {
|
||||||
return ThemeColorConfig{
|
return &ThemeColorConfig{
|
||||||
// 基础色调
|
// 基础色调
|
||||||
Background: "#252B37",
|
Background: "#252B37",
|
||||||
BackgroundSecondary: "#213644",
|
BackgroundSecondary: "#213644",
|
||||||
@@ -66,9 +110,9 @@ func NewDefaultDarkTheme() ThemeColorConfig {
|
|||||||
Cursor: "#ffffff",
|
Cursor: "#ffffff",
|
||||||
Selection: "#0865a9",
|
Selection: "#0865a9",
|
||||||
SelectionBlur: "#225377",
|
SelectionBlur: "#225377",
|
||||||
ActiveLine: "#ffffff",
|
ActiveLine: "#ffffff0a",
|
||||||
LineNumber: "#ffffff",
|
LineNumber: "#ffffff26",
|
||||||
ActiveLineNumber: "#ffffff",
|
ActiveLineNumber: "#ffffff99",
|
||||||
|
|
||||||
// 边框分割线
|
// 边框分割线
|
||||||
BorderColor: "#1e222a",
|
BorderColor: "#1e222a",
|
||||||
@@ -81,8 +125,8 @@ func NewDefaultDarkTheme() ThemeColorConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewDefaultLightTheme 创建默认浅色主题配置
|
// NewDefaultLightTheme 创建默认浅色主题配置
|
||||||
func NewDefaultLightTheme() ThemeColorConfig {
|
func NewDefaultLightTheme() *ThemeColorConfig {
|
||||||
return ThemeColorConfig{
|
return &ThemeColorConfig{
|
||||||
// 基础色调
|
// 基础色调
|
||||||
Background: "#ffffff",
|
Background: "#ffffff",
|
||||||
BackgroundSecondary: "#f1faf1",
|
BackgroundSecondary: "#f1faf1",
|
||||||
@@ -104,24 +148,16 @@ func NewDefaultLightTheme() ThemeColorConfig {
|
|||||||
Cursor: "#000000",
|
Cursor: "#000000",
|
||||||
Selection: "#77baff",
|
Selection: "#77baff",
|
||||||
SelectionBlur: "#b2c2ca",
|
SelectionBlur: "#b2c2ca",
|
||||||
ActiveLine: "#000000",
|
ActiveLine: "#0000000a",
|
||||||
LineNumber: "#000000",
|
LineNumber: "#00000040",
|
||||||
ActiveLineNumber: "#000000",
|
ActiveLineNumber: "#000000aa",
|
||||||
|
|
||||||
// 边框分割线
|
// 边框分割线
|
||||||
BorderColor: "#dfdfdf",
|
BorderColor: "#dfdfdf",
|
||||||
BorderLight: "#0000000d",
|
BorderLight: "#0000000c",
|
||||||
|
|
||||||
// 搜索匹配
|
// 搜索匹配
|
||||||
SearchMatch: "#005cc5",
|
SearchMatch: "#005cc5",
|
||||||
MatchingBracket: "#0000001a",
|
MatchingBracket: "#00000019",
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDefaultCustomThemeConfig 创建默认自定义主题配置
|
|
||||||
func NewDefaultCustomThemeConfig() *CustomThemeConfig {
|
|
||||||
return &CustomThemeConfig{
|
|
||||||
DarkTheme: NewDefaultDarkTheme(),
|
|
||||||
LightTheme: NewDefaultLightTheme(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"` // 上次移动时间,用于判断移动速度
|
||||||
|
}
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
gitConfig "github.com/go-git/go-git/v5/config"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
|
|
||||||
|
"voidraft/internal/models"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dbSerializeFile = "voidraft_data.bin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BackupService 提供基于Git的备份功能
|
||||||
|
type BackupService struct {
|
||||||
|
configService *ConfigService
|
||||||
|
dbService *DatabaseService
|
||||||
|
repository *git.Repository
|
||||||
|
logger *log.LogService
|
||||||
|
isInitialized bool
|
||||||
|
autoBackupTicker *time.Ticker
|
||||||
|
autoBackupStop chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBackupService 创建新的备份服务实例
|
||||||
|
func NewBackupService(configService *ConfigService, dbService *DatabaseService, logger *log.LogService) *BackupService {
|
||||||
|
return &BackupService{
|
||||||
|
configService: configService,
|
||||||
|
dbService: dbService,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize 初始化备份服务
|
||||||
|
func (s *BackupService) Initialize() error {
|
||||||
|
config, repoPath, err := s.getConfigAndPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting backup config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化仓库
|
||||||
|
if err := s.initializeRepository(config, repoPath); err != nil {
|
||||||
|
return fmt.Errorf("initializing repository: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动自动备份
|
||||||
|
if config.AutoBackup && config.BackupInterval > 0 {
|
||||||
|
s.StartAutoBackup()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.isInitialized = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getConfigAndPath 获取备份配置和仓库路径
|
||||||
|
func (s *BackupService) getConfigAndPath() (*models.GitBackupConfig, string, error) {
|
||||||
|
appConfig, err := s.configService.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("getting app config: %w", err)
|
||||||
|
}
|
||||||
|
return &appConfig.Backup, appConfig.General.DataPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// initializeRepository 初始化或打开Git仓库并设置远程
|
||||||
|
func (s *BackupService) initializeRepository(config *models.GitBackupConfig, repoPath string) error {
|
||||||
|
|
||||||
|
// 检查本地仓库是否存在
|
||||||
|
_, err := os.Stat(filepath.Join(repoPath, ".git"))
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
// 仓库不存在,初始化新仓库
|
||||||
|
repo, err := git.PlainInit(repoPath, false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error initializing repository: %w", err)
|
||||||
|
}
|
||||||
|
s.repository = repo
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("error checking repository path: %w", err)
|
||||||
|
} else {
|
||||||
|
// 仓库已存在,打开现有仓库
|
||||||
|
repo, err := git.PlainOpen(repoPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error opening local repository: %w", err)
|
||||||
|
}
|
||||||
|
s.repository = repo
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置或更新远程仓库
|
||||||
|
remote, err := s.repository.Remote("origin")
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, git.ErrRemoteNotFound) {
|
||||||
|
// 远程不存在,添加远程
|
||||||
|
_, err = s.repository.CreateRemote(&gitConfig.RemoteConfig{
|
||||||
|
Name: "origin",
|
||||||
|
URLs: []string{config.RepoURL},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating remote: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("error getting remote: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 检查远程URL是否一致,如果不一致则更新
|
||||||
|
if len(remote.Config().URLs) > 0 && remote.Config().URLs[0] != config.RepoURL {
|
||||||
|
if err := s.repository.DeleteRemote("origin"); err != nil {
|
||||||
|
return fmt.Errorf("error deleting remote: %w", err)
|
||||||
|
}
|
||||||
|
_, err = s.repository.CreateRemote(&gitConfig.RemoteConfig{
|
||||||
|
Name: "origin",
|
||||||
|
URLs: []string{config.RepoURL},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating new remote: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAuthMethod 根据配置获取认证方法
|
||||||
|
func (s *BackupService) getAuthMethod(config *models.GitBackupConfig) (transport.AuthMethod, error) {
|
||||||
|
switch config.AuthMethod {
|
||||||
|
case models.Token:
|
||||||
|
if config.Token == "" {
|
||||||
|
return nil, errors.New("token authentication requires a valid token")
|
||||||
|
}
|
||||||
|
return &http.BasicAuth{
|
||||||
|
Username: "git", // 使用token时,用户名可以是任意值
|
||||||
|
Password: config.Token,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
case models.UserPass:
|
||||||
|
if config.Username == "" || config.Password == "" {
|
||||||
|
return nil, errors.New("username/password authentication requires both username and password")
|
||||||
|
}
|
||||||
|
return &http.BasicAuth{
|
||||||
|
Username: config.Username,
|
||||||
|
Password: config.Password,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
case models.SSHKey:
|
||||||
|
if config.SSHKeyPath == "" {
|
||||||
|
return nil, errors.New("SSH key authentication requires a valid SSH key path")
|
||||||
|
}
|
||||||
|
publicKeys, err := ssh.NewPublicKeysFromFile("git", config.SSHKeyPath, config.SSHKeyPass)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating SSH public keys: %w", err)
|
||||||
|
}
|
||||||
|
return publicKeys, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported authentication method: %s", config.AuthMethod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// serializeDatabase 序列化数据库到文件
|
||||||
|
func (s *BackupService) serializeDatabase(repoPath string) error {
|
||||||
|
if s.dbService == nil || s.dbService.db == nil {
|
||||||
|
return errors.New("database service not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据库路径
|
||||||
|
dbPath, err := s.dbService.getDatabasePath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting database path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭数据库连接以确保所有更改都写入磁盘
|
||||||
|
if err := s.dbService.ServiceShutdown(); err != nil {
|
||||||
|
s.logger.Error("Failed to close database connection", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接复制数据库文件到序列化文件
|
||||||
|
dbData, err := os.ReadFile(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading database file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
binFilePath := filepath.Join(repoPath, dbSerializeFile)
|
||||||
|
if err := os.WriteFile(binFilePath, dbData, 0644); err != nil {
|
||||||
|
return fmt.Errorf("writing serialized database to file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新初始化数据库服务
|
||||||
|
if err := s.dbService.initDatabase(); err != nil {
|
||||||
|
return fmt.Errorf("reinitializing database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushToRemote 推送本地更改到远程仓库
|
||||||
|
func (s *BackupService) PushToRemote() error {
|
||||||
|
if !s.isInitialized {
|
||||||
|
return errors.New("backup service not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
config, repoPath, err := s.getConfigAndPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting backup config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.Enabled {
|
||||||
|
return errors.New("backup is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据库序列化文件的路径
|
||||||
|
binFilePath := filepath.Join(repoPath, dbSerializeFile)
|
||||||
|
|
||||||
|
// 函数返回前都删除临时文件
|
||||||
|
defer func() {
|
||||||
|
if _, err := os.Stat(binFilePath); err == nil {
|
||||||
|
os.Remove(binFilePath)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 序列化数据库
|
||||||
|
if err := s.serializeDatabase(repoPath); err != nil {
|
||||||
|
return fmt.Errorf("serializing database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取工作树
|
||||||
|
w, err := s.repository.Worktree()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting worktree: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加序列化的数据库文件
|
||||||
|
if _, err := w.Add(dbSerializeFile); err != nil {
|
||||||
|
return fmt.Errorf("adding serialized database file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有变化需要提交
|
||||||
|
status, err := w.Status()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting worktree status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有变化,直接返回
|
||||||
|
if status.IsClean() {
|
||||||
|
return errors.New("no changes to backup")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建提交
|
||||||
|
_, err = w.Commit(fmt.Sprintf("Backup %s", time.Now().Format("2006-01-02 15:04:05")), &git.CommitOptions{
|
||||||
|
Author: &object.Signature{
|
||||||
|
Name: "voidraft",
|
||||||
|
Email: "backup@voidraft.app",
|
||||||
|
When: time.Now(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "cannot create empty commit") {
|
||||||
|
return errors.New("no changes to backup")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("creating commit: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取认证方法并推送到远程
|
||||||
|
auth, err := s.getAuthMethod(config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting auth method: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 推送到远程仓库
|
||||||
|
if err := s.repository.Push(&git.PushOptions{
|
||||||
|
RemoteName: "origin",
|
||||||
|
Auth: auth,
|
||||||
|
}); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
|
||||||
|
// 忽略一些常见的非错误情况
|
||||||
|
if strings.Contains(err.Error(), "clean working tree") ||
|
||||||
|
strings.Contains(err.Error(), "already up-to-date") ||
|
||||||
|
strings.Contains(err.Error(), " clean working tree") ||
|
||||||
|
strings.Contains(err.Error(), "reference not found") {
|
||||||
|
// 更新最后推送时间
|
||||||
|
return errors.New("no changes to backup")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("push failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartAutoBackup 启动自动备份定时器
|
||||||
|
func (s *BackupService) StartAutoBackup() error {
|
||||||
|
config, _, err := s.getConfigAndPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting backup config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.AutoBackup || config.BackupInterval <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.StopAutoBackup()
|
||||||
|
|
||||||
|
// 将秒转换为分钟
|
||||||
|
s.autoBackupTicker = time.NewTicker(time.Duration(config.BackupInterval) * time.Minute)
|
||||||
|
s.autoBackupStop = make(chan bool)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.autoBackupTicker.C:
|
||||||
|
// 执行推送操作
|
||||||
|
if err := s.PushToRemote(); err != nil {
|
||||||
|
s.logger.Error("Auto backup failed", "error", err)
|
||||||
|
}
|
||||||
|
case <-s.autoBackupStop:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopAutoBackup 停止自动备份
|
||||||
|
func (s *BackupService) StopAutoBackup() {
|
||||||
|
if s.autoBackupTicker != nil {
|
||||||
|
s.autoBackupTicker.Stop()
|
||||||
|
s.autoBackupTicker = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.autoBackupStop != nil {
|
||||||
|
close(s.autoBackupStop)
|
||||||
|
s.autoBackupStop = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reinitialize 重新初始化备份服务,用于响应配置变更
|
||||||
|
func (s *BackupService) Reinitialize() error {
|
||||||
|
// 停止自动备份
|
||||||
|
s.StopAutoBackup()
|
||||||
|
|
||||||
|
// 重新设置标志
|
||||||
|
s.isInitialized = false
|
||||||
|
|
||||||
|
// 重新初始化
|
||||||
|
return s.Initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleConfigChange 处理备份配置变更
|
||||||
|
func (s *BackupService) HandleConfigChange(config *models.GitBackupConfig) error {
|
||||||
|
|
||||||
|
// 如果备份功能禁用,只需停止自动备份
|
||||||
|
if !config.Enabled {
|
||||||
|
s.StopAutoBackup()
|
||||||
|
s.isInitialized = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果服务已初始化,重新初始化以应用新配置
|
||||||
|
if s.isInitialized {
|
||||||
|
return s.Reinitialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果服务未初始化但已启用,则初始化
|
||||||
|
if config.Enabled && !s.isInitialized {
|
||||||
|
return s.Initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceShutdown 服务关闭时的清理工作
|
||||||
|
func (s *BackupService) ServiceShutdown() {
|
||||||
|
s.StopAutoBackup()
|
||||||
|
}
|
||||||
@@ -1,318 +1,200 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"time"
|
|
||||||
"voidraft/internal/models"
|
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
|
||||||
jsonparser "github.com/knadh/koanf/parsers/json"
|
jsonparser "github.com/knadh/koanf/parsers/json"
|
||||||
"github.com/knadh/koanf/providers/structs"
|
"github.com/knadh/koanf/providers/structs"
|
||||||
"github.com/knadh/koanf/v2"
|
"github.com/knadh/koanf/v2"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// CurrentAppConfigVersion 当前应用配置版本
|
// BackupFilePattern backup file name pattern
|
||||||
CurrentAppConfigVersion = "1.2.0"
|
|
||||||
// BackupFilePattern 备份文件名模式
|
|
||||||
BackupFilePattern = "%s.backup.%s.json"
|
BackupFilePattern = "%s.backup.%s.json"
|
||||||
|
// MaxConfigFileSize maximum config file size
|
||||||
// 资源限制常量
|
|
||||||
MaxConfigFileSize = 10 * 1024 * 1024 // 10MB
|
MaxConfigFileSize = 10 * 1024 * 1024 // 10MB
|
||||||
MaxRecursionDepth = 50 // 最大递归深度
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Migratable 可迁移的配置接口
|
// ConfigMigrator elegant configuration migrator with automatic field detection
|
||||||
type Migratable interface {
|
type ConfigMigrator struct {
|
||||||
GetVersion() string // 获取当前版本
|
logger *log.LogService
|
||||||
SetVersion(string) // 设置版本
|
|
||||||
SetLastUpdated(string) // 设置最后更新时间
|
|
||||||
GetDefaultConfig() any // 获取默认配置
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfigMigrationService 配置迁移服务
|
|
||||||
type ConfigMigrationService[T Migratable] struct {
|
|
||||||
logger *log.Service
|
|
||||||
configDir string
|
configDir string
|
||||||
configName string
|
configName string
|
||||||
targetVersion string
|
|
||||||
configPath string
|
configPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// MigrationResult 迁移结果
|
// MigrationResult migration operation result
|
||||||
type MigrationResult struct {
|
type MigrationResult struct {
|
||||||
Migrated, ConfigUpdated bool
|
Migrated bool `json:"migrated"` // Whether migration was performed
|
||||||
FromVersion, ToVersion string
|
MissingFields []string `json:"missingFields"` // Fields that were missing
|
||||||
BackupPath string
|
BackupPath string `json:"backupPath"` // Path to backup file
|
||||||
|
Description string `json:"description"` // Description of migration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfigMigrationService 创建配置迁移服务
|
// NewConfigMigrator creates a new configuration migrator
|
||||||
func NewConfigMigrationService[T Migratable](
|
func NewConfigMigrator(
|
||||||
logger *log.Service,
|
logger *log.LogService,
|
||||||
configDir string,
|
configDir string,
|
||||||
configName, targetVersion, configPath string,
|
configName, configPath string,
|
||||||
) *ConfigMigrationService[T] {
|
) *ConfigMigrator {
|
||||||
return &ConfigMigrationService[T]{
|
if logger == nil {
|
||||||
logger: orDefault(logger, log.New()),
|
logger = log.New()
|
||||||
|
}
|
||||||
|
return &ConfigMigrator{
|
||||||
|
logger: logger,
|
||||||
configDir: configDir,
|
configDir: configDir,
|
||||||
configName: configName,
|
configName: configName,
|
||||||
targetVersion: targetVersion,
|
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MigrateConfig 迁移配置文件
|
// AutoMigrate automatically detects and migrates missing configuration fields
|
||||||
func (cms *ConfigMigrationService[T]) MigrateConfig(existingConfig *koanf.Koanf) (*MigrationResult, error) {
|
func (cm *ConfigMigrator) AutoMigrate(defaultConfig interface{}, currentConfig *koanf.Koanf) (*MigrationResult, error) {
|
||||||
currentVersion := orDefault(existingConfig.String("metadata.version"), "0.0.0")
|
defaultKoanf := koanf.New(".")
|
||||||
|
if err := defaultKoanf.Load(structs.Provider(defaultConfig, "json"), nil); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load default config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect missing fields
|
||||||
|
missingFields := cm.detectMissingFields(currentConfig.All(), defaultKoanf.All())
|
||||||
|
|
||||||
|
// Create result object
|
||||||
result := &MigrationResult{
|
result := &MigrationResult{
|
||||||
FromVersion: currentVersion,
|
MissingFields: missingFields,
|
||||||
ToVersion: cms.targetVersion,
|
Migrated: len(missingFields) > 0,
|
||||||
|
Description: fmt.Sprintf("Found %d missing fields", len(missingFields)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if needsMigration, err := cms.needsMigration(currentVersion); err != nil {
|
// No migration needed
|
||||||
return result, fmt.Errorf("version comparison failed: %w", err)
|
if !result.Migrated {
|
||||||
} else if !needsMigration {
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 资源检查和备份
|
// Create backup before migration
|
||||||
if err := cms.checkResourceLimits(); err != nil {
|
backupPath, err := cm.createBackup()
|
||||||
return result, fmt.Errorf("resource limit check failed: %w", err)
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
if backupPath, err := cms.createBackupOptimized(); err != nil {
|
|
||||||
return result, fmt.Errorf("backup creation failed: %w", err)
|
return result, fmt.Errorf("backup creation failed: %w", err)
|
||||||
} else {
|
}
|
||||||
result.BackupPath = backupPath
|
result.BackupPath = backupPath
|
||||||
|
|
||||||
|
// Merge missing fields from default config
|
||||||
|
if err := cm.mergeDefaultFields(currentConfig, defaultKoanf, missingFields); err != nil {
|
||||||
|
return result, fmt.Errorf("failed to merge default fields: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动恢复检查
|
// Save updated config
|
||||||
cms.tryQuickRecovery(existingConfig)
|
if err := cm.saveConfig(currentConfig); err != nil {
|
||||||
|
return result, fmt.Errorf("failed to save updated config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// 执行迁移
|
// Clean up backup on success
|
||||||
if configUpdated, err := cms.performOptimizedMigration(existingConfig); err != nil {
|
if backupPath != "" {
|
||||||
return result, fmt.Errorf("migration failed: %w", err)
|
if err := os.Remove(backupPath); err != nil {
|
||||||
} else {
|
cm.logger.Error("Failed to remove backup", "error", err)
|
||||||
result.Migrated = true
|
}
|
||||||
result.ConfigUpdated = configUpdated
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// needsMigration 检查是否需要迁移
|
// detectMissingFields detects missing configuration fields
|
||||||
func (cms *ConfigMigrationService[T]) needsMigration(current string) (bool, error) {
|
func (cm *ConfigMigrator) detectMissingFields(current, defaultConfig map[string]interface{}) []string {
|
||||||
currentVer, err := semver.NewVersion(current)
|
var missing []string
|
||||||
if err != nil {
|
cm.findMissing("", defaultConfig, current, &missing)
|
||||||
return true, nil
|
return missing
|
||||||
}
|
|
||||||
targetVer, err := semver.NewVersion(cms.targetVersion)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("invalid target version: %s", cms.targetVersion)
|
|
||||||
}
|
|
||||||
return currentVer.LessThan(targetVer), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkResourceLimits 检查资源限制
|
// findMissing recursively finds missing fields
|
||||||
func (cms *ConfigMigrationService[T]) checkResourceLimits() error {
|
func (cm *ConfigMigrator) findMissing(prefix string, defaultMap, currentMap map[string]interface{}, missing *[]string) {
|
||||||
if info, err := os.Stat(cms.configPath); err == nil && info.Size() > MaxConfigFileSize {
|
for key, defaultVal := range defaultMap {
|
||||||
return fmt.Errorf("config file size (%d bytes) exceeds limit (%d bytes)", info.Size(), MaxConfigFileSize)
|
fullKey := key
|
||||||
|
if prefix != "" {
|
||||||
|
fullKey = prefix + "." + key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentVal, exists := currentMap[key]
|
||||||
|
if !exists {
|
||||||
|
// Field completely missing - add it
|
||||||
|
*missing = append(*missing, fullKey)
|
||||||
|
} else if defaultNestedMap, ok := defaultVal.(map[string]interface{}); ok {
|
||||||
|
if currentNestedMap, ok := currentVal.(map[string]interface{}); ok {
|
||||||
|
// Both are maps, recurse into them
|
||||||
|
cm.findMissing(fullKey, defaultNestedMap, currentNestedMap, missing)
|
||||||
|
}
|
||||||
|
// Type mismatch: user has different type, don't recurse
|
||||||
|
}
|
||||||
|
// For non-map default values, field exists, preserve user's value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeDefaultFields merges default values for missing fields into current config
|
||||||
|
func (cm *ConfigMigrator) mergeDefaultFields(current, defaultConfig *koanf.Koanf, missingFields []string) error {
|
||||||
|
actuallyMerged := 0
|
||||||
|
|
||||||
|
for _, field := range missingFields {
|
||||||
|
if defaultConfig.Exists(field) {
|
||||||
|
if defaultValue := defaultConfig.Get(field); defaultValue != nil {
|
||||||
|
// Always set the field, even if it causes type conflicts
|
||||||
|
// This allows configuration structure evolution during upgrades
|
||||||
|
current.Set(field, defaultValue)
|
||||||
|
actuallyMerged++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update timestamp if we actually merged fields
|
||||||
|
if actuallyMerged > 0 {
|
||||||
|
current.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// createBackupOptimized 优化的备份创建
|
// createBackup creates a backup of the configuration file
|
||||||
func (cms *ConfigMigrationService[T]) createBackupOptimized() (string, error) {
|
func (cm *ConfigMigrator) createBackup() (string, error) {
|
||||||
if _, err := os.Stat(cms.configPath); os.IsNotExist(err) {
|
if _, err := os.Stat(cm.configPath); os.IsNotExist(err) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
configDir := cms.configDir
|
|
||||||
timestamp := time.Now().Format("20060102150405")
|
timestamp := time.Now().Format("20060102150405")
|
||||||
newBackupPath := filepath.Join(configDir, fmt.Sprintf(BackupFilePattern, cms.configName, timestamp))
|
backupPath := filepath.Join(cm.configDir, fmt.Sprintf(BackupFilePattern, cm.configName, timestamp))
|
||||||
|
|
||||||
// 单次扫描:删除旧备份并创建新备份
|
data, err := os.ReadFile(cm.configPath)
|
||||||
pattern := filepath.Join(configDir, fmt.Sprintf("%s.backup.*.json", cms.configName))
|
|
||||||
if matches, err := filepath.Glob(pattern); err == nil {
|
|
||||||
for _, oldBackup := range matches {
|
|
||||||
if oldBackup != newBackupPath {
|
|
||||||
os.Remove(oldBackup) // 忽略删除错误,继续处理
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newBackupPath, copyFile(cms.configPath, newBackupPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryQuickRecovery 快速恢复检查
|
|
||||||
func (cms *ConfigMigrationService[T]) tryQuickRecovery(existingConfig *koanf.Koanf) {
|
|
||||||
var testConfig T
|
|
||||||
if existingConfig.Unmarshal("", &testConfig) != nil {
|
|
||||||
cms.logger.Info("Config appears corrupted, attempting quick recovery")
|
|
||||||
if backupPath := cms.findLatestBackupQuick(); backupPath != "" {
|
|
||||||
if data, err := os.ReadFile(backupPath); err == nil {
|
|
||||||
existingConfig.Delete("")
|
|
||||||
existingConfig.Load(&BytesProvider{data}, jsonparser.Parser())
|
|
||||||
cms.logger.Info("Quick recovery successful")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// findLatestBackupQuick 快速查找最新备份(优化排序)
|
|
||||||
func (cms *ConfigMigrationService[T]) findLatestBackupQuick() string {
|
|
||||||
pattern := filepath.Join(cms.configDir, fmt.Sprintf("%s.backup.*.json", cms.configName))
|
|
||||||
matches, err := filepath.Glob(pattern)
|
|
||||||
if err != nil || len(matches) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
sort.Strings(matches) // 字典序排序,时间戳格式确保正确性
|
|
||||||
return matches[len(matches)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// performOptimizedMigration 优化的迁移执行
|
|
||||||
func (cms *ConfigMigrationService[T]) performOptimizedMigration(existingConfig *koanf.Koanf) (bool, error) {
|
|
||||||
// 直接从koanf实例获取配置,避免额外序列化
|
|
||||||
var currentConfig T
|
|
||||||
if err := existingConfig.Unmarshal("", ¤tConfig); err != nil {
|
|
||||||
return false, fmt.Errorf("unmarshal existing config failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig, ok := currentConfig.GetDefaultConfig().(T)
|
|
||||||
if !ok {
|
|
||||||
return false, fmt.Errorf("default config type mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
return cms.mergeInPlace(existingConfig, currentConfig, defaultConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// mergeInPlace 就地合并配置
|
|
||||||
func (cms *ConfigMigrationService[T]) mergeInPlace(existingConfig *koanf.Koanf, currentConfig, defaultConfig T) (bool, error) {
|
|
||||||
// 创建临时合并实例
|
|
||||||
mergeKoanf := koanf.New(".")
|
|
||||||
|
|
||||||
// 使用快速加载链
|
|
||||||
if err := chainLoad(mergeKoanf,
|
|
||||||
func() error { return mergeKoanf.Load(structs.Provider(defaultConfig, "json"), nil) },
|
|
||||||
func() error {
|
|
||||||
return mergeKoanf.Load(structs.Provider(currentConfig, "json"), nil,
|
|
||||||
koanf.WithMergeFunc(cms.fastMerge))
|
|
||||||
},
|
|
||||||
); err != nil {
|
|
||||||
return false, fmt.Errorf("config merge failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新元数据
|
|
||||||
mergeKoanf.Set("metadata.version", cms.targetVersion)
|
|
||||||
mergeKoanf.Set("metadata.lastUpdated", time.Now().Format(time.RFC3339))
|
|
||||||
|
|
||||||
// 一次性序列化和原子写入
|
|
||||||
configBytes, err := mergeKoanf.Marshal(jsonparser.Parser())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("marshal config failed: %w", err)
|
return "", fmt.Errorf("failed to read config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(backupPath, data, 0644); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create backup: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return backupPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveConfig saves configuration to file
|
||||||
|
func (cm *ConfigMigrator) saveConfig(config *koanf.Koanf) error {
|
||||||
|
configBytes, err := config.Marshal(jsonparser.Parser())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(configBytes) > MaxConfigFileSize {
|
if len(configBytes) > MaxConfigFileSize {
|
||||||
return false, fmt.Errorf("merged config size exceeds limit")
|
return fmt.Errorf("config size (%d bytes) exceeds limit (%d bytes)", len(configBytes), MaxConfigFileSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 原子写入
|
// Atomic write
|
||||||
return true, cms.atomicWrite(existingConfig, configBytes)
|
tempPath := cm.configPath + ".tmp"
|
||||||
}
|
|
||||||
|
|
||||||
// atomicWrite 原子写入操作
|
|
||||||
func (cms *ConfigMigrationService[T]) atomicWrite(existingConfig *koanf.Koanf, configBytes []byte) error {
|
|
||||||
tempPath := cms.configPath + ".tmp"
|
|
||||||
|
|
||||||
if err := os.WriteFile(tempPath, configBytes, 0644); err != nil {
|
if err := os.WriteFile(tempPath, configBytes, 0644); err != nil {
|
||||||
return fmt.Errorf("write temp config failed: %w", err)
|
return fmt.Errorf("failed to write temp config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Rename(tempPath, cms.configPath); err != nil {
|
if err := os.Rename(tempPath, cm.configPath); err != nil {
|
||||||
os.Remove(tempPath)
|
os.Remove(tempPath)
|
||||||
return fmt.Errorf("atomic rename failed: %w", err)
|
return fmt.Errorf("failed to rename temp config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新加载到原实例
|
|
||||||
existingConfig.Delete("")
|
|
||||||
return existingConfig.Load(&BytesProvider{configBytes}, jsonparser.Parser())
|
|
||||||
}
|
|
||||||
|
|
||||||
// fastMerge 快速合并函数
|
|
||||||
func (cms *ConfigMigrationService[T]) fastMerge(src, dest map[string]interface{}) error {
|
|
||||||
return cms.fastMergeRecursive(src, dest, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fastMergeRecursive 快速递归合并
|
|
||||||
func (cms *ConfigMigrationService[T]) fastMergeRecursive(src, dest map[string]interface{}, depth int) error {
|
|
||||||
if depth > MaxRecursionDepth {
|
|
||||||
return fmt.Errorf("recursion depth exceeded")
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, srcVal := range src {
|
|
||||||
if destVal, exists := dest[key]; exists {
|
|
||||||
// 优先检查map类型(最常见情况)
|
|
||||||
if srcMap, srcOK := srcVal.(map[string]interface{}); srcOK {
|
|
||||||
if destMap, destOK := destVal.(map[string]interface{}); destOK {
|
|
||||||
if err := cms.fastMergeRecursive(srcMap, destMap, depth+1); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 快速空值检查(避免反射)
|
|
||||||
if srcVal == nil || srcVal == "" || srcVal == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dest[key] = srcVal
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BytesProvider 轻量字节提供器
|
|
||||||
type BytesProvider struct{ data []byte }
|
|
||||||
|
|
||||||
func (bp *BytesProvider) ReadBytes() ([]byte, error) { return bp.data, nil }
|
|
||||||
func (bp *BytesProvider) Read() (map[string]interface{}, error) {
|
|
||||||
var result map[string]interface{}
|
|
||||||
return result, json.Unmarshal(bp.data, &result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 工具函数
|
|
||||||
func orDefault[T any](value, defaultValue T) T {
|
|
||||||
var zero T
|
|
||||||
if reflect.DeepEqual(value, zero) {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyFile(src, dst string) error {
|
|
||||||
data, err := os.ReadFile(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(dst, data, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func chainLoad(k *koanf.Koanf, loaders ...func() error) error {
|
|
||||||
for _, loader := range loaders {
|
|
||||||
if err := loader(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 工厂函数
|
|
||||||
func NewAppConfigMigrationService(logger *log.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
@@ -22,6 +22,10 @@ const (
|
|||||||
ConfigChangeTypeHotkey ConfigChangeType = "hotkey"
|
ConfigChangeTypeHotkey ConfigChangeType = "hotkey"
|
||||||
// ConfigChangeTypeDataPath 数据路径配置变更
|
// ConfigChangeTypeDataPath 数据路径配置变更
|
||||||
ConfigChangeTypeDataPath ConfigChangeType = "datapath"
|
ConfigChangeTypeDataPath ConfigChangeType = "datapath"
|
||||||
|
// ConfigChangeTypeBackup 备份配置变更
|
||||||
|
ConfigChangeTypeBackup ConfigChangeType = "backup"
|
||||||
|
// ConfigChangeTypeWindowSnap 窗口吸附配置变更
|
||||||
|
ConfigChangeTypeWindowSnap ConfigChangeType = "windowsnap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigChangeCallback 配置变更回调函数类型
|
// ConfigChangeCallback 配置变更回调函数类型
|
||||||
@@ -49,7 +53,7 @@ type ConfigListener struct {
|
|||||||
type ConfigNotificationService struct {
|
type ConfigNotificationService struct {
|
||||||
listeners map[ConfigChangeType][]*ConfigListener // 支持多监听器的map
|
listeners map[ConfigChangeType][]*ConfigListener // 支持多监听器的map
|
||||||
mu sync.RWMutex // 监听器map的读写锁
|
mu sync.RWMutex // 监听器map的读写锁
|
||||||
logger *log.Service // 日志服务
|
logger *log.LogService // 日志服务
|
||||||
koanf *koanf.Koanf // koanf实例
|
koanf *koanf.Koanf // koanf实例
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -57,7 +61,7 @@ type ConfigNotificationService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewConfigNotificationService 创建配置通知服务
|
// NewConfigNotificationService 创建配置通知服务
|
||||||
func NewConfigNotificationService(k *koanf.Koanf, logger *log.Service) *ConfigNotificationService {
|
func NewConfigNotificationService(k *koanf.Koanf, logger *log.LogService) *ConfigNotificationService {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
return &ConfigNotificationService{
|
return &ConfigNotificationService{
|
||||||
listeners: make(map[ConfigChangeType][]*ConfigListener),
|
listeners: make(map[ConfigChangeType][]*ConfigListener),
|
||||||
@@ -284,25 +288,6 @@ func deepCopyValue(src, dst reflect.Value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// deepCopyConfig 保留原有的JSON深拷贝方法作为备用
|
|
||||||
func deepCopyConfig(src *models.AppConfig) *models.AppConfig {
|
|
||||||
if src == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonBytes, err := json.Marshal(src)
|
|
||||||
if err != nil {
|
|
||||||
return src
|
|
||||||
}
|
|
||||||
|
|
||||||
var dst models.AppConfig
|
|
||||||
if err := json.Unmarshal(jsonBytes, &dst); err != nil {
|
|
||||||
return src
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dst
|
|
||||||
}
|
|
||||||
|
|
||||||
// debounceNotify 防抖通知
|
// debounceNotify 防抖通知
|
||||||
func (cns *ConfigNotificationService) debounceNotify(listener *ConfigListener, oldConfig, newConfig *models.AppConfig) {
|
func (cns *ConfigNotificationService) debounceNotify(listener *ConfigListener, oldConfig, newConfig *models.AppConfig) {
|
||||||
listener.mu.Lock()
|
listener.mu.Lock()
|
||||||
@@ -445,6 +430,52 @@ func CreateDataPathListener(name string, callback func() error) *ConfigListener
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateBackupConfigListener 创建备份配置监听器
|
||||||
|
func CreateBackupConfigListener(name string, callback func(config *models.GitBackupConfig) error) *ConfigListener {
|
||||||
|
return &ConfigListener{
|
||||||
|
Name: name,
|
||||||
|
ChangeType: ConfigChangeTypeBackup,
|
||||||
|
Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
|
||||||
|
if newConfig == nil {
|
||||||
|
defaultConfig := models.NewDefaultAppConfig()
|
||||||
|
return callback(&defaultConfig.Backup)
|
||||||
|
}
|
||||||
|
return callback(&newConfig.Backup)
|
||||||
|
},
|
||||||
|
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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateWindowSnapConfigListener 创建窗口吸附配置监听器
|
||||||
|
func CreateWindowSnapConfigListener(name string, callback func(enabled bool) error) *ConfigListener {
|
||||||
|
return &ConfigListener{
|
||||||
|
Name: name,
|
||||||
|
ChangeType: ConfigChangeTypeWindowSnap,
|
||||||
|
Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
|
||||||
|
if newConfig == nil {
|
||||||
|
defaultConfig := models.NewDefaultAppConfig()
|
||||||
|
return callback(defaultConfig.General.EnableWindowSnap)
|
||||||
|
}
|
||||||
|
return callback(newConfig.General.EnableWindowSnap)
|
||||||
|
},
|
||||||
|
DebounceDelay: 200 * time.Millisecond,
|
||||||
|
GetConfigFunc: func(k *koanf.Koanf) *models.AppConfig {
|
||||||
|
var config models.AppConfig
|
||||||
|
if err := k.Unmarshal("", &config); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &config
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ServiceShutdown 关闭服务
|
// ServiceShutdown 关闭服务
|
||||||
func (cns *ConfigNotificationService) ServiceShutdown() error {
|
func (cns *ConfigNotificationService) ServiceShutdown() error {
|
||||||
cns.Cleanup()
|
cns.Cleanup()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
// ConfigService 应用配置服务
|
// ConfigService 应用配置服务
|
||||||
type ConfigService struct {
|
type ConfigService struct {
|
||||||
koanf *koanf.Koanf // koanf 实例
|
koanf *koanf.Koanf // koanf 实例
|
||||||
logger *log.Service // 日志服务
|
logger *log.LogService // 日志服务
|
||||||
configDir string // 配置目录
|
configDir string // 配置目录
|
||||||
settingsPath string // 设置文件路径
|
settingsPath string // 设置文件路径
|
||||||
mu sync.RWMutex // 读写锁
|
mu sync.RWMutex // 读写锁
|
||||||
@@ -27,8 +27,9 @@ type ConfigService struct {
|
|||||||
|
|
||||||
// 配置通知服务
|
// 配置通知服务
|
||||||
notificationService *ConfigNotificationService
|
notificationService *ConfigNotificationService
|
||||||
// 配置迁移服务
|
|
||||||
migrationService *ConfigMigrationService[*models.AppConfig]
|
// 配置迁移器
|
||||||
|
configMigrator *ConfigMigrator
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigError 配置错误
|
// ConfigError 配置错误
|
||||||
@@ -55,7 +56,7 @@ func (e *ConfigError) Is(target error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewConfigService 创建新的配置服务实例
|
// NewConfigService 创建新的配置服务实例
|
||||||
func NewConfigService(logger *log.Service) *ConfigService {
|
func NewConfigService(logger *log.LogService) *ConfigService {
|
||||||
// 获取用户主目录
|
// 获取用户主目录
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -71,12 +72,14 @@ func NewConfigService(logger *log.Service) *ConfigService {
|
|||||||
configDir: configDir,
|
configDir: configDir,
|
||||||
settingsPath: settingsPath,
|
settingsPath: settingsPath,
|
||||||
koanf: koanf.New("."),
|
koanf: koanf.New("."),
|
||||||
migrationService: NewAppConfigMigrationService(logger, configDir, settingsPath),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化配置通知服务
|
// 初始化配置通知服务
|
||||||
cs.notificationService = NewConfigNotificationService(cs.koanf, logger)
|
cs.notificationService = NewConfigNotificationService(cs.koanf, logger)
|
||||||
|
|
||||||
|
// 初始化配置迁移器
|
||||||
|
cs.configMigrator = NewConfigMigrator(logger, configDir, "settings", settingsPath)
|
||||||
|
|
||||||
cs.initConfig()
|
cs.initConfig()
|
||||||
|
|
||||||
// 启动配置文件监听
|
// 启动配置文件监听
|
||||||
@@ -106,23 +109,33 @@ func (cs *ConfigService) initConfig() error {
|
|||||||
return cs.createDefaultConfig()
|
return cs.createDefaultConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 配置文件存在,先加载现有配置
|
// 配置文件存在,直接加载现有配置
|
||||||
cs.fileProvider = file.Provider(cs.settingsPath)
|
cs.fileProvider = file.Provider(cs.settingsPath)
|
||||||
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
|
if err := cs.koanf.Load(cs.fileProvider, jsonparser.Parser()); err != nil {
|
||||||
return &ConfigError{Operation: "load_config_file", Err: err}
|
return &ConfigError{Operation: "load_config_file", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查并执行配置迁移
|
return nil
|
||||||
if cs.migrationService != nil {
|
|
||||||
result, err := cs.migrationService.MigrateConfig(cs.koanf)
|
|
||||||
if err != nil {
|
|
||||||
return &ConfigError{Operation: "migrate_config", Err: err}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Migrated && result.ConfigUpdated {
|
// MigrateConfig 执行配置迁移
|
||||||
// 迁移完成且配置已更新,重新创建文件提供器以监听新文件
|
func (cs *ConfigService) MigrateConfig() error {
|
||||||
cs.fileProvider = file.Provider(cs.settingsPath)
|
if cs.configMigrator == nil {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultConfig := models.NewDefaultAppConfig()
|
||||||
|
result, err := cs.configMigrator.AutoMigrate(defaultConfig, cs.koanf)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
cs.logger.Error("Failed to check config migration", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != nil && result.Migrated {
|
||||||
|
cs.logger.Info("Config migration performed",
|
||||||
|
"fields", result.MissingFields,
|
||||||
|
"backup", result.BackupPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -298,6 +311,26 @@ func (cs *ConfigService) SetDataPathChangeCallback(callback func() error) error
|
|||||||
return cs.notificationService.RegisterListener(dataPathListener)
|
return cs.notificationService.RegisterListener(dataPathListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetBackupConfigChangeCallback 设置备份配置变更回调
|
||||||
|
func (cs *ConfigService) SetBackupConfigChangeCallback(callback func(config *models.GitBackupConfig) error) error {
|
||||||
|
cs.mu.Lock()
|
||||||
|
defer cs.mu.Unlock()
|
||||||
|
|
||||||
|
// 创建备份配置监听器并注册
|
||||||
|
backupListener := CreateBackupConfigListener("DefaultBackupConfigListener", callback)
|
||||||
|
return cs.notificationService.RegisterListener(backupListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWindowSnapConfigChangeCallback 设置窗口吸附配置变更回调
|
||||||
|
func (cs *ConfigService) SetWindowSnapConfigChangeCallback(callback func(enabled bool) error) error {
|
||||||
|
cs.mu.Lock()
|
||||||
|
defer cs.mu.Unlock()
|
||||||
|
|
||||||
|
// 创建窗口吸附配置监听器并注册
|
||||||
|
windowSnapListener := CreateWindowSnapConfigListener("DefaultWindowSnapConfigListener", callback)
|
||||||
|
return cs.notificationService.RegisterListener(windowSnapListener)
|
||||||
|
}
|
||||||
|
|
||||||
// ServiceShutdown 关闭服务
|
// ServiceShutdown 关闭服务
|
||||||
func (cs *ConfigService) ServiceShutdown() error {
|
func (cs *ConfigService) ServiceShutdown() error {
|
||||||
cs.stopWatching()
|
cs.stopWatching()
|
||||||
@@ -306,3 +339,13 @@ func (cs *ConfigService) ServiceShutdown() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetConfigDir 获取配置目录
|
||||||
|
func (cs *ConfigService) GetConfigDir() string {
|
||||||
|
return cs.configDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSettingsPath 获取设置文件路径
|
||||||
|
func (cs *ConfigService) GetSettingsPath() string {
|
||||||
|
return cs.settingsPath
|
||||||
|
}
|
||||||
@@ -2,14 +2,18 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
"voidraft/internal/models"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -31,7 +35,8 @@ CREATE TABLE IF NOT EXISTS documents (
|
|||||||
content TEXT DEFAULT '∞∞∞text-a',
|
content TEXT DEFAULT '∞∞∞text-a',
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
is_deleted INTEGER DEFAULT 0
|
is_deleted INTEGER DEFAULT 0,
|
||||||
|
is_locked INTEGER DEFAULT 0
|
||||||
)`
|
)`
|
||||||
|
|
||||||
// Extensions table
|
// Extensions table
|
||||||
@@ -58,28 +63,70 @@ CREATE TABLE IF NOT EXISTS key_bindings (
|
|||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE(command, extension)
|
UNIQUE(command, extension)
|
||||||
)`
|
)`
|
||||||
|
|
||||||
|
// Themes table
|
||||||
|
sqlCreateThemesTable = `
|
||||||
|
CREATE TABLE IF NOT EXISTS themes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
colors TEXT NOT NULL,
|
||||||
|
is_default INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(type, is_default)
|
||||||
|
)`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ColumnInfo 存储列的信息
|
||||||
|
type ColumnInfo struct {
|
||||||
|
SQLType string
|
||||||
|
DefaultValue string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableModel 表示表与模型之间的映射关系
|
||||||
|
type TableModel struct {
|
||||||
|
TableName string
|
||||||
|
Model interface{}
|
||||||
|
}
|
||||||
|
|
||||||
// DatabaseService provides shared database functionality
|
// DatabaseService provides shared database functionality
|
||||||
type DatabaseService struct {
|
type DatabaseService struct {
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
SQLite *sqlite.Service
|
db *sql.DB
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
tableModels []TableModel // 注册的表模型
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDatabaseService creates a new database service
|
// NewDatabaseService creates a new database service
|
||||||
func NewDatabaseService(configService *ConfigService, logger *log.Service) *DatabaseService {
|
func NewDatabaseService(configService *ConfigService, logger *log.LogService) *DatabaseService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DatabaseService{
|
ds := &DatabaseService{
|
||||||
configService: configService,
|
configService: configService,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
SQLite: sqlite.New(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 注册所有模型
|
||||||
|
ds.registerAllModels()
|
||||||
|
|
||||||
|
return ds
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerAllModels 注册所有数据模型,集中管理表-模型映射
|
||||||
|
func (ds *DatabaseService) registerAllModels() {
|
||||||
|
// 文档表
|
||||||
|
ds.RegisterModel("documents", &models.Document{})
|
||||||
|
// 扩展表
|
||||||
|
ds.RegisterModel("extensions", &models.Extension{})
|
||||||
|
// 快捷键表
|
||||||
|
ds.RegisterModel("key_bindings", &models.KeyBinding{})
|
||||||
|
// 主题表
|
||||||
|
ds.RegisterModel("themes", &models.Theme{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceStartup initializes the service when the application starts
|
// ServiceStartup initializes the service when the application starts
|
||||||
@@ -101,28 +148,19 @@ func (ds *DatabaseService) initDatabase() error {
|
|||||||
return fmt.Errorf("failed to create database directory: %w", err)
|
return fmt.Errorf("failed to create database directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查数据库文件是否存在,如果不存在则创建
|
|
||||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
|
||||||
// 创建空文件
|
|
||||||
file, err := os.Create(dbPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create database file: %w", err)
|
|
||||||
}
|
|
||||||
file.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 配置SQLite服务
|
|
||||||
ds.SQLite.Configure(&sqlite.Config{
|
|
||||||
DBSource: dbPath,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 打开数据库连接
|
// 打开数据库连接
|
||||||
if err := ds.SQLite.Open(); err != nil {
|
ds.db, err = sql.Open("sqlite", dbPath)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open database: %w", err)
|
return fmt.Errorf("failed to open database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 测试连接
|
||||||
|
if err := ds.db.Ping(); err != nil {
|
||||||
|
return fmt.Errorf("failed to ping database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// 应用性能优化设置
|
// 应用性能优化设置
|
||||||
if err := ds.SQLite.Execute(sqlOptimizationSettings); err != nil {
|
if _, err := ds.db.Exec(sqlOptimizationSettings); err != nil {
|
||||||
return fmt.Errorf("failed to apply optimization settings: %w", err)
|
return fmt.Errorf("failed to apply optimization settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +173,11 @@ func (ds *DatabaseService) initDatabase() error {
|
|||||||
return fmt.Errorf("failed to create indexes: %w", err)
|
return fmt.Errorf("failed to create indexes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 执行模型与表结构同步
|
||||||
|
if err := ds.syncAllModelTables(); err != nil {
|
||||||
|
return fmt.Errorf("failed to sync model tables: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,10 +196,11 @@ func (ds *DatabaseService) createTables() error {
|
|||||||
sqlCreateDocumentsTable,
|
sqlCreateDocumentsTable,
|
||||||
sqlCreateExtensionsTable,
|
sqlCreateExtensionsTable,
|
||||||
sqlCreateKeyBindingsTable,
|
sqlCreateKeyBindingsTable,
|
||||||
|
sqlCreateThemesTable,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
if err := ds.SQLite.Execute(table); err != nil {
|
if _, err := ds.db.Exec(table); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,27 +220,173 @@ func (ds *DatabaseService) createIndexes() error {
|
|||||||
`CREATE INDEX IF NOT EXISTS idx_key_bindings_command ON key_bindings(command)`,
|
`CREATE INDEX IF NOT EXISTS idx_key_bindings_command ON key_bindings(command)`,
|
||||||
`CREATE INDEX IF NOT EXISTS idx_key_bindings_extension ON key_bindings(extension)`,
|
`CREATE INDEX IF NOT EXISTS idx_key_bindings_extension ON key_bindings(extension)`,
|
||||||
`CREATE INDEX IF NOT EXISTS idx_key_bindings_enabled ON key_bindings(enabled)`,
|
`CREATE INDEX IF NOT EXISTS idx_key_bindings_enabled ON key_bindings(enabled)`,
|
||||||
|
// Themes indexes
|
||||||
|
`CREATE INDEX IF NOT EXISTS idx_themes_type ON themes(type)`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS idx_themes_is_default ON themes(is_default)`,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, index := range indexes {
|
for _, index := range indexes {
|
||||||
if err := ds.SQLite.Execute(index); err != nil {
|
if _, err := ds.db.Exec(index); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterModel 注册模型与表的映射关系
|
||||||
|
func (ds *DatabaseService) RegisterModel(tableName string, model interface{}) {
|
||||||
|
ds.mu.Lock()
|
||||||
|
defer ds.mu.Unlock()
|
||||||
|
|
||||||
|
ds.tableModels = append(ds.tableModels, TableModel{
|
||||||
|
TableName: tableName,
|
||||||
|
Model: model,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncAllModelTables 同步所有注册的模型与表结构
|
||||||
|
func (ds *DatabaseService) syncAllModelTables() error {
|
||||||
|
for _, tm := range ds.tableModels {
|
||||||
|
if err := ds.syncModelTable(tm.TableName, tm.Model); err != nil {
|
||||||
|
return fmt.Errorf("failed to sync table %s: %w", tm.TableName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncModelTable 同步模型与表结构
|
||||||
|
func (ds *DatabaseService) syncModelTable(tableName string, model interface{}) error {
|
||||||
|
// 获取表结构元数据
|
||||||
|
columns, err := ds.getTableColumns(tableName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get table columns: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用反射从模型中提取字段信息
|
||||||
|
expectedColumns, err := ds.getModelColumns(model)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get model columns: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查缺失的列并添加
|
||||||
|
for colName, colInfo := range expectedColumns {
|
||||||
|
if _, exists := columns[colName]; !exists {
|
||||||
|
// 执行添加列的SQL
|
||||||
|
alterSQL := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s DEFAULT %s",
|
||||||
|
tableName, colName, colInfo.SQLType, colInfo.DefaultValue)
|
||||||
|
if _, err := ds.db.Exec(alterSQL); err != nil {
|
||||||
|
return fmt.Errorf("failed to add column %s: %w", colName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTableColumns 获取表的列信息
|
||||||
|
func (ds *DatabaseService) getTableColumns(table string) (map[string]string, error) {
|
||||||
|
query := fmt.Sprintf("PRAGMA table_info(%s)", table)
|
||||||
|
rows, err := ds.db.Query(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
columns := make(map[string]string)
|
||||||
|
for rows.Next() {
|
||||||
|
var cid int
|
||||||
|
var name, typeName string
|
||||||
|
var notNull, pk int
|
||||||
|
var dflt_value interface{}
|
||||||
|
|
||||||
|
if err := rows.Scan(&cid, &name, &typeName, ¬Null, &dflt_value, &pk); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
columns[name] = typeName
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getModelColumns 从模型结构体中提取数据库列信息
|
||||||
|
func (ds *DatabaseService) getModelColumns(model interface{}) (map[string]ColumnInfo, error) {
|
||||||
|
columns := make(map[string]ColumnInfo)
|
||||||
|
|
||||||
|
// 使用反射获取结构体的类型信息
|
||||||
|
t := reflect.TypeOf(model)
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Kind() != reflect.Struct {
|
||||||
|
return nil, fmt.Errorf("model must be a struct or a pointer to struct")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历所有字段
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
|
||||||
|
// 只处理有db标签的字段
|
||||||
|
dbTag := field.Tag.Get("db")
|
||||||
|
if dbTag == "" {
|
||||||
|
// 如果没有db标签,跳过该字段
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取字段类型对应的SQL类型和默认值
|
||||||
|
sqlType, defaultVal := getSQLTypeAndDefault(field.Type)
|
||||||
|
|
||||||
|
columns[dbTag] = ColumnInfo{
|
||||||
|
SQLType: sqlType,
|
||||||
|
DefaultValue: defaultVal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSQLTypeAndDefault 根据Go类型获取对应的SQL类型和默认值
|
||||||
|
func getSQLTypeAndDefault(t reflect.Type) (string, string) {
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
return "INTEGER", "0"
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||||
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return "INTEGER", "0"
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return "REAL", "0.0"
|
||||||
|
case reflect.String:
|
||||||
|
return "TEXT", "''"
|
||||||
|
default:
|
||||||
|
// 处理特殊类型
|
||||||
|
if t == reflect.TypeOf(time.Time{}) {
|
||||||
|
return "DATETIME", "CURRENT_TIMESTAMP"
|
||||||
|
}
|
||||||
|
return "TEXT", "NULL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ServiceShutdown shuts down the service when the application closes
|
// ServiceShutdown shuts down the service when the application closes
|
||||||
func (ds *DatabaseService) ServiceShutdown() error {
|
func (ds *DatabaseService) ServiceShutdown() error {
|
||||||
return ds.SQLite.Close()
|
if ds.db != nil {
|
||||||
|
return ds.db.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnDataPathChanged handles data path changes
|
// OnDataPathChanged handles data path changes
|
||||||
func (ds *DatabaseService) OnDataPathChanged() error {
|
func (ds *DatabaseService) OnDataPathChanged() error {
|
||||||
// 关闭当前连接
|
// 关闭当前连接
|
||||||
if err := ds.SQLite.Close(); err != nil {
|
if ds.db != nil {
|
||||||
|
if err := ds.db.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 用新路径重新初始化
|
// 用新路径重新初始化
|
||||||
return ds.initDatabase()
|
return ds.initDatabase()
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import (
|
|||||||
|
|
||||||
// DialogService 对话框服务,处理文件选择等对话框操作
|
// DialogService 对话框服务,处理文件选择等对话框操作
|
||||||
type DialogService struct {
|
type DialogService struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
window *application.WebviewWindow // 绑定的窗口
|
window *application.WebviewWindow // 绑定的窗口
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDialogService 创建新的对话框服务实例
|
// NewDialogService 创建新的对话框服务实例
|
||||||
func NewDialogService(logger *log.Service) *DialogService {
|
func NewDialogService(logger *log.LogService) *DialogService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,6 @@ func (ds *DialogService) SelectDirectory() (string, error) {
|
|||||||
|
|
||||||
// 对话框文本配置
|
// 对话框文本配置
|
||||||
Title: "Select Directory",
|
Title: "Select Directory",
|
||||||
Message: "Select the folder where you want to store your app data",
|
|
||||||
ButtonText: "Select",
|
ButtonText: "Select",
|
||||||
|
|
||||||
// 不设置过滤器,因为我们选择目录
|
// 不设置过滤器,因为我们选择目录
|
||||||
@@ -69,3 +68,44 @@ func (ds *DialogService) SelectDirectory() (string, error) {
|
|||||||
}
|
}
|
||||||
return path, nil
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SelectFile 打开文件选择对话框
|
||||||
|
func (ds *DialogService) SelectFile() (string, error) {
|
||||||
|
dialog := application.OpenFileDialog()
|
||||||
|
dialog.SetOptions(&application.OpenFileDialogOptions{
|
||||||
|
// 目录选择配置
|
||||||
|
CanChooseDirectories: false, // 允许选择目录
|
||||||
|
CanChooseFiles: true, // 不允许选择文件
|
||||||
|
CanCreateDirectories: true, // 允许创建新目录
|
||||||
|
AllowsMultipleSelection: false, // 单选模式
|
||||||
|
|
||||||
|
// 显示配置
|
||||||
|
ShowHiddenFiles: true, // 不显示隐藏文件
|
||||||
|
HideExtension: false, // 不隐藏扩展名
|
||||||
|
CanSelectHiddenExtension: false, // 不允许选择隐藏扩展名
|
||||||
|
TreatsFilePackagesAsDirectories: false, // 不将文件包当作目录处理
|
||||||
|
AllowsOtherFileTypes: false, // 不允许其他文件类型
|
||||||
|
|
||||||
|
// 系统配置
|
||||||
|
ResolvesAliases: true, // 解析别名/快捷方式
|
||||||
|
|
||||||
|
// 对话框文本配置
|
||||||
|
Title: "Select File",
|
||||||
|
ButtonText: "Select File",
|
||||||
|
|
||||||
|
// 不设置过滤器,因为我们选择目录
|
||||||
|
Filters: nil,
|
||||||
|
|
||||||
|
// 不指定默认目录,让系统决定
|
||||||
|
Directory: "",
|
||||||
|
|
||||||
|
// 绑定到主窗口
|
||||||
|
Window: ds.window,
|
||||||
|
})
|
||||||
|
|
||||||
|
path, err := dialog.PromptForSingleSelection()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
@@ -18,28 +18,28 @@ const (
|
|||||||
|
|
||||||
// Document operations
|
// Document operations
|
||||||
sqlGetDocumentByID = `
|
sqlGetDocumentByID = `
|
||||||
SELECT id, title, content, created_at, updated_at, is_deleted
|
SELECT id, title, content, created_at, updated_at, is_deleted, is_locked
|
||||||
FROM documents
|
FROM documents
|
||||||
WHERE id = ?`
|
WHERE id = ?`
|
||||||
|
|
||||||
sqlInsertDocument = `
|
sqlInsertDocument = `
|
||||||
INSERT INTO documents (title, content, created_at, updated_at, is_deleted)
|
INSERT INTO documents (title, content, created_at, updated_at, is_deleted, is_locked)
|
||||||
VALUES (?, ?, ?, ?, 0)`
|
VALUES (?, ?, ?, ?, 0, 0)`
|
||||||
|
|
||||||
sqlUpdateDocumentContent = `
|
sqlUpdateDocumentContent = `
|
||||||
UPDATE documents
|
UPDATE documents
|
||||||
SET content = ?, updated_at = ?
|
SET content = ?, updated_at = ?
|
||||||
WHERE id = ?`
|
WHERE id = ? AND is_deleted = 0`
|
||||||
|
|
||||||
sqlUpdateDocumentTitle = `
|
sqlUpdateDocumentTitle = `
|
||||||
UPDATE documents
|
UPDATE documents
|
||||||
SET title = ?, updated_at = ?
|
SET title = ?, updated_at = ?
|
||||||
WHERE id = ?`
|
WHERE id = ? AND is_deleted = 0`
|
||||||
|
|
||||||
sqlMarkDocumentAsDeleted = `
|
sqlMarkDocumentAsDeleted = `
|
||||||
UPDATE documents
|
UPDATE documents
|
||||||
SET is_deleted = 1, updated_at = ?
|
SET is_deleted = 1, updated_at = ?
|
||||||
WHERE id = ?`
|
WHERE id = ? AND is_locked = 0`
|
||||||
|
|
||||||
sqlRestoreDocument = `
|
sqlRestoreDocument = `
|
||||||
UPDATE documents
|
UPDATE documents
|
||||||
@@ -47,13 +47,13 @@ SET is_deleted = 0, updated_at = ?
|
|||||||
WHERE id = ?`
|
WHERE id = ?`
|
||||||
|
|
||||||
sqlListAllDocumentsMeta = `
|
sqlListAllDocumentsMeta = `
|
||||||
SELECT id, title, created_at, updated_at
|
SELECT id, title, created_at, updated_at, is_locked
|
||||||
FROM documents
|
FROM documents
|
||||||
WHERE is_deleted = 0
|
WHERE is_deleted = 0
|
||||||
ORDER BY updated_at DESC`
|
ORDER BY updated_at DESC`
|
||||||
|
|
||||||
sqlListDeletedDocumentsMeta = `
|
sqlListDeletedDocumentsMeta = `
|
||||||
SELECT id, title, created_at, updated_at
|
SELECT id, title, created_at, updated_at, is_locked
|
||||||
FROM documents
|
FROM documents
|
||||||
WHERE is_deleted = 1
|
WHERE is_deleted = 1
|
||||||
ORDER BY updated_at DESC`
|
ORDER BY updated_at DESC`
|
||||||
@@ -63,33 +63,46 @@ SELECT id FROM documents WHERE is_deleted = 0 ORDER BY id LIMIT 1`
|
|||||||
|
|
||||||
sqlCountDocuments = `SELECT COUNT(*) FROM documents WHERE is_deleted = 0`
|
sqlCountDocuments = `SELECT COUNT(*) FROM documents WHERE is_deleted = 0`
|
||||||
|
|
||||||
|
sqlSetDocumentLocked = `
|
||||||
|
UPDATE documents
|
||||||
|
SET is_locked = 1, updated_at = ?
|
||||||
|
WHERE id = ?`
|
||||||
|
|
||||||
|
sqlSetDocumentUnlocked = `
|
||||||
|
UPDATE documents
|
||||||
|
SET is_locked = 0, updated_at = ?
|
||||||
|
WHERE id = ?`
|
||||||
|
|
||||||
sqlDefaultDocumentID = 1 // 默认文档的ID
|
sqlDefaultDocumentID = 1 // 默认文档的ID
|
||||||
)
|
)
|
||||||
|
|
||||||
// DocumentService provides document management functionality
|
// DocumentService provides document management functionality
|
||||||
type DocumentService struct {
|
type DocumentService struct {
|
||||||
databaseService *DatabaseService
|
databaseService *DatabaseService
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDocumentService creates a new document service
|
// NewDocumentService creates a new document service
|
||||||
func NewDocumentService(databaseService *DatabaseService, logger *log.Service) *DocumentService {
|
func NewDocumentService(databaseService *DatabaseService, logger *log.LogService) *DocumentService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DocumentService{
|
ds := &DocumentService{
|
||||||
databaseService: databaseService,
|
databaseService: databaseService,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ds
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceStartup initializes the service when the application starts
|
// ServiceStartup initializes the service when the application starts
|
||||||
func (ds *DocumentService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
func (ds *DocumentService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||||
ds.ctx = ctx
|
ds.ctx = ctx
|
||||||
// Ensure default document exists
|
|
||||||
|
// 确保默认文档存在
|
||||||
if err := ds.ensureDefaultDocument(); err != nil {
|
if err := ds.ensureDefaultDocument(); err != nil {
|
||||||
return fmt.Errorf("failed to ensure default document: %w", err)
|
return fmt.Errorf("failed to ensure default document: %w", err)
|
||||||
}
|
}
|
||||||
@@ -98,19 +111,15 @@ func (ds *DocumentService) ServiceStartup(ctx context.Context, options applicati
|
|||||||
|
|
||||||
// ensureDefaultDocument ensures a default document exists
|
// ensureDefaultDocument ensures a default document exists
|
||||||
func (ds *DocumentService) ensureDefaultDocument() error {
|
func (ds *DocumentService) ensureDefaultDocument() error {
|
||||||
|
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||||
|
return errors.New("database service not available")
|
||||||
|
}
|
||||||
|
|
||||||
// Check if any document exists
|
// Check if any document exists
|
||||||
rows, err := ds.databaseService.SQLite.Query(sqlCountDocuments)
|
var count int64
|
||||||
|
err := ds.databaseService.db.QueryRow(sqlCountDocuments).Scan(&count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to query document count: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
if len(rows) == 0 {
|
|
||||||
return fmt.Errorf("failed to query document count")
|
|
||||||
}
|
|
||||||
|
|
||||||
count, ok := rows[0]["COUNT(*)"].(int64)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("failed to convert count to int64")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no documents exist, create default document
|
// If no documents exist, create default document
|
||||||
@@ -127,48 +136,42 @@ func (ds *DocumentService) GetDocumentByID(id int64) (*models.Document, error) {
|
|||||||
ds.mu.RLock()
|
ds.mu.RLock()
|
||||||
defer ds.mu.RUnlock()
|
defer ds.mu.RUnlock()
|
||||||
|
|
||||||
rows, err := ds.databaseService.SQLite.Query(sqlGetDocumentByID, id)
|
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||||
|
return nil, errors.New("database service not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
doc := &models.Document{}
|
||||||
|
var createdAt, updatedAt string
|
||||||
|
var isDeleted, isLocked int
|
||||||
|
|
||||||
|
err := ds.databaseService.db.QueryRow(sqlGetDocumentByID, id).Scan(
|
||||||
|
&doc.ID,
|
||||||
|
&doc.Title,
|
||||||
|
&doc.Content,
|
||||||
|
&createdAt,
|
||||||
|
&updatedAt,
|
||||||
|
&isDeleted,
|
||||||
|
&isLocked,
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("failed to get document by ID: %w", err)
|
return nil, fmt.Errorf("failed to get document by ID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rows) == 0 {
|
// 转换时间字段
|
||||||
return nil, nil
|
if t, err := time.Parse("2006-01-02 15:04:05", createdAt); err == nil {
|
||||||
}
|
|
||||||
|
|
||||||
row := rows[0]
|
|
||||||
doc := &models.Document{}
|
|
||||||
|
|
||||||
// 从Row中提取数据
|
|
||||||
if idVal, ok := row["id"].(int64); ok {
|
|
||||||
doc.ID = idVal
|
|
||||||
}
|
|
||||||
|
|
||||||
if title, ok := row["title"].(string); ok {
|
|
||||||
doc.Title = title
|
|
||||||
}
|
|
||||||
|
|
||||||
if content, ok := row["content"].(string); ok {
|
|
||||||
doc.Content = content
|
|
||||||
}
|
|
||||||
|
|
||||||
if createdAt, ok := row["created_at"].(string); ok {
|
|
||||||
t, err := time.Parse("2006-01-02 15:04:05", createdAt)
|
|
||||||
if err == nil {
|
|
||||||
doc.CreatedAt = t
|
doc.CreatedAt = t
|
||||||
}
|
}
|
||||||
}
|
if t, err := time.Parse("2006-01-02 15:04:05", updatedAt); err == nil {
|
||||||
|
|
||||||
if updatedAt, ok := row["updated_at"].(string); ok {
|
|
||||||
t, err := time.Parse("2006-01-02 15:04:05", updatedAt)
|
|
||||||
if err == nil {
|
|
||||||
doc.UpdatedAt = t
|
doc.UpdatedAt = t
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if isDeletedInt, ok := row["is_deleted"].(int64); ok {
|
// 转换布尔字段
|
||||||
doc.IsDeleted = isDeletedInt == 1
|
doc.IsDeleted = isDeleted == 1
|
||||||
}
|
doc.IsLocked = isLocked == 1
|
||||||
|
|
||||||
return doc, nil
|
return doc, nil
|
||||||
}
|
}
|
||||||
@@ -178,6 +181,10 @@ func (ds *DocumentService) CreateDocument(title string) (*models.Document, error
|
|||||||
ds.mu.Lock()
|
ds.mu.Lock()
|
||||||
defer ds.mu.Unlock()
|
defer ds.mu.Unlock()
|
||||||
|
|
||||||
|
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||||
|
return nil, errors.New("database service not available")
|
||||||
|
}
|
||||||
|
|
||||||
// Create document with default content
|
// Create document with default content
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
doc := &models.Document{
|
doc := &models.Document{
|
||||||
@@ -186,41 +193,102 @@ func (ds *DocumentService) CreateDocument(title string) (*models.Document, error
|
|||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
IsDeleted: false,
|
IsDeleted: false,
|
||||||
|
IsLocked: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行插入操作
|
// 执行插入操作
|
||||||
if err := ds.databaseService.SQLite.Execute(sqlInsertDocument,
|
result, err := ds.databaseService.db.Exec(sqlInsertDocument,
|
||||||
doc.Title, doc.Content, doc.CreatedAt, doc.UpdatedAt); err != nil {
|
doc.Title, doc.Content, doc.CreatedAt.Format("2006-01-02 15:04:05"), doc.UpdatedAt.Format("2006-01-02 15:04:05"))
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create document: %w", err)
|
return nil, fmt.Errorf("failed to create document: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取自增ID
|
// 获取自增ID
|
||||||
lastIDRows, err := ds.databaseService.SQLite.Query("SELECT last_insert_rowid()")
|
lastID, err := result.LastInsertId()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get last insert ID: %w", err)
|
return nil, fmt.Errorf("failed to get last insert ID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(lastIDRows) == 0 {
|
|
||||||
return nil, fmt.Errorf("no rows returned for last insert ID query")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从结果中提取ID
|
|
||||||
lastID, ok := lastIDRows[0]["last_insert_rowid()"].(int64)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("failed to convert last insert ID to int64")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回带ID的文档
|
// 返回带ID的文档
|
||||||
doc.ID = lastID
|
doc.ID = lastID
|
||||||
return doc, nil
|
return doc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LockDocument 锁定文档,防止删除
|
||||||
|
func (ds *DocumentService) LockDocument(id int64) error {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if doc == nil {
|
||||||
|
return fmt.Errorf("document not found: %d", id)
|
||||||
|
}
|
||||||
|
if doc.IsDeleted {
|
||||||
|
return fmt.Errorf("cannot lock deleted document: %d", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经锁定,无需操作
|
||||||
|
if doc.IsLocked {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnlockDocument 解锁文档
|
||||||
|
func (ds *DocumentService) UnlockDocument(id int64) error {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if doc == nil {
|
||||||
|
return fmt.Errorf("document not found: %d", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果未锁定,无需操作
|
||||||
|
if !doc.IsLocked {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateDocumentContent updates the content of a document
|
// UpdateDocumentContent updates the content of a document
|
||||||
func (ds *DocumentService) UpdateDocumentContent(id int64, content string) error {
|
func (ds *DocumentService) UpdateDocumentContent(id int64, content string) error {
|
||||||
ds.mu.Lock()
|
ds.mu.Lock()
|
||||||
defer ds.mu.Unlock()
|
defer ds.mu.Unlock()
|
||||||
|
|
||||||
err := ds.databaseService.SQLite.Execute(sqlUpdateDocumentContent, content, time.Now(), id)
|
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||||
|
return errors.New("database service not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ds.databaseService.db.Exec(sqlUpdateDocumentContent, content, time.Now().Format("2006-01-02 15:04:05"), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update document content: %w", err)
|
return fmt.Errorf("failed to update document content: %w", err)
|
||||||
}
|
}
|
||||||
@@ -232,7 +300,11 @@ func (ds *DocumentService) UpdateDocumentTitle(id int64, title string) error {
|
|||||||
ds.mu.Lock()
|
ds.mu.Lock()
|
||||||
defer ds.mu.Unlock()
|
defer ds.mu.Unlock()
|
||||||
|
|
||||||
err := ds.databaseService.SQLite.Execute(sqlUpdateDocumentTitle, title, time.Now(), id)
|
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||||
|
return errors.New("database service not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ds.databaseService.db.Exec(sqlUpdateDocumentTitle, title, time.Now().Format("2006-01-02 15:04:05"), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update document title: %w", err)
|
return fmt.Errorf("failed to update document title: %w", err)
|
||||||
}
|
}
|
||||||
@@ -241,15 +313,33 @@ func (ds *DocumentService) UpdateDocumentTitle(id int64, title string) error {
|
|||||||
|
|
||||||
// DeleteDocument marks a document as deleted (default document with ID=1 cannot be deleted)
|
// DeleteDocument marks a document as deleted (default document with ID=1 cannot be deleted)
|
||||||
func (ds *DocumentService) DeleteDocument(id int64) error {
|
func (ds *DocumentService) DeleteDocument(id int64) error {
|
||||||
ds.mu.Lock()
|
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||||
defer ds.mu.Unlock()
|
ds.logger.Error("database service not available")
|
||||||
|
return errors.New("database service not available")
|
||||||
|
}
|
||||||
|
|
||||||
// 不允许删除默认文档
|
// 不允许删除默认文档
|
||||||
if id == sqlDefaultDocumentID {
|
if id == sqlDefaultDocumentID {
|
||||||
return fmt.Errorf("cannot delete the default document")
|
return fmt.Errorf("cannot delete the default document")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := ds.databaseService.SQLite.Execute(sqlMarkDocumentAsDeleted, time.Now(), id)
|
// 先检查文档是否存在和锁定状态(不加锁避免死锁)
|
||||||
|
doc, err := ds.GetDocumentByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get document: %w", err)
|
||||||
|
}
|
||||||
|
if doc == nil {
|
||||||
|
return fmt.Errorf("document not found: %d", id)
|
||||||
|
}
|
||||||
|
if doc.IsLocked {
|
||||||
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to mark document as deleted: %w", err)
|
return fmt.Errorf("failed to mark document as deleted: %w", err)
|
||||||
}
|
}
|
||||||
@@ -261,7 +351,11 @@ func (ds *DocumentService) RestoreDocument(id int64) error {
|
|||||||
ds.mu.Lock()
|
ds.mu.Lock()
|
||||||
defer ds.mu.Unlock()
|
defer ds.mu.Unlock()
|
||||||
|
|
||||||
err := ds.databaseService.SQLite.Execute(sqlRestoreDocument, time.Now(), id)
|
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||||
|
return errors.New("database service not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ds.databaseService.db.Exec(sqlRestoreDocument, time.Now().Format("2006-01-02 15:04:05"), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to restore document: %w", err)
|
return fmt.Errorf("failed to restore document: %w", err)
|
||||||
}
|
}
|
||||||
@@ -273,38 +367,48 @@ func (ds *DocumentService) ListAllDocumentsMeta() ([]*models.Document, error) {
|
|||||||
ds.mu.RLock()
|
ds.mu.RLock()
|
||||||
defer ds.mu.RUnlock()
|
defer ds.mu.RUnlock()
|
||||||
|
|
||||||
rows, err := ds.databaseService.SQLite.Query(sqlListAllDocumentsMeta)
|
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||||
|
return nil, errors.New("database service not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := ds.databaseService.db.Query(sqlListAllDocumentsMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to list document meta: %w", err)
|
return nil, fmt.Errorf("failed to list document meta: %w", err)
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
var documents []*models.Document
|
var documents []*models.Document
|
||||||
for _, row := range rows {
|
for rows.Next() {
|
||||||
doc := &models.Document{IsDeleted: false}
|
doc := &models.Document{IsDeleted: false}
|
||||||
|
var createdAt, updatedAt string
|
||||||
|
var isLocked int
|
||||||
|
|
||||||
if id, ok := row["id"].(int64); ok {
|
err := rows.Scan(
|
||||||
doc.ID = id
|
&doc.ID,
|
||||||
|
&doc.Title,
|
||||||
|
&createdAt,
|
||||||
|
&updatedAt,
|
||||||
|
&isLocked,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to scan document row: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if title, ok := row["title"].(string); ok {
|
// 转换时间字段
|
||||||
doc.Title = title
|
if t, err := time.Parse("2006-01-02 15:04:05", createdAt); err == nil {
|
||||||
}
|
|
||||||
|
|
||||||
if createdAt, ok := row["created_at"].(string); ok {
|
|
||||||
t, err := time.Parse("2006-01-02 15:04:05", createdAt)
|
|
||||||
if err == nil {
|
|
||||||
doc.CreatedAt = t
|
doc.CreatedAt = t
|
||||||
}
|
}
|
||||||
}
|
if t, err := time.Parse("2006-01-02 15:04:05", updatedAt); err == nil {
|
||||||
|
|
||||||
if updatedAt, ok := row["updated_at"].(string); ok {
|
|
||||||
t, err := time.Parse("2006-01-02 15:04:05", updatedAt)
|
|
||||||
if err == nil {
|
|
||||||
doc.UpdatedAt = t
|
doc.UpdatedAt = t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
doc.IsLocked = isLocked == 1
|
||||||
|
documents = append(documents, doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
documents = append(documents, doc)
|
if err = rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("error iterating document rows: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return documents, nil
|
return documents, nil
|
||||||
@@ -315,38 +419,48 @@ func (ds *DocumentService) ListDeletedDocumentsMeta() ([]*models.Document, error
|
|||||||
ds.mu.RLock()
|
ds.mu.RLock()
|
||||||
defer ds.mu.RUnlock()
|
defer ds.mu.RUnlock()
|
||||||
|
|
||||||
rows, err := ds.databaseService.SQLite.Query(sqlListDeletedDocumentsMeta)
|
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||||
|
return nil, errors.New("database service not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := ds.databaseService.db.Query(sqlListDeletedDocumentsMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to list deleted document meta: %w", err)
|
return nil, fmt.Errorf("failed to list deleted document meta: %w", err)
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
var documents []*models.Document
|
var documents []*models.Document
|
||||||
for _, row := range rows {
|
for rows.Next() {
|
||||||
doc := &models.Document{IsDeleted: true}
|
doc := &models.Document{IsDeleted: true}
|
||||||
|
var createdAt, updatedAt string
|
||||||
|
var isLocked int
|
||||||
|
|
||||||
if id, ok := row["id"].(int64); ok {
|
err := rows.Scan(
|
||||||
doc.ID = id
|
&doc.ID,
|
||||||
|
&doc.Title,
|
||||||
|
&createdAt,
|
||||||
|
&updatedAt,
|
||||||
|
&isLocked,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to scan document row: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if title, ok := row["title"].(string); ok {
|
// 转换时间字段
|
||||||
doc.Title = title
|
if t, err := time.Parse("2006-01-02 15:04:05", createdAt); err == nil {
|
||||||
}
|
|
||||||
|
|
||||||
if createdAt, ok := row["created_at"].(string); ok {
|
|
||||||
t, err := time.Parse("2006-01-02 15:04:05", createdAt)
|
|
||||||
if err == nil {
|
|
||||||
doc.CreatedAt = t
|
doc.CreatedAt = t
|
||||||
}
|
}
|
||||||
}
|
if t, err := time.Parse("2006-01-02 15:04:05", updatedAt); err == nil {
|
||||||
|
|
||||||
if updatedAt, ok := row["updated_at"].(string); ok {
|
|
||||||
t, err := time.Parse("2006-01-02 15:04:05", updatedAt)
|
|
||||||
if err == nil {
|
|
||||||
doc.UpdatedAt = t
|
doc.UpdatedAt = t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
doc.IsLocked = isLocked == 1
|
||||||
|
documents = append(documents, doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
documents = append(documents, doc)
|
if err = rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("error iterating deleted document rows: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return documents, nil
|
return documents, nil
|
||||||
@@ -357,7 +471,12 @@ func (ds *DocumentService) GetFirstDocumentID() (int64, error) {
|
|||||||
ds.mu.RLock()
|
ds.mu.RLock()
|
||||||
defer ds.mu.RUnlock()
|
defer ds.mu.RUnlock()
|
||||||
|
|
||||||
rows, err := ds.databaseService.SQLite.Query(sqlGetFirstDocumentID)
|
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||||
|
return 0, errors.New("database service not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
var id int64
|
||||||
|
err := ds.databaseService.db.QueryRow(sqlGetFirstDocumentID).Scan(&id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return 0, nil // No documents exist
|
return 0, nil // No documents exist
|
||||||
@@ -365,14 +484,5 @@ func (ds *DocumentService) GetFirstDocumentID() (int64, error) {
|
|||||||
return 0, fmt.Errorf("failed to get first document ID: %w", err)
|
return 0, fmt.Errorf("failed to get first document ID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rows) == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
id, ok := rows[0]["id"].(int64)
|
|
||||||
if !ok {
|
|
||||||
return 0, fmt.Errorf("failed to convert ID to int64")
|
|
||||||
}
|
|
||||||
|
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ WHERE id = ?`
|
|||||||
// ExtensionService 扩展管理服务
|
// ExtensionService 扩展管理服务
|
||||||
type ExtensionService struct {
|
type ExtensionService struct {
|
||||||
databaseService *DatabaseService
|
databaseService *DatabaseService
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -73,7 +73,7 @@ func (e *ExtensionError) Is(target error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewExtensionService 创建扩展服务实例
|
// NewExtensionService 创建扩展服务实例
|
||||||
func NewExtensionService(databaseService *DatabaseService, logger *log.Service) *ExtensionService {
|
func NewExtensionService(databaseService *DatabaseService, logger *log.LogService) *ExtensionService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
@@ -104,21 +104,17 @@ func (es *ExtensionService) initDatabase() error {
|
|||||||
es.mu.Lock()
|
es.mu.Lock()
|
||||||
defer es.mu.Unlock()
|
defer es.mu.Unlock()
|
||||||
|
|
||||||
|
if es.databaseService == nil || es.databaseService.db == nil {
|
||||||
|
return &ExtensionError{"check_db", "", errors.New("database service not available")}
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否已有扩展数据
|
// 检查是否已有扩展数据
|
||||||
rows, err := es.databaseService.SQLite.Query("SELECT COUNT(*) FROM extensions")
|
var count int64
|
||||||
|
err := es.databaseService.db.QueryRow("SELECT COUNT(*) FROM extensions").Scan(&count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &ExtensionError{"check_extensions_count", "", err}
|
return &ExtensionError{"check_extensions_count", "", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rows) == 0 {
|
|
||||||
return &ExtensionError{"check_extensions_count", "", fmt.Errorf("no rows returned")}
|
|
||||||
}
|
|
||||||
|
|
||||||
count, ok := rows[0]["COUNT(*)"].(int64)
|
|
||||||
if !ok {
|
|
||||||
return &ExtensionError{"convert_count", "", fmt.Errorf("failed to convert count to int64")}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有数据,插入默认配置
|
// 如果没有数据,插入默认配置
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
if err := es.insertDefaultExtensions(); err != nil {
|
if err := es.insertDefaultExtensions(); err != nil {
|
||||||
@@ -133,16 +129,15 @@ func (es *ExtensionService) initDatabase() error {
|
|||||||
// insertDefaultExtensions 插入默认扩展配置
|
// insertDefaultExtensions 插入默认扩展配置
|
||||||
func (es *ExtensionService) insertDefaultExtensions() error {
|
func (es *ExtensionService) insertDefaultExtensions() error {
|
||||||
defaultSettings := models.NewDefaultExtensionSettings()
|
defaultSettings := models.NewDefaultExtensionSettings()
|
||||||
now := time.Now()
|
now := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
for _, ext := range defaultSettings.Extensions {
|
for _, ext := range defaultSettings.Extensions {
|
||||||
|
|
||||||
configJSON, err := json.Marshal(ext.Config)
|
configJSON, err := json.Marshal(ext.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &ExtensionError{"marshal_config", string(ext.ID), err}
|
return &ExtensionError{"marshal_config", string(ext.ID), err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = es.databaseService.SQLite.Execute(sqlInsertExtension,
|
_, err = es.databaseService.db.Exec(sqlInsertExtension,
|
||||||
string(ext.ID),
|
string(ext.ID),
|
||||||
ext.Enabled,
|
ext.Enabled,
|
||||||
ext.IsDefault,
|
ext.IsDefault,
|
||||||
@@ -153,7 +148,6 @@ func (es *ExtensionService) insertDefaultExtensions() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return &ExtensionError{"insert_extension", string(ext.ID), err}
|
return &ExtensionError{"insert_extension", string(ext.ID), err}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -179,38 +173,51 @@ func (es *ExtensionService) GetAllExtensions() ([]models.Extension, error) {
|
|||||||
es.mu.RLock()
|
es.mu.RLock()
|
||||||
defer es.mu.RUnlock()
|
defer es.mu.RUnlock()
|
||||||
|
|
||||||
rows, err := es.databaseService.SQLite.Query(sqlGetAllExtensions)
|
if es.databaseService == nil || es.databaseService.db == nil {
|
||||||
|
return nil, &ExtensionError{"query_db", "", errors.New("database service not available")}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := es.databaseService.db.Query(sqlGetAllExtensions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &ExtensionError{"query_extensions", "", err}
|
return nil, &ExtensionError{"query_extensions", "", err}
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
var extensions []models.Extension
|
var extensions []models.Extension
|
||||||
for _, row := range rows {
|
for rows.Next() {
|
||||||
var ext models.Extension
|
var ext models.Extension
|
||||||
|
var id string
|
||||||
|
var configJSON string
|
||||||
|
var enabled, isDefault int
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&id,
|
||||||
|
&enabled,
|
||||||
|
&isDefault,
|
||||||
|
&configJSON,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, &ExtensionError{"scan_extension", "", err}
|
||||||
|
}
|
||||||
|
|
||||||
if id, ok := row["id"].(string); ok {
|
|
||||||
ext.ID = models.ExtensionID(id)
|
ext.ID = models.ExtensionID(id)
|
||||||
}
|
|
||||||
|
|
||||||
if enabled, ok := row["enabled"].(int64); ok {
|
|
||||||
ext.Enabled = enabled == 1
|
ext.Enabled = enabled == 1
|
||||||
}
|
|
||||||
|
|
||||||
if isDefault, ok := row["is_default"].(int64); ok {
|
|
||||||
ext.IsDefault = isDefault == 1
|
ext.IsDefault = isDefault == 1
|
||||||
}
|
|
||||||
|
|
||||||
if configJSON, ok := row["config"].(string); ok {
|
|
||||||
var config models.ExtensionConfig
|
var config models.ExtensionConfig
|
||||||
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
|
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
|
||||||
return nil, &ExtensionError{"unmarshal_config", string(ext.ID), err}
|
return nil, &ExtensionError{"unmarshal_config", id, err}
|
||||||
}
|
}
|
||||||
ext.Config = config
|
ext.Config = config
|
||||||
}
|
|
||||||
|
|
||||||
extensions = append(extensions, ext)
|
extensions = append(extensions, ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return nil, &ExtensionError{"iterate_extensions", "", err}
|
||||||
|
}
|
||||||
|
|
||||||
return extensions, nil
|
return extensions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,6 +231,10 @@ func (es *ExtensionService) UpdateExtensionState(id models.ExtensionID, enabled
|
|||||||
es.mu.Lock()
|
es.mu.Lock()
|
||||||
defer es.mu.Unlock()
|
defer es.mu.Unlock()
|
||||||
|
|
||||||
|
if es.databaseService == nil || es.databaseService.db == nil {
|
||||||
|
return &ExtensionError{"check_db", string(id), errors.New("database service not available")}
|
||||||
|
}
|
||||||
|
|
||||||
var configJSON []byte
|
var configJSON []byte
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -234,24 +245,19 @@ func (es *ExtensionService) UpdateExtensionState(id models.ExtensionID, enabled
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果没有提供配置,保持原有配置
|
// 如果没有提供配置,保持原有配置
|
||||||
rows, err := es.databaseService.SQLite.Query("SELECT config FROM extensions WHERE id = ?", string(id))
|
var currentConfigJSON string
|
||||||
|
err := es.databaseService.db.QueryRow("SELECT config FROM extensions WHERE id = ?", string(id)).Scan(¤tConfigJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &ExtensionError{"query_current_config", string(id), err}
|
return &ExtensionError{"query_current_config", string(id), err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rows) == 0 {
|
|
||||||
return &ExtensionError{"query_current_config", string(id), fmt.Errorf("extension not found")}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentConfigJSON, ok := rows[0]["config"].(string)
|
|
||||||
if !ok {
|
|
||||||
return &ExtensionError{"convert_config", string(id), fmt.Errorf("failed to get current config")}
|
|
||||||
}
|
|
||||||
|
|
||||||
configJSON = []byte(currentConfigJSON)
|
configJSON = []byte(currentConfigJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = es.databaseService.SQLite.Execute(sqlUpdateExtension, enabled, string(configJSON), time.Now(), string(id))
|
_, err = es.databaseService.db.Exec(sqlUpdateExtension,
|
||||||
|
enabled,
|
||||||
|
string(configJSON),
|
||||||
|
time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
string(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &ExtensionError{"update_extension", string(id), err}
|
return &ExtensionError{"update_extension", string(id), err}
|
||||||
}
|
}
|
||||||
@@ -276,8 +282,12 @@ func (es *ExtensionService) ResetAllExtensionsToDefault() error {
|
|||||||
es.mu.Lock()
|
es.mu.Lock()
|
||||||
defer es.mu.Unlock()
|
defer es.mu.Unlock()
|
||||||
|
|
||||||
|
if es.databaseService == nil || es.databaseService.db == nil {
|
||||||
|
return &ExtensionError{"check_db", "", errors.New("database service not available")}
|
||||||
|
}
|
||||||
|
|
||||||
// 删除所有现有扩展
|
// 删除所有现有扩展
|
||||||
err := es.databaseService.SQLite.Execute(sqlDeleteAllExtensions)
|
_, err := es.databaseService.db.Exec(sqlDeleteAllExtensions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &ExtensionError{"delete_all_extensions", "", err}
|
return &ExtensionError{"delete_all_extensions", "", err}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ package services
|
|||||||
#cgo CFLAGS: -I../lib
|
#cgo CFLAGS: -I../lib
|
||||||
#cgo LDFLAGS: -luser32
|
#cgo LDFLAGS: -luser32
|
||||||
#include "../lib/hotkey_windows.c"
|
#include "../lib/hotkey_windows.c"
|
||||||
#include "hotkey_windows.h"
|
#include "../lib/hotkey_windows.h"
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
@@ -24,16 +24,18 @@ import (
|
|||||||
|
|
||||||
// HotkeyService Windows全局热键服务
|
// HotkeyService Windows全局热键服务
|
||||||
type HotkeyService struct {
|
type HotkeyService struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
|
windowService *WindowService
|
||||||
app *application.App
|
app *application.App
|
||||||
|
mainWindow *application.WebviewWindow
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
currentHotkey *models.HotkeyCombo
|
currentHotkey *models.HotkeyCombo
|
||||||
isRegistered atomic.Bool
|
isRegistered atomic.Bool
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancelFunc atomic.Value // 使用atomic.Value存储cancel函数,避免竞态条件
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,24 +53,48 @@ func (e *HotkeyError) Unwrap() error {
|
|||||||
return e.Err
|
return e.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setCancelFunc 原子地设置cancel函数
|
||||||
|
func (hs *HotkeyService) setCancelFunc(cancel context.CancelFunc) {
|
||||||
|
hs.cancelFunc.Store(cancel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCancelFunc 原子地获取cancel函数
|
||||||
|
func (hs *HotkeyService) getCancelFunc() context.CancelFunc {
|
||||||
|
if cancel := hs.cancelFunc.Load(); cancel != nil {
|
||||||
|
if cancelFunc, ok := cancel.(context.CancelFunc); ok {
|
||||||
|
return cancelFunc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearCancelFunc 原子地清除cancel函数
|
||||||
|
func (hs *HotkeyService) clearCancelFunc() {
|
||||||
|
hs.cancelFunc.Store((context.CancelFunc)(nil))
|
||||||
|
}
|
||||||
|
|
||||||
// NewHotkeyService 创建热键服务实例
|
// NewHotkeyService 创建热键服务实例
|
||||||
func NewHotkeyService(configService *ConfigService, logger *log.Service) *HotkeyService {
|
func NewHotkeyService(configService *ConfigService, windowService *WindowService, logger *log.LogService) *HotkeyService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
return &HotkeyService{
|
service := &HotkeyService{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
configService: configService,
|
configService: configService,
|
||||||
|
windowService: windowService,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
|
||||||
}
|
}
|
||||||
|
// 初始化时设置cancel函数
|
||||||
|
service.setCancelFunc(cancel)
|
||||||
|
return service
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize 初始化热键服务
|
// Initialize 初始化热键服务
|
||||||
func (hs *HotkeyService) Initialize(app *application.App) error {
|
func (hs *HotkeyService) Initialize(app *application.App, mainWindow *application.WebviewWindow) error {
|
||||||
hs.app = app
|
hs.app = app
|
||||||
|
hs.mainWindow = mainWindow
|
||||||
|
|
||||||
config, err := hs.configService.GetConfig()
|
config, err := hs.configService.GetConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -119,7 +145,7 @@ func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error {
|
|||||||
|
|
||||||
hs.currentHotkey = hotkey
|
hs.currentHotkey = hotkey
|
||||||
hs.isRegistered.Store(true)
|
hs.isRegistered.Store(true)
|
||||||
hs.cancel = cancel
|
hs.setCancelFunc(cancel)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -137,13 +163,15 @@ func (hs *HotkeyService) unregisterInternal() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if hs.cancel != nil {
|
// 原子地获取并调用cancel函数
|
||||||
hs.cancel()
|
if cancel := hs.getCancelFunc(); cancel != nil {
|
||||||
|
cancel()
|
||||||
hs.wg.Wait()
|
hs.wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
hs.currentHotkey = nil
|
hs.currentHotkey = nil
|
||||||
hs.isRegistered.Store(false)
|
hs.isRegistered.Store(false)
|
||||||
|
hs.clearCancelFunc()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +193,7 @@ func (hs *HotkeyService) hotkeyListener(ctx context.Context, hotkey *models.Hotk
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ticker := time.NewTicker(100 * time.Millisecond)
|
ticker := time.NewTicker(50 * time.Millisecond)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
var wasPressed bool
|
var wasPressed bool
|
||||||
@@ -199,11 +227,61 @@ func cBool(b bool) C.int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// toggleWindow 切换窗口
|
// toggleWindow 切换窗口显示状态
|
||||||
func (hs *HotkeyService) toggleWindow() {
|
func (hs *HotkeyService) toggleWindow() {
|
||||||
if hs.app != nil {
|
if hs.mainWindow == nil {
|
||||||
hs.app.Event.Emit("hotkey:toggle-window", nil)
|
hs.logger.Error("main window not set")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查主窗口是否可见
|
||||||
|
if hs.isWindowVisible(hs.mainWindow) {
|
||||||
|
// 如果主窗口可见,隐藏所有窗口
|
||||||
|
hs.hideAllWindows()
|
||||||
|
} else {
|
||||||
|
// 如果主窗口不可见,显示所有窗口
|
||||||
|
hs.showAllWindows()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isWindowVisible 检查窗口是否可见
|
||||||
|
func (hs *HotkeyService) isWindowVisible(window *application.WebviewWindow) bool {
|
||||||
|
return window.IsVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
// hideAllWindows 隐藏所有窗口
|
||||||
|
func (hs *HotkeyService) hideAllWindows() {
|
||||||
|
// 隐藏主窗口
|
||||||
|
hs.mainWindow.Hide()
|
||||||
|
|
||||||
|
// 隐藏所有子窗口
|
||||||
|
if hs.windowService != nil {
|
||||||
|
openWindows := hs.windowService.GetOpenWindows()
|
||||||
|
for _, windowInfo := range openWindows {
|
||||||
|
windowInfo.Window.Hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hs.logger.Debug("all windows hidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// showAllWindows 显示所有窗口
|
||||||
|
func (hs *HotkeyService) showAllWindows() {
|
||||||
|
// 显示主窗口
|
||||||
|
hs.mainWindow.Show()
|
||||||
|
hs.mainWindow.Restore()
|
||||||
|
hs.mainWindow.Focus()
|
||||||
|
|
||||||
|
// 显示所有子窗口
|
||||||
|
if hs.windowService != nil {
|
||||||
|
openWindows := hs.windowService.GetOpenWindows()
|
||||||
|
for _, windowInfo := range openWindows {
|
||||||
|
windowInfo.Window.Show()
|
||||||
|
windowInfo.Window.Restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hs.logger.Debug("all windows shown")
|
||||||
}
|
}
|
||||||
|
|
||||||
// keyToVirtualKeyCode 键名转虚拟键码
|
// keyToVirtualKeyCode 键名转虚拟键码
|
||||||
@@ -261,7 +339,10 @@ func (hs *HotkeyService) IsRegistered() bool {
|
|||||||
|
|
||||||
// ServiceShutdown 关闭服务
|
// ServiceShutdown 关闭服务
|
||||||
func (hs *HotkeyService) ServiceShutdown() error {
|
func (hs *HotkeyService) ServiceShutdown() error {
|
||||||
hs.cancel()
|
// 原子地获取并调用cancel函数
|
||||||
|
if cancel := hs.getCancelFunc(); cancel != nil {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
hs.wg.Wait()
|
hs.wg.Wait()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -66,6 +66,7 @@ int isHotkeyRegistered() {
|
|||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -80,12 +81,15 @@ var globalHotkeyService *HotkeyService
|
|||||||
|
|
||||||
// HotkeyService macOS全局热键服务
|
// HotkeyService macOS全局热键服务
|
||||||
type HotkeyService struct {
|
type HotkeyService struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
|
windowService *WindowService
|
||||||
app *application.App
|
app *application.App
|
||||||
|
mainWindow *application.WebviewWindow
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
isRegistered atomic.Bool
|
isRegistered atomic.Bool
|
||||||
currentHotkey *models.HotkeyCombo
|
currentHotkey *models.HotkeyCombo
|
||||||
|
cancelFunc atomic.Value // 使用atomic.Value存储cancel函数,避免竞态条件
|
||||||
}
|
}
|
||||||
|
|
||||||
// HotkeyError 热键错误
|
// HotkeyError 热键错误
|
||||||
@@ -104,8 +108,28 @@ func (e *HotkeyError) Unwrap() error {
|
|||||||
return e.Err
|
return e.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setCancelFunc 原子地设置cancel函数
|
||||||
|
func (hs *HotkeyService) setCancelFunc(cancel context.CancelFunc) {
|
||||||
|
hs.cancelFunc.Store(cancel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCancelFunc 原子地获取cancel函数
|
||||||
|
func (hs *HotkeyService) getCancelFunc() context.CancelFunc {
|
||||||
|
if cancel := hs.cancelFunc.Load(); cancel != nil {
|
||||||
|
if cancelFunc, ok := cancel.(context.CancelFunc); ok {
|
||||||
|
return cancelFunc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearCancelFunc 原子地清除cancel函数
|
||||||
|
func (hs *HotkeyService) clearCancelFunc() {
|
||||||
|
hs.cancelFunc.Store((context.CancelFunc)(nil))
|
||||||
|
}
|
||||||
|
|
||||||
// NewHotkeyService 创建新的热键服务实例
|
// NewHotkeyService 创建新的热键服务实例
|
||||||
func NewHotkeyService(configService *ConfigService, logger *log.Service) *HotkeyService {
|
func NewHotkeyService(configService *ConfigService, windowService *WindowService, logger *log.LogService) *HotkeyService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
@@ -113,6 +137,7 @@ func NewHotkeyService(configService *ConfigService, logger *log.Service) *Hotkey
|
|||||||
service := &HotkeyService{
|
service := &HotkeyService{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
configService: configService,
|
configService: configService,
|
||||||
|
windowService: windowService,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置全局实例
|
// 设置全局实例
|
||||||
@@ -122,8 +147,9 @@ func NewHotkeyService(configService *ConfigService, logger *log.Service) *Hotkey
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize 初始化热键服务
|
// Initialize 初始化热键服务
|
||||||
func (hs *HotkeyService) Initialize(app *application.App) error {
|
func (hs *HotkeyService) Initialize(app *application.App, mainWindow *application.WebviewWindow) error {
|
||||||
hs.app = app
|
hs.app = app
|
||||||
|
hs.mainWindow = mainWindow
|
||||||
|
|
||||||
// 加载并应用当前配置
|
// 加载并应用当前配置
|
||||||
config, err := hs.configService.GetConfig()
|
config, err := hs.configService.GetConfig()
|
||||||
@@ -285,9 +311,59 @@ func (hs *HotkeyService) IsRegistered() bool {
|
|||||||
|
|
||||||
// ToggleWindow 切换窗口显示状态
|
// ToggleWindow 切换窗口显示状态
|
||||||
func (hs *HotkeyService) ToggleWindow() {
|
func (hs *HotkeyService) ToggleWindow() {
|
||||||
if hs.app != nil {
|
if hs.mainWindow == nil {
|
||||||
hs.app.EmitEvent("hotkey:toggle-window", nil)
|
hs.logger.Error("main window not set")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查主窗口是否可见
|
||||||
|
if hs.isWindowVisible(hs.mainWindow) {
|
||||||
|
// 如果主窗口可见,隐藏所有窗口
|
||||||
|
hs.hideAllWindows()
|
||||||
|
} else {
|
||||||
|
// 如果主窗口不可见,显示所有窗口
|
||||||
|
hs.showAllWindows()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isWindowVisible 检查窗口是否可见
|
||||||
|
func (hs *HotkeyService) isWindowVisible(window *application.WebviewWindow) bool {
|
||||||
|
return window.IsVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
// hideAllWindows 隐藏所有窗口
|
||||||
|
func (hs *HotkeyService) hideAllWindows() {
|
||||||
|
// 隐藏主窗口
|
||||||
|
hs.mainWindow.Hide()
|
||||||
|
|
||||||
|
// 隐藏所有子窗口
|
||||||
|
if hs.windowService != nil {
|
||||||
|
openWindows := hs.windowService.GetOpenWindows()
|
||||||
|
for _, windowInfo := range openWindows {
|
||||||
|
windowInfo.Window.Hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hs.logger.Debug("all windows hidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// showAllWindows 显示所有窗口
|
||||||
|
func (hs *HotkeyService) showAllWindows() {
|
||||||
|
// 显示主窗口
|
||||||
|
hs.mainWindow.Show()
|
||||||
|
hs.mainWindow.Restore()
|
||||||
|
hs.mainWindow.Focus()
|
||||||
|
|
||||||
|
// 显示所有子窗口
|
||||||
|
if hs.windowService != nil {
|
||||||
|
openWindows := hs.windowService.GetOpenWindows()
|
||||||
|
for _, windowInfo := range openWindows {
|
||||||
|
windowInfo.Window.Show()
|
||||||
|
windowInfo.Window.Restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hs.logger.Debug("all windows shown")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceShutdown 关闭热键服务
|
// ServiceShutdown 关闭热键服务
|
||||||
|
|||||||
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user