54 Commits

Author SHA1 Message Date
67d35626cb 🚧 Refactor backup service 2025-12-14 23:48:52 +08:00
cc4c2189dc 🚧 Refactor basic services 2025-12-14 02:19:50 +08:00
d16905c0a3 🐛 Fixed some known issues 2025-12-12 22:56:22 +08:00
4e611db349 Optimize minmap extension completed 2025-12-11 23:59:06 +08:00
7e9fc0ac3f Optimize minmap extension 2025-12-10 22:25:19 +08:00
ff072d1a93 Optimize minmap performance 2025-12-10 00:41:24 +08:00
a9c81c878e 🚚 2025-12-08 23:28:36 +08:00
3660d13d7d ♻️ Refactor search 2025-12-08 23:20:37 +08:00
281f53c049 Optimized markdown preview performance 2025-12-07 00:09:52 +08:00
71ca541f78 🚧 Added support for markdown preview table 2025-12-04 00:47:51 +08:00
91f4f4afac Merge branch 'markdown'
# Conflicts:
#	frontend/package-lock.json
2025-12-03 00:46:17 +08:00
fc5639d7bd 🚧 Added support for markdown preview math 2025-12-03 00:45:01 +08:00
dependabot[bot]
6668c11846 ⬆️ Bump mdast-util-to-hast from 13.2.0 to 13.2.1 in /frontend
Bumps [mdast-util-to-hast](https://github.com/syntax-tree/mdast-util-to-hast) from 13.2.0 to 13.2.1.
- [Release notes](https://github.com/syntax-tree/mdast-util-to-hast/releases)
- [Commits](https://github.com/syntax-tree/mdast-util-to-hast/compare/13.2.0...13.2.1)

---
updated-dependencies:
- dependency-name: mdast-util-to-hast
  dependency-version: 13.2.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-02 04:14:35 +00:00
17f3351cea 🚧 Added support for markdown preview footnotes 2025-12-02 00:22:22 +08:00
dd3dd4ddb2 🚧 Refactor markdown preview extension 2025-12-01 00:00:05 +08:00
60d1494d45 🚧 Refactor markdown preview extension 2025-11-30 01:09:31 +08:00
1ef5350b3f 🚧 Refactor markdown preview extension 2025-11-29 22:54:38 +08:00
3521e5787b 🚧 Refactor markdown preview extension 2025-11-29 19:24:20 +08:00
8d9bcdad7e 🚧 Refactor markdown preview extension 2025-11-28 00:38:38 +08:00
ac086db1ed ♻️ Updated markdown preview extension 2025-11-26 22:11:16 +08:00
6dff0181d2 ♻️ Refactored markdown preview extension 2025-11-24 00:10:28 +08:00
ad24d3a140 ♻️ Refactored translation extension 2025-11-23 18:45:49 +08:00
4b0f39d747 Merge branch 'master' into dev 2025-11-21 23:37:36 +08:00
096cc1da94 🎨 Optimize hyperlink extension 2025-11-21 23:35:42 +08:00
2d3200ad97 ♻️ Refactor context menu 2025-11-21 22:30:47 +08:00
4e82e2f6f7 ♻️ Refactor the Markdown preview theme application logic 2025-11-21 20:20:06 +08:00
339ed53c2e ♻️ Refactor theme module 2025-11-21 00:03:03 +08:00
fc7c162e2f ♻️ Refactor theme module 2025-11-20 23:07:12 +08:00
dependabot[bot]
24f1549730 ⬆️ Bump golang.org/x/crypto from 0.44.0 to 0.45.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.44.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 03:04:41 +00:00
5584a46ca2 ♻️ Refactor theme module 2025-11-20 00:39:00 +08:00
4471441d6f ♻️ Refactor some code 2025-11-19 20:54:58 +08:00
991a89147e 🎨 Optimize code 2025-11-17 23:14:58 +08:00
a08c0d8448 🎨 Modify code block logic 2025-11-17 22:11:16 +08:00
59db8dd177 Added Monocraft font 2025-11-16 22:04:02 +08:00
29693f1baf 💄 Modify some styles 2025-11-16 21:23:59 +08:00
5d6f157ae1 🎨 Update initial code block definition
Some checks failed
Build and Release Voidraft / prepare (push) Failing after 2s
Build and Release Voidraft / build (push) Has been cancelled
Build and Release Voidraft / release (push) Has been cancelled
2025-11-16 17:49:19 +08:00
afda3d5301 Merge remote-tracking branch 'github/dependabot/npm_and_yarn/frontend/js-yaml-4.1.1' 2025-11-16 15:44:58 +08:00
5d4ba757aa Optimize HTTP client 2025-11-16 15:41:16 +08:00
d12d58b15a 🐛 Fixed markdown preview issue 2025-11-16 15:22:49 +08:00
627c3dc71f 🐛 Fixed markdown preview issue 2025-11-16 15:16:49 +08:00
26c7a3241c 🐛 Fixed module path issues 2025-11-16 02:42:59 +08:00
46c5e3dd1a Added markdown and mermaid preview 2025-11-16 02:42:01 +08:00
dependabot[bot]
92a6c6bfdb ⬆️ Bump js-yaml from 4.1.0 to 4.1.1 in /frontend
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-15 18:38:56 +00:00
031aa49f9f Added markdown and mermaid preview 2025-11-16 02:37:30 +08:00
1d7aee4cea 🐛 Fixed data migration issues 2025-11-14 20:16:16 +08:00
dec3ef5ef4 Added cursor protection extension 2025-11-13 21:00:35 +08:00
d42f913250 Added editor cursor state persistence 2025-11-13 20:44:47 +08:00
bae4e663fb ⬆️ Upgrade dependencies 2025-11-13 20:02:22 +08:00
a17e060d16 ⬆️ Upgrade dependencies 2025-11-13 19:36:11 +08:00
71946965eb 🐛 Fixed database constraint issues 2025-11-08 17:35:29 +08:00
d4cd22d234 🚀 Update build and release workflows 2025-11-08 17:17:07 +08:00
05f2f7d46d 🚀 Update build and release workflows 2025-11-08 17:05:31 +08:00
9deb2744a9 🚀 Update build and release workflows 2025-11-08 16:24:22 +08:00
6fac7c42d6 🚀 Update build and release workflows 2025-11-08 16:03:26 +08:00
308 changed files with 36371 additions and 14294 deletions

View File

@@ -50,24 +50,27 @@ jobs:
MATRIX='{"include":[]}'
if [[ "$PLATFORMS" == *"windows"* ]]; then
MATRIX=$(echo $MATRIX | jq '.include += [{"platform":"windows-latest","os":"windows","arch":"amd64","output_name":"voidraft-windows-amd64.exe","id":"windows"}]')
MATRIX=$(echo "$MATRIX" | jq -c '.include += [{"platform":"windows-latest","os":"windows","arch":"amd64","platform_dir":"windows","id":"windows"}]')
fi
if [[ "$PLATFORMS" == *"linux"* ]]; then
MATRIX=$(echo $MATRIX | jq '.include += [{"platform":"ubuntu-22.04","os":"linux","arch":"amd64","output_name":"voidraft-linux-amd64","id":"linux"}]')
MATRIX=$(echo "$MATRIX" | jq -c '.include += [{"platform":"ubuntu-22.04","os":"linux","arch":"amd64","platform_dir":"linux","id":"linux"}]')
fi
if [[ "$PLATFORMS" == *"macos-intel"* ]]; then
MATRIX=$(echo $MATRIX | jq '.include += [{"platform":"macos-latest","os":"darwin","arch":"amd64","output_name":"voidraft-darwin-amd64","id":"macos-intel"}]')
MATRIX=$(echo "$MATRIX" | jq -c '.include += [{"platform":"macos-latest","os":"darwin","arch":"amd64","platform_dir":"darwin","id":"macos-intel"}]')
fi
if [[ "$PLATFORMS" == *"macos-arm"* ]]; then
MATRIX=$(echo $MATRIX | jq '.include += [{"platform":"macos-latest","os":"darwin","arch":"arm64","output_name":"voidraft-darwin-arm64","id":"macos-arm"}]')
MATRIX=$(echo "$MATRIX" | jq -c '.include += [{"platform":"macos-latest","os":"darwin","arch":"arm64","platform_dir":"darwin","id":"macos-arm"}]')
fi
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
# 使用 -c 确保输出紧凑的单行 JSON符合 GitHub Actions 输出格式
echo "matrix=$(echo "$MATRIX" | jq -c .)" >> $GITHUB_OUTPUT
# 输出美化的 JSON 用于日志查看
echo "生成的矩阵:"
echo $MATRIX | jq .
echo "$MATRIX" | jq .
- name: 检查是否创建 Release
id: check-release
@@ -113,17 +116,41 @@ jobs:
if: matrix.os == 'linux'
run: |
sudo apt-get update
# Wails 3 + 项目特定依赖
sudo apt-get install -y \
build-essential \
pkg-config \
libgtk-3-dev \
libwebkit2gtk-4.0-dev \
pkg-config
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf \
libx11-dev \
rpm \
fuse \
file
# 依赖说明:
# - build-essential: C/C++ 编译工具链
# - pkg-config: 包配置工具
# - libgtk-3-dev: GTK3 GUI 框架
# - libwebkit2gtk-4.1-dev: WebKit2GTK 4.1 (Wails 3 要求)
# - libayatana-appindicator3-dev: 系统托盘支持
# - librsvg2-dev: SVG 图标支持
# - patchelf: 修改 ELF 二进制文件
# - libx11-dev: X11 库 (热键服务依赖)
# - rpm: RPM 打包工具
# - fuse: AppImage 运行依赖
# - file: 文件类型检测工具
# Windows 平台依赖GitHub Actions 的 Windows runner 已包含 MinGW
# Windows 平台依赖
- name: 设置 Windows 构建环境
if: matrix.os == 'windows'
run: |
echo "Windows runner 已包含构建工具"
# 安装 NSIS (用于创建安装程序)
choco install nsis -y
# 将 NSIS 添加到 PATH
echo "C:\Program Files (x86)\NSIS" >> $GITHUB_PATH
shell: bash
# macOS 平台依赖
@@ -147,52 +174,64 @@ jobs:
working-directory: frontend
run: npm run build
# 构建 Wails 应用
- name: 构建 Wails 应用
run: |
wails3 build -platform ${{ matrix.os }}/${{ matrix.arch }}
# 使用 Wails Task 构建和打包应用
- name: 构建和打包 Wails 应用
run: wails3 task ${{ matrix.id }}:package PRODUCTION=true ARCH=${{ matrix.arch }}
env:
CGO_ENABLED: 1
APP_NAME: voidraft
BIN_DIR: bin
ROOT_DIR: .
shell: bash
# 查找构建产物
- name: 查找构建产物
id: find_binary
shell: bash
# 整理构建产物
- name: 整理构建产物
id: organize_artifacts
run: |
echo "=== 构建产物列表 ==="
ls -lhR bin/ || echo "bin 目录不存在"
# 创建输出目录
mkdir -p artifacts
# 根据平台复制产物
if [ "${{ matrix.os }}" = "windows" ]; then
BINARY=$(find build/bin -name "*.exe" -type f | head -n 1)
echo "Windows 平台:查找 NSIS 安装程序"
find . -name "*.exe" -type f
cp bin/*.exe artifacts/ 2>/dev/null || echo "未找到 .exe 文件"
elif [ "${{ matrix.os }}" = "linux" ]; then
echo "Linux 平台:查找 AppImage, deb, rpm, archlinux 包"
find bin -type f
cp bin/*.AppImage artifacts/ 2>/dev/null || echo "未找到 AppImage"
cp bin/*.deb artifacts/ 2>/dev/null || echo "未找到 deb"
cp bin/*.rpm artifacts/ 2>/dev/null || echo "未找到 rpm"
cp bin/*.pkg.tar.zst artifacts/ 2>/dev/null || echo "未找到 archlinux 包"
elif [ "${{ matrix.os }}" = "darwin" ]; then
# macOS 可能生成 .app 包或二进制文件
BINARY=$(find build/bin -type f \( -name "*.app" -o ! -name ".*" \) | head -n 1)
else
BINARY=$(find build/bin -type f ! -name ".*" | head -n 1)
fi
echo "binary_path=$BINARY" >> $GITHUB_OUTPUT
echo "找到的二进制文件: $BINARY"
# 重命名构建产物
- name: 重命名构建产物
shell: bash
run: |
BINARY_PATH="${{ steps.find_binary.outputs.binary_path }}"
if [ -n "$BINARY_PATH" ]; then
# 对于 macOS .app 包,打包为 zip
if [[ "$BINARY_PATH" == *.app ]]; then
cd "$(dirname "$BINARY_PATH")"
zip -r "${{ matrix.output_name }}.zip" "$(basename "$BINARY_PATH")"
echo "ARTIFACT_PATH=$(pwd)/${{ matrix.output_name }}.zip" >> $GITHUB_ENV
echo "macOS 平台:查找 .app bundle"
find bin -name "*.app" -type d
# macOS: .app bundle打包成 zip
if [ -d "bin/voidraft.app" ]; then
cd bin
zip -r ../artifacts/voidraft-darwin-${{ matrix.arch }}.app.zip voidraft.app
cd ..
else
cp "$BINARY_PATH" "${{ matrix.output_name }}"
echo "ARTIFACT_PATH=$(pwd)/${{ matrix.output_name }}" >> $GITHUB_ENV
echo "未找到 .app bundle"
fi
fi
echo "=== 最终产物 ==="
ls -lh artifacts/ || echo "artifacts 目录为空"
shell: bash
# 上传构建产物到 Artifacts
- name: 上传构建产物
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.output_name }}
path: ${{ env.ARTIFACT_PATH }}
if-no-files-found: error
name: voidraft-${{ matrix.id }}
path: artifacts/*
if-no-files-found: warn
# 创建 GitHub Release 并上传所有构建产物
release:

View File

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

293
build/COMMANDS.md Normal file
View File

@@ -0,0 +1,293 @@
# Wails 3 命令参考表
本文档列出了 Voidraft 项目中使用的所有 Wails 3 命令和参数。
## 📋 命令总览
| 类别 | 命令 | 用途 |
|------|------|------|
| 生成工具 | `wails3 generate` | 生成项目所需的各种资源文件 |
| 打包工具 | `wails3 tool package` | 使用 nfpm 打包应用程序 |
| 任务执行 | `wails3 task` | 执行 Taskfile.yml 中定义的任务 |
---
## 🔧 详细命令参数
### 1. `wails3 generate bindings`
**生成 TypeScript 绑定文件**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-f` | 构建标志 | `''` (空字符串) |
| `-clean` | 清理旧绑定 | `true` |
| `-ts` | 生成 TypeScript | 无需值 |
**使用位置:** `build/Taskfile.yml:53`
**完整命令:**
```bash
wails3 generate bindings -f '' -clean=true -ts
```
---
### 2. `wails3 generate icons`
**从单个图片生成多平台图标**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-input` | 输入图片路径 | `appicon.png` |
| `-macfilename` | macOS 图标输出路径 | `darwin/icons.icns` |
| `-windowsfilename` | Windows 图标输出路径 | `windows/icons.ico` |
**使用位置:** `build/Taskfile.yml:64`
**完整命令:**
```bash
wails3 generate icons \
-input appicon.png \
-macfilename darwin/icons.icns \
-windowsfilename windows/icons.ico
```
---
### 3. `wails3 generate syso`
**生成 Windows .syso 资源文件**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-arch` | 目标架构 | `amd64` / `arm64` |
| `-icon` | 图标文件 | `windows/icon.ico` |
| `-manifest` | 清单文件 | `windows/wails.exe.manifest` |
| `-info` | 应用信息 JSON | `windows/info.json` |
| `-out` | 输出文件路径 | `../wails_windows_amd64.syso` |
**使用位置:** `build/windows/Taskfile.yml:42`
**完整命令:**
```bash
wails3 generate syso \
-arch amd64 \
-icon windows/icon.ico \
-manifest windows/wails.exe.manifest \
-info windows/info.json \
-out ../wails_windows_amd64.syso
```
---
### 4. `wails3 generate webview2bootstrapper`
**生成 Windows WebView2 引导程序**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-dir` | 输出目录 | `build/windows/nsis` |
**使用位置:** `build/windows/Taskfile.yml:55`
**完整命令:**
```bash
wails3 generate webview2bootstrapper -dir "build/windows/nsis"
```
**说明:** 下载 Microsoft Edge WebView2 运行时安装程序,用于 NSIS 打包。
---
### 5. `wails3 generate .desktop`
**生成 Linux .desktop 桌面文件**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-name` | 应用名称 | `voidraft` |
| `-exec` | 可执行文件名 | `voidraft` |
| `-icon` | 图标名称 | `appicon` |
| `-outputfile` | 输出文件路径 | `build/linux/voidraft.desktop` |
| `-categories` | 应用分类 | `Development;` |
**使用位置:** `build/linux/Taskfile.yml:107`
**完整命令:**
```bash
wails3 generate .desktop \
-name "voidraft" \
-exec "voidraft" \
-icon "appicon" \
-outputfile build/linux/voidraft.desktop \
-categories "Development;"
```
---
### 6. `wails3 generate appimage`
**生成 Linux AppImage 包**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-binary` | 二进制文件名 | `voidraft` |
| `-icon` | 图标文件路径 | `../../appicon.png` |
| `-desktopfile` | .desktop 文件路径 | `../voidraft.desktop` |
| `-outputdir` | 输出目录 | `../../../bin` |
| `-builddir` | 构建临时目录 | `build/linux/appimage/build` |
**使用位置:** `build/linux/Taskfile.yml:49`
**完整命令:**
```bash
wails3 generate appimage \
-binary voidraft \
-icon ../../appicon.png \
-desktopfile ../voidraft.desktop \
-outputdir ../../../bin \
-builddir build/linux/appimage/build
```
**说明:** 自动下载 linuxdeploy 工具并创建 AppImage。
---
### 7. `wails3 tool package` (deb)
**创建 Debian/Ubuntu .deb 包**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-name` | 包名称 | `voidraft` |
| `-format` | 包格式 | `deb` |
| `-config` | nfpm 配置文件 | `./build/linux/nfpm/nfpm.yaml` |
| `-out` | 输出目录 | `./bin` |
**使用位置:** `build/linux/Taskfile.yml:90`
**完整命令:**
```bash
wails3 tool package \
-name voidraft \
-format deb \
-config ./build/linux/nfpm/nfpm.yaml \
-out ./bin
```
---
### 8. `wails3 tool package` (rpm)
**创建 RedHat/CentOS/Fedora .rpm 包**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-name` | 包名称 | `voidraft` |
| `-format` | 包格式 | `rpm` |
| `-config` | nfpm 配置文件 | `./build/linux/nfpm/nfpm.yaml` |
| `-out` | 输出目录 | `./bin` |
**使用位置:** `build/linux/Taskfile.yml:95`
**完整命令:**
```bash
wails3 tool package \
-name voidraft \
-format rpm \
-config ./build/linux/nfpm/nfpm.yaml \
-out ./bin
```
---
### 9. `wails3 tool package` (archlinux)
**创建 Arch Linux .pkg.tar.zst 包**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `-name` | 包名称 | `voidraft` |
| `-format` | 包格式 | `archlinux` |
| `-config` | nfpm 配置文件 | `./build/linux/nfpm/nfpm.yaml` |
| `-out` | 输出目录 | `./bin` |
**使用位置:** `build/linux/Taskfile.yml:100`
**完整命令:**
```bash
wails3 tool package \
-name voidraft \
-format archlinux \
-config ./build/linux/nfpm/nfpm.yaml \
-out ./bin
```
---
### 10. `wails3 task`
**执行 Taskfile.yml 中定义的任务**
| 参数 | 说明 | 示例值 |
|------|------|--------|
| `[taskname]` | 任务名称 | `build`, `package`, `run` |
| `[VAR=value]` | 变量赋值 | `PRODUCTION=true`, `ARCH=amd64` |
**常用任务:**
| 任务 | 说明 | 命令 |
|------|------|------|
| `build` | 构建应用 | `wails3 task build PRODUCTION=true` |
| `package` | 打包应用 | `wails3 task package` |
| `run` | 运行应用 | `wails3 task run` |
**使用位置:** `build/config.yml`
---
## 📊 平台对应命令表
| 平台 | 主要命令 | 产物 |
|------|----------|------|
| **Windows** | `generate syso`<br>`generate webview2bootstrapper` | `.syso` 资源文件<br>NSIS 安装程序 |
| **Linux** | `generate .desktop`<br>`generate appimage`<br>`tool package -format deb/rpm/archlinux` | `.desktop` 文件<br>`.AppImage`<br>`.deb` / `.rpm` / `.pkg.tar.zst` |
| **macOS** | `generate icons` | `.icns` 图标<br>`.app` 应用包 |
| **通用** | `generate bindings`<br>`generate icons` | TypeScript 绑定<br>多平台图标 |
---
## 🚀 快速参考
### 完整构建流程
```bash
# 1. 生成绑定和图标
wails3 task common:generate:bindings
wails3 task common:generate:icons
# 2. 构建前端
cd frontend
npm install
npm run build
cd ..
# 3. 构建应用(各平台)
cd build/windows && wails3 task build PRODUCTION=true # Windows
cd build/linux && wails3 task build PRODUCTION=true # Linux
cd build/darwin && wails3 task build PRODUCTION=true # macOS
# 4. 打包应用(各平台)
cd build/windows && wails3 task package # NSIS 安装程序
cd build/linux && wails3 task package # AppImage + deb + rpm + archlinux
cd build/darwin && wails3 task package # .app bundle
```
---
## 📝 注意事项
1. **变量传递:** Task 命令支持通过 `VAR=value` 格式传递变量
2. **路径问题:** 相对路径基于 Taskfile.yml 所在目录
3. **依赖顺序:** 某些任务有依赖关系(通过 `deps:` 定义)
4. **环境变量:** 使用 `env:` 定义的环境变量会自动设置
---
## 🔗 相关文档
- [Wails 3 官方文档](https://v3alpha.wails.io/)
- [Taskfile 语法](https://taskfile.dev/)
- [nfpm 打包工具](https://nfpm.goreleaser.com/)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

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

View File

@@ -0,0 +1,334 @@
// 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 theme$0 from "./theme/models.js";
/**
* Document is the model entity for the Document schema.
*/
export class Document {
/**
* ID of the ent.
*/
"id"?: number;
/**
* UUID for cross-device sync (UUIDv7)
*/
"uuid": string;
/**
* creation time
*/
"created_at": string;
/**
* update time
*/
"updated_at": string;
/**
* deleted at
*/
"deleted_at"?: string | null;
/**
* document title
*/
"title": string;
/**
* document content
*/
"content": string;
/**
* document locked status
*/
"locked": boolean;
/** Creates a new Document instance. */
constructor($$source: Partial<Document> = {}) {
if (!("uuid" in $$source)) {
this["uuid"] = "";
}
if (!("created_at" in $$source)) {
this["created_at"] = "";
}
if (!("updated_at" in $$source)) {
this["updated_at"] = "";
}
if (!("title" in $$source)) {
this["title"] = "";
}
if (!("content" in $$source)) {
this["content"] = "";
}
if (!("locked" in $$source)) {
this["locked"] = false;
}
Object.assign(this, $$source);
}
/**
* Creates a new Document instance from a string or object.
*/
static createFrom($$source: any = {}): Document {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new Document($$parsedSource as Partial<Document>);
}
}
/**
* Extension is the model entity for the Extension schema.
*/
export class Extension {
/**
* ID of the ent.
*/
"id"?: number;
/**
* UUID for cross-device sync (UUIDv7)
*/
"uuid": string;
/**
* creation time
*/
"created_at": string;
/**
* update time
*/
"updated_at": string;
/**
* deleted at
*/
"deleted_at"?: string | null;
/**
* extension key
*/
"key": string;
/**
* extension enabled or not
*/
"enabled": boolean;
/**
* extension config
*/
"config": { [_: string]: any };
/** Creates a new Extension instance. */
constructor($$source: Partial<Extension> = {}) {
if (!("uuid" in $$source)) {
this["uuid"] = "";
}
if (!("created_at" in $$source)) {
this["created_at"] = "";
}
if (!("updated_at" in $$source)) {
this["updated_at"] = "";
}
if (!("key" in $$source)) {
this["key"] = "";
}
if (!("enabled" in $$source)) {
this["enabled"] = false;
}
if (!("config" in $$source)) {
this["config"] = {};
}
Object.assign(this, $$source);
}
/**
* Creates a new Extension instance from a string or object.
*/
static createFrom($$source: any = {}): Extension {
const $$createField7_0 = $$createType0;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("config" in $$parsedSource) {
$$parsedSource["config"] = $$createField7_0($$parsedSource["config"]);
}
return new Extension($$parsedSource as Partial<Extension>);
}
}
/**
* KeyBinding is the model entity for the KeyBinding schema.
*/
export class KeyBinding {
/**
* ID of the ent.
*/
"id"?: number;
/**
* UUID for cross-device sync (UUIDv7)
*/
"uuid": string;
/**
* creation time
*/
"created_at": string;
/**
* update time
*/
"updated_at": string;
/**
* deleted at
*/
"deleted_at"?: string | null;
/**
* key binding key
*/
"key": string;
/**
* key binding command
*/
"command": string;
/**
* key binding extension
*/
"extension"?: string;
/**
* key binding enabled
*/
"enabled": boolean;
/** Creates a new KeyBinding instance. */
constructor($$source: Partial<KeyBinding> = {}) {
if (!("uuid" in $$source)) {
this["uuid"] = "";
}
if (!("created_at" in $$source)) {
this["created_at"] = "";
}
if (!("updated_at" in $$source)) {
this["updated_at"] = "";
}
if (!("key" in $$source)) {
this["key"] = "";
}
if (!("command" in $$source)) {
this["command"] = "";
}
if (!("enabled" in $$source)) {
this["enabled"] = false;
}
Object.assign(this, $$source);
}
/**
* Creates a new KeyBinding instance from a string or object.
*/
static createFrom($$source: any = {}): KeyBinding {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new KeyBinding($$parsedSource as Partial<KeyBinding>);
}
}
/**
* Theme is the model entity for the Theme schema.
*/
export class Theme {
/**
* ID of the ent.
*/
"id"?: number;
/**
* UUID for cross-device sync (UUIDv7)
*/
"uuid": string;
/**
* creation time
*/
"created_at": string;
/**
* update time
*/
"updated_at": string;
/**
* deleted at
*/
"deleted_at"?: string | null;
/**
* theme key
*/
"key": string;
/**
* theme type
*/
"type": theme$0.Type;
/**
* theme colors
*/
"colors": { [_: string]: any };
/** Creates a new Theme instance. */
constructor($$source: Partial<Theme> = {}) {
if (!("uuid" in $$source)) {
this["uuid"] = "";
}
if (!("created_at" in $$source)) {
this["created_at"] = "";
}
if (!("updated_at" in $$source)) {
this["updated_at"] = "";
}
if (!("key" in $$source)) {
this["key"] = "";
}
if (!("type" in $$source)) {
this["type"] = ("" as theme$0.Type);
}
if (!("colors" in $$source)) {
this["colors"] = {};
}
Object.assign(this, $$source);
}
/**
* Creates a new Theme instance from a string or object.
*/
static createFrom($$source: any = {}): Theme {
const $$createField7_0 = $$createType0;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("colors" in $$parsedSource) {
$$parsedSource["colors"] = $$createField7_0($$parsedSource["colors"]);
}
return new Theme($$parsedSource as Partial<Theme>);
}
}
// Private type creation functions
const $$createType0 = $Create.Map($Create.Any, $Create.Any);

View File

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

View File

@@ -0,0 +1,22 @@
// 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";
/**
* Type defines the type for the "type" enum field.
*/
export enum Type {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* Type values.
*/
TypeDark = "dark",
TypeLight = "light",
};

View File

@@ -193,58 +193,6 @@ export class ConfigMetadata {
}
}
/**
* Document represents a document in the system
*/
export class Document {
"id": number;
"title": string;
"content": string;
"createdAt": string;
"updatedAt": string;
"is_deleted": boolean;
/**
* 锁定标志,锁定的文档无法被删除
*/
"is_locked": boolean;
/** Creates a new Document instance. */
constructor($$source: Partial<Document> = {}) {
if (!("id" in $$source)) {
this["id"] = 0;
}
if (!("title" in $$source)) {
this["title"] = "";
}
if (!("content" in $$source)) {
this["content"] = "";
}
if (!("createdAt" in $$source)) {
this["createdAt"] = "";
}
if (!("updatedAt" in $$source)) {
this["updatedAt"] = "";
}
if (!("is_deleted" in $$source)) {
this["is_deleted"] = false;
}
if (!("is_locked" in $$source)) {
this["is_locked"] = false;
}
Object.assign(this, $$source);
}
/**
* Creates a new Document instance from a string or object.
*/
static createFrom($$source: any = {}): Document {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new Document($$parsedSource as Partial<Document>);
}
}
/**
* EditingConfig 编辑设置配置
*/
@@ -332,40 +280,21 @@ export class EditingConfig {
}
/**
* Extension 单个扩展配置
* Extension 扩展配置
*/
export class Extension {
/**
* 扩展唯一标识
*/
"id": ExtensionID;
/**
* 是否启用
*/
"key": ExtensionKey;
"enabled": boolean;
/**
* 是否为默认扩展
*/
"isDefault": boolean;
/**
* 扩展配置项
*/
"config": ExtensionConfig;
/** Creates a new Extension instance. */
constructor($$source: Partial<Extension> = {}) {
if (!("id" in $$source)) {
this["id"] = ("" as ExtensionID);
if (!("key" in $$source)) {
this["key"] = ("" as ExtensionKey);
}
if (!("enabled" in $$source)) {
this["enabled"] = false;
}
if (!("isDefault" in $$source)) {
this["isDefault"] = false;
}
if (!("config" in $$source)) {
this["config"] = ({} as ExtensionConfig);
}
@@ -377,10 +306,10 @@ export class Extension {
* Creates a new Extension instance from a string or object.
*/
static createFrom($$source: any = {}): Extension {
const $$createField3_0 = $$createType6;
const $$createField2_0 = $$createType6;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("config" in $$parsedSource) {
$$parsedSource["config"] = $$createField3_0($$parsedSource["config"]);
$$parsedSource["config"] = $$createField2_0($$parsedSource["config"]);
}
return new Extension($$parsedSource as Partial<Extension>);
}
@@ -392,9 +321,9 @@ export class Extension {
export type ExtensionConfig = { [_: string]: any };
/**
* ExtensionID 扩展标识符
* ExtensionKey 扩展标识符
*/
export enum ExtensionID {
export enum ExtensionKey {
/**
* The Go zero value for the underlying type of the enum.
*/
@@ -415,13 +344,11 @@ export enum ExtensionID {
* 颜色选择器
*/
ExtensionColorSelector = "colorSelector",
ExtensionFold = "fold",
ExtensionTextHighlight = "textHighlight",
/**
* 选择框
* 代码折叠
*/
ExtensionCheckbox = "checkbox",
ExtensionFold = "fold",
/**
* 划词翻译
@@ -429,22 +356,44 @@ export enum ExtensionID {
ExtensionTranslator = "translator",
/**
* UI增强扩展
* Markdown渲染
*/
ExtensionMarkdown = "markdown",
/**
* 显示空白字符
*/
ExtensionHighlightWhitespace = "highlightWhitespace",
/**
* 高亮行尾空白
*/
ExtensionHighlightTrailingWhitespace = "highlightTrailingWhitespace",
/**
* 小地图
*/
ExtensionMinimap = "minimap",
/**
* 工具扩展
* 行号显示
*/
ExtensionLineNumbers = "lineNumbers",
/**
* 上下文菜单
*/
ExtensionContextMenu = "contextMenu",
/**
* 搜索功能
*/
ExtensionSearch = "search",
/**
* 核心扩展
* 编辑器核心功能
* HTTP 客户端
*/
ExtensionEditor = "editor",
ExtensionHttpClient = "httpClient",
};
/**
@@ -735,48 +684,25 @@ export class HotkeyCombo {
* KeyBinding 单个快捷键绑定
*/
export class KeyBinding {
/**
* 快捷键动作
*/
"command": KeyBindingCommand;
/**
* 所属扩展
*/
"extension": ExtensionID;
/**
* 快捷键组合(如 "Mod-f", "Ctrl-Shift-p"
*/
"key": string;
/**
* 是否启用
*/
"key": KeyBindingKey;
"command": string;
"extension": ExtensionKey;
"enabled": boolean;
/**
* 是否为默认快捷键
*/
"isDefault": boolean;
/** Creates a new KeyBinding instance. */
constructor($$source: Partial<KeyBinding> = {}) {
if (!("key" in $$source)) {
this["key"] = ("" as KeyBindingKey);
}
if (!("command" in $$source)) {
this["command"] = ("" as KeyBindingCommand);
this["command"] = "";
}
if (!("extension" in $$source)) {
this["extension"] = ("" as ExtensionID);
}
if (!("key" in $$source)) {
this["key"] = "";
this["extension"] = ("" as ExtensionKey);
}
if (!("enabled" in $$source)) {
this["enabled"] = false;
}
if (!("isDefault" in $$source)) {
this["isDefault"] = false;
}
Object.assign(this, $$source);
}
@@ -791,294 +717,258 @@ export class KeyBinding {
}
/**
* KeyBindingCommand 快捷键命令
* KeyBindingKey 快捷键命令
*/
export enum KeyBindingCommand {
export enum KeyBindingKey {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* 搜索扩展相关
* 显示搜索
*/
ShowSearchCommand = "showSearch",
ShowSearchKeyBindingKey = "showSearch",
/**
* 隐藏搜索
*/
HideSearchCommand = "hideSearch",
HideSearchKeyBindingKey = "hideSearch",
/**
* 搜索切换大小写
*/
SearchToggleCaseCommand = "searchToggleCase",
/**
* 搜索切换整词
*/
SearchToggleWordCommand = "searchToggleWord",
/**
* 搜索切换正则
*/
SearchToggleRegexCommand = "searchToggleRegex",
/**
* 显示替换
*/
SearchShowReplaceCommand = "searchShowReplace",
/**
* 替换全部
*/
SearchReplaceAllCommand = "searchReplaceAll",
/**
* 代码块扩展相关
* 块内选择全部
*/
BlockSelectAllCommand = "blockSelectAll",
BlockSelectAllKeyBindingKey = "blockSelectAll",
/**
* 在当前块后添加新块
*/
BlockAddAfterCurrentCommand = "blockAddAfterCurrent",
BlockAddAfterCurrentKeyBindingKey = "blockAddAfterCurrent",
/**
* 在最后添加新块
*/
BlockAddAfterLastCommand = "blockAddAfterLast",
BlockAddAfterLastKeyBindingKey = "blockAddAfterLast",
/**
* 在当前块前添加新块
*/
BlockAddBeforeCurrentCommand = "blockAddBeforeCurrent",
BlockAddBeforeCurrentKeyBindingKey = "blockAddBeforeCurrent",
/**
* 跳转到上一个块
*/
BlockGotoPreviousCommand = "blockGotoPrevious",
BlockGotoPreviousKeyBindingKey = "blockGotoPrevious",
/**
* 跳转到下一个块
*/
BlockGotoNextCommand = "blockGotoNext",
BlockGotoNextKeyBindingKey = "blockGotoNext",
/**
* 选择上一个块
*/
BlockSelectPreviousCommand = "blockSelectPrevious",
BlockSelectPreviousKeyBindingKey = "blockSelectPrevious",
/**
* 选择下一个块
*/
BlockSelectNextCommand = "blockSelectNext",
BlockSelectNextKeyBindingKey = "blockSelectNext",
/**
* 删除当前块
*/
BlockDeleteCommand = "blockDelete",
BlockDeleteKeyBindingKey = "blockDelete",
/**
* 向上移动当前块
*/
BlockMoveUpCommand = "blockMoveUp",
BlockMoveUpKeyBindingKey = "blockMoveUp",
/**
* 向下移动当前块
*/
BlockMoveDownCommand = "blockMoveDown",
BlockMoveDownKeyBindingKey = "blockMoveDown",
/**
* 删除行
*/
BlockDeleteLineCommand = "blockDeleteLine",
BlockDeleteLineKeyBindingKey = "blockDeleteLine",
/**
* 向上移动行
*/
BlockMoveLineUpCommand = "blockMoveLineUp",
BlockMoveLineUpKeyBindingKey = "blockMoveLineUp",
/**
* 向下移动行
*/
BlockMoveLineDownCommand = "blockMoveLineDown",
BlockMoveLineDownKeyBindingKey = "blockMoveLineDown",
/**
* 字符转置
*/
BlockTransposeCharsCommand = "blockTransposeChars",
BlockTransposeCharsKeyBindingKey = "blockTransposeChars",
/**
* 格式化代码块
*/
BlockFormatCommand = "blockFormat",
BlockFormatKeyBindingKey = "blockFormat",
/**
* 复制
*/
BlockCopyCommand = "blockCopy",
BlockCopyKeyBindingKey = "blockCopy",
/**
* 剪切
*/
BlockCutCommand = "blockCut",
BlockCutKeyBindingKey = "blockCut",
/**
* 粘贴
*/
BlockPasteCommand = "blockPaste",
BlockPasteKeyBindingKey = "blockPaste",
/**
* 代码折叠扩展相关
* 折叠代码
*/
FoldCodeCommand = "foldCode",
FoldCodeKeyBindingKey = "foldCode",
/**
* 展开代码
*/
UnfoldCodeCommand = "unfoldCode",
UnfoldCodeKeyBindingKey = "unfoldCode",
/**
* 折叠全部
*/
FoldAllCommand = "foldAll",
FoldAllKeyBindingKey = "foldAll",
/**
* 展开全部
*/
UnfoldAllCommand = "unfoldAll",
UnfoldAllKeyBindingKey = "unfoldAll",
/**
* 通用编辑扩展相关
* 光标按语法左移
*/
CursorSyntaxLeftCommand = "cursorSyntaxLeft",
CursorSyntaxLeftKeyBindingKey = "cursorSyntaxLeft",
/**
* 光标按语法右移
*/
CursorSyntaxRightCommand = "cursorSyntaxRight",
CursorSyntaxRightKeyBindingKey = "cursorSyntaxRight",
/**
* 按语法选择左侧
*/
SelectSyntaxLeftCommand = "selectSyntaxLeft",
SelectSyntaxLeftKeyBindingKey = "selectSyntaxLeft",
/**
* 按语法选择右侧
*/
SelectSyntaxRightCommand = "selectSyntaxRight",
SelectSyntaxRightKeyBindingKey = "selectSyntaxRight",
/**
* 向上复制行
*/
CopyLineUpCommand = "copyLineUp",
CopyLineUpKeyBindingKey = "copyLineUp",
/**
* 向下复制行
*/
CopyLineDownCommand = "copyLineDown",
CopyLineDownKeyBindingKey = "copyLineDown",
/**
* 插入空行
*/
InsertBlankLineCommand = "insertBlankLine",
InsertBlankLineKeyBindingKey = "insertBlankLine",
/**
* 选择行
*/
SelectLineCommand = "selectLine",
SelectLineKeyBindingKey = "selectLine",
/**
* 选择父级语法
*/
SelectParentSyntaxCommand = "selectParentSyntax",
SelectParentSyntaxKeyBindingKey = "selectParentSyntax",
/**
* 减少缩进
*/
IndentLessCommand = "indentLess",
IndentLessKeyBindingKey = "indentLess",
/**
* 增加缩进
*/
IndentMoreCommand = "indentMore",
IndentMoreKeyBindingKey = "indentMore",
/**
* 缩进选择
*/
IndentSelectionCommand = "indentSelection",
IndentSelectionKeyBindingKey = "indentSelection",
/**
* 光标到匹配括号
*/
CursorMatchingBracketCommand = "cursorMatchingBracket",
CursorMatchingBracketKeyBindingKey = "cursorMatchingBracket",
/**
* 切换注释
*/
ToggleCommentCommand = "toggleComment",
ToggleCommentKeyBindingKey = "toggleComment",
/**
* 切换块注释
*/
ToggleBlockCommentCommand = "toggleBlockComment",
ToggleBlockCommentKeyBindingKey = "toggleBlockComment",
/**
* 插入新行并缩进
*/
InsertNewlineAndIndentCommand = "insertNewlineAndIndent",
InsertNewlineAndIndentKeyBindingKey = "insertNewlineAndIndent",
/**
* 向后删除字符
*/
DeleteCharBackwardCommand = "deleteCharBackward",
DeleteCharBackwardKeyBindingKey = "deleteCharBackward",
/**
* 向前删除字符
*/
DeleteCharForwardCommand = "deleteCharForward",
DeleteCharForwardKeyBindingKey = "deleteCharForward",
/**
* 向后删除组
*/
DeleteGroupBackwardCommand = "deleteGroupBackward",
DeleteGroupBackwardKeyBindingKey = "deleteGroupBackward",
/**
* 向前删除组
*/
DeleteGroupForwardCommand = "deleteGroupForward",
DeleteGroupForwardKeyBindingKey = "deleteGroupForward",
/**
* 历史记录扩展相关
* 撤销
*/
HistoryUndoCommand = "historyUndo",
HistoryUndoKeyBindingKey = "historyUndo",
/**
* 重做
*/
HistoryRedoCommand = "historyRedo",
HistoryRedoKeyBindingKey = "historyRedo",
/**
* 撤销选择
*/
HistoryUndoSelectionCommand = "historyUndoSelection",
HistoryUndoSelectionKeyBindingKey = "historyUndoSelection",
/**
* 重做选择
*/
HistoryRedoSelectionCommand = "historyRedoSelection",
/**
* 文本高亮扩展相关
* 切换文本高亮
*/
TextHighlightToggleCommand = "textHighlightToggle",
HistoryRedoSelectionKeyBindingKey = "historyRedoSelection",
};
/**
@@ -1146,370 +1036,6 @@ export enum TabType {
TabTypeTab = "tab",
};
/**
* Theme 主题数据库模型
*/
export class Theme {
"id": number;
"name": string;
"type": ThemeType;
"colors": ThemeColorConfig;
"isDefault": boolean;
"createdAt": string;
"updatedAt": string;
/** 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"] = "";
}
if (!("updatedAt" in $$source)) {
this["updatedAt"] = "";
}
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 主题颜色配置(与前端 ThemeColors 接口保持一致)
*/
export class ThemeColorConfig {
/**
* 主题基本信息
* 主题名称
*/
"name": string;
/**
* 是否为深色主题
*/
"dark": boolean;
/**
* 基础色调
* 主背景色
*/
"background": string;
/**
* 次要背景色(用于代码块交替背景)
*/
"backgroundSecondary": string;
/**
* 面板背景
*/
"surface": string;
/**
* 下拉菜单背景
*/
"dropdownBackground": string;
/**
* 下拉菜单边框
*/
"dropdownBorder": string;
/**
* 文本颜色
* 主文本色
*/
"foreground": string;
/**
* 次要文本色
*/
"foregroundSecondary": string;
/**
* 注释色
*/
"comment": string;
/**
* 语法高亮色 - 核心
* 关键字
*/
"keyword": string;
/**
* 字符串
*/
"string": string;
/**
* 函数名
*/
"function": string;
/**
* 数字
*/
"number": string;
/**
* 操作符
*/
"operator": string;
/**
* 变量
*/
"variable": string;
/**
* 类型
*/
"type": string;
/**
* 语法高亮色 - 扩展
* 常量
*/
"constant": string;
/**
* 存储类型(如 static, const
*/
"storage": string;
/**
* 参数
*/
"parameter": string;
/**
* 类名
*/
"class": string;
/**
* 标题Markdown等
*/
"heading": string;
/**
* 无效内容/错误
*/
"invalid": string;
/**
* 正则表达式
*/
"regexp": string;
/**
* 界面元素
* 光标
*/
"cursor": string;
/**
* 选中背景
*/
"selection": string;
/**
* 失焦选中背景
*/
"selectionBlur": string;
/**
* 当前行高亮
*/
"activeLine": string;
/**
* 行号
*/
"lineNumber": string;
/**
* 活动行号颜色
*/
"activeLineNumber": string;
/**
* 边框和分割线
* 边框色
*/
"borderColor": string;
/**
* 浅色边框
*/
"borderLight": string;
/**
* 搜索和匹配
* 搜索匹配
*/
"searchMatch": string;
/**
* 匹配括号
*/
"matchingBracket": string;
/** Creates a new ThemeColorConfig instance. */
constructor($$source: Partial<ThemeColorConfig> = {}) {
if (!("name" in $$source)) {
this["name"] = "";
}
if (!("dark" in $$source)) {
this["dark"] = false;
}
if (!("background" in $$source)) {
this["background"] = "";
}
if (!("backgroundSecondary" in $$source)) {
this["backgroundSecondary"] = "";
}
if (!("surface" in $$source)) {
this["surface"] = "";
}
if (!("dropdownBackground" in $$source)) {
this["dropdownBackground"] = "";
}
if (!("dropdownBorder" in $$source)) {
this["dropdownBorder"] = "";
}
if (!("foreground" in $$source)) {
this["foreground"] = "";
}
if (!("foregroundSecondary" in $$source)) {
this["foregroundSecondary"] = "";
}
if (!("comment" in $$source)) {
this["comment"] = "";
}
if (!("keyword" in $$source)) {
this["keyword"] = "";
}
if (!("string" in $$source)) {
this["string"] = "";
}
if (!("function" in $$source)) {
this["function"] = "";
}
if (!("number" in $$source)) {
this["number"] = "";
}
if (!("operator" in $$source)) {
this["operator"] = "";
}
if (!("variable" in $$source)) {
this["variable"] = "";
}
if (!("type" in $$source)) {
this["type"] = "";
}
if (!("constant" in $$source)) {
this["constant"] = "";
}
if (!("storage" in $$source)) {
this["storage"] = "";
}
if (!("parameter" in $$source)) {
this["parameter"] = "";
}
if (!("class" in $$source)) {
this["class"] = "";
}
if (!("heading" in $$source)) {
this["heading"] = "";
}
if (!("invalid" in $$source)) {
this["invalid"] = "";
}
if (!("regexp" in $$source)) {
this["regexp"] = "";
}
if (!("cursor" in $$source)) {
this["cursor"] = "";
}
if (!("selection" in $$source)) {
this["selection"] = "";
}
if (!("selectionBlur" in $$source)) {
this["selectionBlur"] = "";
}
if (!("activeLine" in $$source)) {
this["activeLine"] = "";
}
if (!("lineNumber" in $$source)) {
this["lineNumber"] = "";
}
if (!("activeLineNumber" in $$source)) {
this["activeLineNumber"] = "";
}
if (!("borderColor" in $$source)) {
this["borderColor"] = "";
}
if (!("borderLight" in $$source)) {
this["borderLight"] = "";
}
if (!("searchMatch" in $$source)) {
this["searchMatch"] = "";
}
if (!("matchingBracket" in $$source)) {
this["matchingBracket"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new ThemeColorConfig instance from a string or object.
*/
static createFrom($$source: any = {}): ThemeColorConfig {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new ThemeColorConfig($$parsedSource as Partial<ThemeColorConfig>);
}
}
/**
* ThemeType 主题类型枚举
*/
export enum ThemeType {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
ThemeTypeDark = "dark",
ThemeTypeLight = "light",
};
/**
* UpdateSourceType 更新源类型
*/
@@ -1608,8 +1134,8 @@ export class UpdatesConfig {
* Creates a new UpdatesConfig instance from a string or object.
*/
static createFrom($$source: any = {}): UpdatesConfig {
const $$createField6_0 = $$createType10;
const $$createField7_0 = $$createType11;
const $$createField6_0 = $$createType9;
const $$createField7_0 = $$createType10;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("github" in $$parsedSource) {
$$parsedSource["github"] = $$createField6_0($$parsedSource["github"]);
@@ -1636,6 +1162,5 @@ var $$createType6 = (function $$initCreateType6(...args): any {
});
const $$createType7 = $Create.Map($Create.Any, $Create.Any);
const $$createType8 = HotkeyCombo.createFrom;
const $$createType9 = ThemeColorConfig.createFrom;
const $$createType10 = GithubConfig.createFrom;
const $$createType11 = GiteaConfig.createFrom;
const $$createType9 = GithubConfig.createFrom;
const $$createType10 = GiteaConfig.createFrom;

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* BackupService 提供基于Git的备份功能
* BackupService 提供基于Git的备份同步功能
* @module
*/
@@ -18,7 +18,7 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
import * as models$0 from "../models/models.js";
/**
* HandleConfigChange 处理备份配置变更
* HandleConfigChange 处理配置变更
*/
export function HandleConfigChange(config: models$0.GitBackupConfig | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(395287784, config) as any;
@@ -34,15 +34,7 @@ export function Initialize(): Promise<void> & { cancel(): void } {
}
/**
* PushToRemote 推送本地更改到远程仓库
*/
export function PushToRemote(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(262644139) as any;
return $resultPromise;
}
/**
* Reinitialize 重新初始化备份服务,用于响应配置变更
* Reinitialize 重新初始化
*/
export function Reinitialize(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(301562543) as any;
@@ -50,7 +42,7 @@ export function Reinitialize(): Promise<void> & { cancel(): void } {
}
/**
* ServiceShutdown 服务关闭时的清理工作
* ServiceShutdown 服务关闭
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(422131801) as any;
@@ -63,7 +55,7 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
}
/**
* StartAutoBackup 启动自动备份定时器
* StartAutoBackup 启动自动备份
*/
export function StartAutoBackup(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3035755449) as any;
@@ -77,3 +69,11 @@ export function StopAutoBackup(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2641894021) as any;
return $resultPromise;
}
/**
* Sync 执行完整的同步流程:导出 -> commit -> pull -> 解决冲突 -> push -> 导入
*/
export function Sync(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3420383211) as any;
return $resultPromise;
}

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* DatabaseService provides shared database functionality
* DatabaseService 数据库服务
* @module
*/
@@ -15,23 +15,7 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
/**
* OnDataPathChanged handles data path changes
*/
export function OnDataPathChanged(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3652863491) as any;
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 服务关闭
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3907893632) as any;
@@ -39,7 +23,7 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
}
/**
* ServiceStartup initializes the service when the application starts
* ServiceStartup 服务启动
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2067840771, options) as any;

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* DocumentService provides document management functionality
* DocumentService 文档服务
* @module
*/
@@ -15,12 +15,12 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
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";
import * as ent$0 from "../models/ent/models.js";
/**
* CreateDocument creates a new document and returns the created document with ID
* CreateDocument 创建文档
*/
export function CreateDocument(title: string): Promise<models$0.Document | null> & { cancel(): void } {
export function CreateDocument(title: string): Promise<ent$0.Document | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3360680842, title) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
@@ -30,7 +30,7 @@ export function CreateDocument(title: string): Promise<models$0.Document | null>
}
/**
* DeleteDocument marks a document as deleted (default document with ID=1 cannot be deleted)
* DeleteDocument 删除文档
*/
export function DeleteDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(412287269, id) as any;
@@ -38,9 +38,9 @@ export function DeleteDocument(id: number): Promise<void> & { cancel(): void } {
}
/**
* GetDocumentByID gets a document by ID
* GetDocumentByID 根据ID获取文档
*/
export function GetDocumentByID(id: number): Promise<models$0.Document | null> & { cancel(): void } {
export function GetDocumentByID(id: number): Promise<ent$0.Document | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3468193232, id) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
@@ -50,9 +50,9 @@ export function GetDocumentByID(id: number): Promise<models$0.Document | null> &
}
/**
* ListAllDocumentsMeta lists all active (non-deleted) document metadata
* ListAllDocumentsMeta 列出所有文档
*/
export function ListAllDocumentsMeta(): Promise<(models$0.Document | null)[]> & { cancel(): void } {
export function ListAllDocumentsMeta(): Promise<(ent$0.Document | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3073950297) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
@@ -62,19 +62,7 @@ export function ListAllDocumentsMeta(): Promise<(models$0.Document | null)[]> &
}
/**
* ListDeletedDocumentsMeta lists all deleted document metadata
*/
export function ListDeletedDocumentsMeta(): Promise<(models$0.Document | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(490143787) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* LockDocument 锁定文档,防止删除
* LockDocument 锁定文档
*/
export function LockDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1889494473, id) as any;
@@ -82,15 +70,7 @@ export function LockDocument(id: number): Promise<void> & { cancel(): void } {
}
/**
* RestoreDocument restores a deleted document
*/
export function RestoreDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(784200778, id) as any;
return $resultPromise;
}
/**
* ServiceStartup initializes the service when the application starts
* ServiceStartup 服务启动
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1474135487, options) as any;
@@ -106,7 +86,7 @@ export function UnlockDocument(id: number): Promise<void> & { cancel(): void } {
}
/**
* UpdateDocumentContent updates the content of a document
* UpdateDocumentContent 更新文档内容
*/
export function UpdateDocumentContent(id: number, content: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3251897116, id, content) as any;
@@ -114,7 +94,7 @@ export function UpdateDocumentContent(id: number, content: string): Promise<void
}
/**
* UpdateDocumentTitle updates the title of a document
* UpdateDocumentTitle 更新文档标题
*/
export function UpdateDocumentTitle(id: number, title: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2045530459, id, title) as any;
@@ -122,6 +102,6 @@ export function UpdateDocumentTitle(id: number, title: string): Promise<void> &
}
// Private type creation functions
const $$createType0 = models$0.Document.createFrom;
const $$createType0 = ent$0.Document.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1);

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* ExtensionService 扩展管理服务
* ExtensionService 扩展服务
* @module
*/
@@ -16,12 +16,39 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as models$0 from "../models/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as ent$0 from "../models/ent/models.js";
/**
* GetAllExtensions 获取所有扩展配置
* GetAllExtensions 获取所有扩展
*/
export function GetAllExtensions(): Promise<models$0.Extension[]> & { cancel(): void } {
export function GetAllExtensions(): Promise<(ent$0.Extension | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3094292124) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetDefaultExtensions 获取默认扩展配置(用于前端绑定生成)
*/
export function GetDefaultExtensions(): Promise<models$0.Extension[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(4036328166) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType4($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetExtensionByKey 根据Key获取扩展
*/
export function GetExtensionByKey(key: string): Promise<ent$0.Extension | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(2551065776, key) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
@@ -30,23 +57,15 @@ export function GetAllExtensions(): Promise<models$0.Extension[]> & { cancel():
}
/**
* ResetAllExtensionsToDefault 重置所有扩展到默认状态
* ResetExtensionConfig 重置单个扩展到默认状态
*/
export function ResetAllExtensionsToDefault(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(270611949) as any;
export function ResetExtensionConfig(key: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3990780299, key) as any;
return $resultPromise;
}
/**
* ResetExtensionToDefault 重置扩展到默认状态
*/
export function ResetExtensionToDefault(id: models$0.ExtensionID): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(868308101, id) as any;
return $resultPromise;
}
/**
* ServiceStartup 启动时调用
* ServiceStartup 服务启动
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(40324057, options) as any;
@@ -54,21 +73,32 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
}
/**
* UpdateExtensionEnabled 更新扩展启用状态
* SyncExtensions 同步扩展配置
*/
export function UpdateExtensionEnabled(id: models$0.ExtensionID, enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1067300094, id, enabled) as any;
export function SyncExtensions(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(167560004) as any;
return $resultPromise;
}
/**
* UpdateExtensionState 更新扩展状态
* UpdateExtensionConfig 更新扩展配置
*/
export function UpdateExtensionState(id: models$0.ExtensionID, enabled: boolean, config: models$0.ExtensionConfig): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2917946454, id, enabled, config) as any;
export function UpdateExtensionConfig(key: string, config: { [_: string]: any }): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3184142503, key, config) as any;
return $resultPromise;
}
/**
* UpdateExtensionEnabled 更新扩展启用状态
*/
export function UpdateExtensionEnabled(key: string, enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1067300094, key, enabled) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.Extension.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType0 = ent$0.Extension.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1);
const $$createType3 = models$0.Extension.createFrom;
const $$createType4 = $Create.Array($$createType3);

View File

@@ -17,7 +17,6 @@ import * as SystemService from "./systemservice.js";
import * as TestService from "./testservice.js";
import * as ThemeService from "./themeservice.js";
import * as TranslationService from "./translationservice.js";
import * as TrayService from "./trayservice.js";
import * as WindowService from "./windowservice.js";
export {
BackupService,
@@ -36,7 +35,6 @@ export {
TestService,
ThemeService,
TranslationService,
TrayService,
WindowService
};

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* KeyBindingService 快捷键管理服务
* KeyBindingService 快捷键服务
* @module
*/
@@ -16,12 +16,39 @@ import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/applic
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as models$0 from "../models/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as ent$0 from "../models/ent/models.js";
/**
* GetAllKeyBindings 获取所有快捷键配置
* GetAllKeyBindings 获取所有快捷键
*/
export function GetAllKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } {
export function GetAllKeyBindings(): Promise<(ent$0.KeyBinding | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(1633502882) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetDefaultKeyBindings 获取默认快捷键配置
*/
export function GetDefaultKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3843471588) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType4($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetKeyBindingByKey 根据Key获取快捷键
*/
export function GetKeyBindingByKey(key: string): Promise<ent$0.KeyBinding | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(852938650, key) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
@@ -30,13 +57,40 @@ export function GetAllKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel()
}
/**
* ServiceStartup 启动时调用
* ServiceStartup 服务启动
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2057121990, options) as any;
return $resultPromise;
}
/**
* SyncKeyBindings 同步快捷键配置
*/
export function SyncKeyBindings(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1522202638) as any;
return $resultPromise;
}
/**
* UpdateKeyBindingCommand 更新快捷键命令
*/
export function UpdateKeyBindingCommand(key: string, command: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1293670628, key, command) as any;
return $resultPromise;
}
/**
* UpdateKeyBindingEnabled 更新快捷键启用状态
*/
export function UpdateKeyBindingEnabled(key: string, enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(843626124, key, enabled) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.KeyBinding.createFrom;
const $$createType1 = $Create.Array($$createType0);
const $$createType0 = ent$0.KeyBinding.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1);
const $$createType3 = models$0.KeyBinding.createFrom;
const $$createType4 = $Create.Array($$createType3);

View File

@@ -191,15 +191,14 @@ export class MemoryStats {
* MigrationProgress 迁移进度信息
*/
export class MigrationProgress {
"status": MigrationStatus;
/**
* 0-100
*/
"progress": number;
"error"?: string;
/** Creates a new MigrationProgress instance. */
constructor($$source: Partial<MigrationProgress> = {}) {
if (!("status" in $$source)) {
this["status"] = ("" as MigrationStatus);
}
if (!("progress" in $$source)) {
this["progress"] = 0;
}
@@ -216,20 +215,6 @@ export class MigrationProgress {
}
}
/**
* MigrationStatus 迁移状态
*/
export enum MigrationStatus {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
MigrationStatusMigrating = "migrating",
MigrationStatusCompleted = "completed",
MigrationStatusFailed = "failed",
};
/**
* OSInfo 操作系统信息
*/

View File

@@ -15,26 +15,13 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
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";
import * as ent$0 from "../models/ent/models.js";
/**
* GetAllThemes 获取所有主题
* GetThemeByKey 根据Key获取主题
*/
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;
}
/**
* GetThemeByID 根据ID或名称获取主题
* 如果 id > 0按ID查询如果 id = 0按名称查询
*/
export function GetThemeByIdOrName(id: number, ...name: string[]): Promise<models$0.Theme | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(127385338, id, name) as any;
export function GetThemeByKey(key: string): Promise<ent$0.Theme | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(808794256, key) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
@@ -43,23 +30,15 @@ export function GetThemeByIdOrName(id: number, ...name: string[]): Promise<model
}
/**
* ResetTheme 重置主题为预设配置
* ResetTheme 删除主题
*/
export function ResetTheme(id: number, ...name: string[]): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1806334457, id, name) as any;
export function ResetTheme(key: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1806334457, key) as any;
return $resultPromise;
}
/**
* ServiceShutdown 服务关闭
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1676749034) as any;
return $resultPromise;
}
/**
* ServiceStartup 服务启动时初始化
* ServiceStartup 服务启动
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2915959937, options) as any;
@@ -67,14 +46,13 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
}
/**
* UpdateTheme 更新主题
* UpdateTheme 保存或更新主题
*/
export function UpdateTheme(id: number, colors: models$0.ThemeColorConfig): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(70189749, id, colors) as any;
export function UpdateTheme(key: string, colors: { [_: string]: any }): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(70189749, key, colors) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.Theme.createFrom;
const $$createType0 = ent$0.Theme.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1);

View File

@@ -1,59 +0,0 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* TrayService 系统托盘服务
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
/**
* AutoShowHide 自动显示/隐藏主窗口
*/
export function AutoShowHide(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(4044219428) as any;
return $resultPromise;
}
/**
* HandleWindowClose 处理窗口关闭事件
*/
export function HandleWindowClose(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1824247204) as any;
return $resultPromise;
}
/**
* HandleWindowMinimize 处理窗口最小化事件
*/
export function HandleWindowMinimize(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(178686624) as any;
return $resultPromise;
}
/**
* MinimizeButtonClicked 处理标题栏最小化按钮点击
*/
export function MinimizeButtonClicked(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2477618539) as any;
return $resultPromise;
}
/**
* ShouldMinimizeToTray 检查是否应该最小化到托盘
*/
export function ShouldMinimizeToTray(): Promise<boolean> & { cancel(): void } {
let $resultPromise = $Call.ByID(3403884012) as any;
return $resultPromise;
}
/**
* ShowWindow 显示主窗口
*/
export function ShowWindow(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1315913255) as any;
return $resultPromise;
}

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@
"app:generate": "cd .. && wails3 generate bindings -ts"
},
"dependencies": {
"@codemirror/autocomplete": "^6.19.1",
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.0",
"@codemirror/lang-angular": "^0.1.4",
"@codemirror/lang-cpp": "^6.0.3",
@@ -47,17 +47,18 @@
"@codemirror/language": "^6.11.3",
"@codemirror/language-data": "^6.5.2",
"@codemirror/legacy-modes": "^6.5.2",
"@codemirror/lint": "^6.9.1",
"@codemirror/lint": "^6.9.2",
"@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.6",
"@codemirror/view": "^6.38.8",
"@cospaia/prettier-plugin-clojure": "^0.0.2",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.2",
"@lezer/lr": "^1.4.4",
"@prettier/plugin-xml": "^3.4.2",
"@replit/codemirror-lang-svelte": "^6.0.0",
"@toml-tools/lexer": "^1.0.0",
"@toml-tools/parser": "^1.0.0",
"@types/katex": "^0.16.7",
"codemirror": "^6.0.2",
"codemirror-lang-elixir": "^4.0.0",
"colors-named": "^1.0.2",
@@ -65,38 +66,42 @@
"groovy-beautify": "^0.0.17",
"hsl-matcher": "^1.2.4",
"java-parser": "^3.0.1",
"jsox": "^1.2.123",
"katex": "^0.16.25",
"linguist-languages": "^9.1.0",
"marked": "^17.0.1",
"mermaid": "^11.12.1",
"php-parser": "^3.2.5",
"pinia": "^3.0.3",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"prettier": "^3.6.2",
"remarkable": "^2.0.1",
"sass": "^1.93.3",
"vue": "^3.5.22",
"vue-i18n": "^11.1.12",
"prettier": "^3.7.2",
"sass": "^1.94.2",
"vue": "^3.5.25",
"vue-i18n": "^11.2.2",
"vue-pick-colors": "^1.8.0",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@eslint/js": "^9.39.0",
"@eslint/js": "^9.39.1",
"@lezer/generator": "^1.8.0",
"@types/node": "^24.9.2",
"@types/remarkable": "^2.0.8",
"@vitejs/plugin-vue": "^6.0.1",
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.2",
"@wailsio/runtime": "latest",
"cross-env": "^10.1.0",
"eslint": "^9.39.0",
"eslint-plugin-vue": "^10.5.1",
"eslint": "^9.39.1",
"eslint-plugin-vue": "^10.6.2",
"globals": "^16.5.0",
"happy-dom": "^20.0.11",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.2",
"typescript-eslint": "^8.48.0",
"unplugin-vue-components": "^30.0.0",
"vite": "^7.1.12",
"vite": "npm:rolldown-vite@latest",
"vite-plugin-node-polyfills": "^0.24.0",
"vitepress": "^2.0.0-alpha.12",
"vitest": "^4.0.6",
"vitest": "^4.0.14",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.1.2"
"vue-tsc": "^3.1.5"
},
"overrides": {
"vite": "npm:rolldown-vite@latest"
}
}
}

View File

@@ -19,13 +19,13 @@ onBeforeMount(async () => {
// 并行初始化配置、系统信息和快捷键配置
await Promise.all([
configStore.initConfig(),
systemStore.initializeSystemInfo(),
systemStore.initSystemInfo(),
keybindingStore.loadKeyBindings(),
]);
// 初始化语言和主题
await configStore.initializeLanguage();
await themeStore.initializeTheme();
await configStore.initLanguage();
await themeStore.initTheme();
await translationStore.loadTranslators();
// 启动时检查更新

View File

@@ -1,254 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
鸿蒙字体压缩工具
使用 fonttools 库压缩 TTF 字体文件,减小文件大小
"""
import os
import sys
import subprocess
import shutil
from pathlib import Path
from typing import List, Tuple
def check_dependencies():
"""检查必要的依赖是否已安装"""
missing_packages = []
# 检查 fonttools
try:
import fontTools
except ImportError:
missing_packages.append('fonttools')
# 检查 brotli
try:
import brotli
except ImportError:
missing_packages.append('brotli')
# 检查 pyftsubset 命令是否可用
try:
result = subprocess.run(['pyftsubset', '--help'], capture_output=True, text=True)
if result.returncode != 0:
missing_packages.append('fonttools[subset]')
except FileNotFoundError:
if 'fonttools' not in missing_packages:
missing_packages.append('fonttools[subset]')
if missing_packages:
print(f"缺少必要的依赖包: {', '.join(missing_packages)}")
print("请运行以下命令安装:")
print(f"pip install {' '.join(missing_packages)}")
return False
return True
def get_file_size(file_path: str) -> int:
"""获取文件大小(字节)"""
return os.path.getsize(file_path)
def format_file_size(size_bytes: int) -> str:
"""格式化文件大小显示"""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.2f} KB"
else:
return f"{size_bytes / (1024 * 1024):.2f} MB"
def compress_font(input_path: str, output_path: str, compression_level: str = "basic") -> bool:
"""
压缩单个字体文件
Args:
input_path: 输入字体文件路径
output_path: 输出字体文件路径
compression_level: 压缩级别 ("basic", "medium", "aggressive")
Returns:
bool: 压缩是否成功
"""
try:
# 基础压缩参数
base_args = [
"pyftsubset", input_path,
"--output-file=" + output_path,
"--flavor=woff2", # 输出为 WOFF2 格式,压缩率更高
"--with-zopfli", # 使用 Zopfli 算法进一步压缩
]
# 根据压缩级别设置不同的参数
if compression_level == "basic":
# 基础压缩:保留常用字符和功能
args = base_args + [
"--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F,U+2070-209F,U+20A0-20CF", # 基本拉丁字符、标点符号等
"--layout-features=*", # 保留所有布局特性
"--glyph-names", # 保留字形名称
"--symbol-cmap", # 保留符号映射
"--legacy-cmap", # 保留传统字符映射
"--notdef-glyph", # 保留 .notdef 字形
"--recommended-glyphs", # 保留推荐字形
"--name-IDs=*", # 保留所有名称ID
"--name-legacy", # 保留传统名称
]
elif compression_level == "medium":
# 中等压缩:移除一些不常用的功能
args = base_args + [
"--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F", # 减少字符范围
"--layout-features=kern,liga,clig", # 只保留关键布局特性
"--no-glyph-names", # 移除字形名称
"--notdef-glyph",
"--name-IDs=1,2,3,4,5,6", # 只保留基本名称ID
]
else: # aggressive
# 激进压缩:最大程度减小文件大小
args = base_args + [
"--unicodes=U+0020-007F", # 只保留基本ASCII字符
"--no-layout-features", # 移除所有布局特性
"--no-glyph-names", # 移除字形名称
"--no-symbol-cmap", # 移除符号映射
"--no-legacy-cmap", # 移除传统映射
"--notdef-glyph",
"--name-IDs=1,2", # 只保留最基本的名称
"--desubroutinize", # 去子程序化可能减小CFF字体大小
]
# 执行压缩命令
result = subprocess.run(args, capture_output=True, text=True)
if result.returncode == 0:
return True
else:
print(f"压缩失败: {result.stderr}")
return False
except Exception as e:
print(f"压缩过程中出现错误: {str(e)}")
return False
def find_font_files(directory: str) -> List[str]:
"""查找目录中的所有字体文件"""
font_extensions = ['.ttf', '.otf', '.woff', '.woff2']
font_files = []
for root, dirs, files in os.walk(directory):
for file in files:
if any(file.lower().endswith(ext) for ext in font_extensions):
font_files.append(os.path.join(root, file))
return font_files
def compress_fonts_batch(font_directory: str, compression_level: str = "basic"):
"""
批量压缩字体文件
Args:
font_directory: 字体文件目录
compression_level: 压缩级别
"""
if not os.path.exists(font_directory):
print(f"错误: 目录 {font_directory} 不存在")
return
# 查找所有字体文件
font_files = find_font_files(font_directory)
if not font_files:
print("未找到字体文件")
return
print(f"找到 {len(font_files)} 个字体文件")
print(f"压缩级别: {compression_level}")
print(f"压缩后的文件将与源文件放在同一目录,扩展名为 .woff2")
print("-" * 60)
total_original_size = 0
total_compressed_size = 0
successful_compressions = 0
for i, font_file in enumerate(font_files, 1):
print(f"[{i}/{len(font_files)}] 处理: {os.path.basename(font_file)}")
# 获取原始文件大小
original_size = get_file_size(font_file)
total_original_size += original_size
# 生成输出文件名(保持原文件名,只改变扩展名)
file_dir = os.path.dirname(font_file)
base_name = os.path.splitext(os.path.basename(font_file))[0]
output_file = os.path.join(file_dir, f"{base_name}.woff2")
# 压缩字体
if compress_font(font_file, output_file, compression_level):
if os.path.exists(output_file):
compressed_size = get_file_size(output_file)
total_compressed_size += compressed_size
successful_compressions += 1
# 计算压缩率
compression_ratio = (1 - compressed_size / original_size) * 100
print(f" ✓ 成功: {format_file_size(original_size)}{format_file_size(compressed_size)} "
f"(压缩 {compression_ratio:.1f}%)")
else:
print(f" ✗ 失败: 输出文件未生成")
else:
print(f" ✗ 失败: 压缩过程出错")
print()
# 显示总结
print("=" * 60)
print("压缩完成!")
print(f"成功压缩: {successful_compressions}/{len(font_files)} 个文件")
if successful_compressions > 0:
total_compression_ratio = (1 - total_compressed_size / total_original_size) * 100
print(f"总大小: {format_file_size(total_original_size)}{format_file_size(total_compressed_size)}")
print(f"总压缩率: {total_compression_ratio:.1f}%")
print(f"节省空间: {format_file_size(total_original_size - total_compressed_size)}")
def main():
"""主函数"""
print("鸿蒙字体压缩工具")
print("=" * 60)
# 检查依赖
if not check_dependencies():
return
# 获取当前脚本所在目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 设置默认字体目录
font_directory = current_dir
print(f"字体目录: {font_directory}")
# 让用户选择压缩级别
print("\n请选择压缩级别:")
print("1. 基础压缩 (保留大部分功能,适合网页使用)")
print("2. 中等压缩 (平衡文件大小和功能)")
print("3. 激进压缩 (最小文件大小,可能影响显示效果)")
while True:
choice = input("\n请输入选择 (1-3): ").strip()
if choice == "1":
compression_level = "basic"
break
elif choice == "2":
compression_level = "medium"
break
elif choice == "3":
compression_level = "aggressive"
break
else:
print("无效选择,请输入 1、2 或 3")
# 开始批量压缩
compress_fonts_batch(font_directory, compression_level=compression_level)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,179 @@
# 字体压缩工具使用指南
## 📖 简介
`font_compressor.py` 是一个通用的字体压缩工具,可以:
- ✅ 将 TTF、OTF、WOFF 字体文件转换为 WOFF2 格式
- ✅ 支持相对路径和绝对路径
- ✅ 自动生成 CSS 字体定义文件
- ✅ 智能识别字体字重和样式
- ✅ 批量处理整个目录(包括子目录)
## 🚀 前置要求
安装 Python 依赖包:
```bash
pip install fonttools brotli
```
## 📝 使用方法
### 基础用法
```bash
# 进入 fonts 目录
cd frontend/src/assets/fonts
# 交互式模式处理当前目录
python font_compressor.py
# 处理相对路径的 Monocraft 目录
python font_compressor.py Monocraft
# 处理相对路径并指定压缩级别
python font_compressor.py Monocraft -l basic
```
### 生成 CSS 文件
```bash
# 压缩 Monocraft 字体并生成 CSS 文件
python font_compressor.py Monocraft -l basic -c ../styles/monocraft_fonts.css
# 压缩 Hack 字体并生成 CSS
python font_compressor.py Hack -l basic -c ../styles/hack_fonts_new.css
# 压缩 OpenSans 字体并生成 CSS
python font_compressor.py OpenSans -l medium -c ../styles/opensans_fonts.css
```
### 高级用法
```bash
# 使用绝对路径
python font_compressor.py E:\Go_WorkSpace\voidraft\frontend\src\assets\fonts\Monocraft -l basic -c monocraft.css
# 不同压缩级别
python font_compressor.py Monocraft -l basic # 基础压缩,保留所有功能
python font_compressor.py Monocraft -l medium # 中等压缩,平衡大小和功能
python font_compressor.py Monocraft -l aggressive # 激进压缩,最小文件
```
## ⚙️ 命令行参数
| 参数 | 说明 | 示例 |
|------|------|------|
| `directory` | 字体目录(相对/绝对路径) | `Monocraft``/path/to/fonts` |
| `-l, --level` | 压缩级别 (basic/medium/aggressive) | `-l basic` |
| `-c, --css` | CSS 输出文件路径 | `-c monocraft.css` |
| `--version` | 显示版本信息 | `--version` |
| `-h, --help` | 显示帮助信息 | `-h` |
## 📊 压缩级别说明
### basic基础 - 推荐
- 保留大部分字体功能
- 适合网页使用
- 压缩率约 30-40%
### medium中等
- 移除一些不常用的功能
- 平衡文件大小和功能
- 压缩率约 40-50%
### aggressive激进
- 最大程度减小文件大小
- 可能影响高级排版功能
- 压缩率约 50-60%
## 📁 输出结果
### 字体文件
压缩后的 `.woff2` 文件会保存在原文件相同的目录下,例如:
- `Monocraft/ttf/Monocraft-Bold.ttf``Monocraft/ttf/Monocraft-Bold.woff2`
- `Hack/hack-regular.ttf``Hack/hack-regular.woff2`
### CSS 文件
生成的 CSS 文件会包含:
- 自动识别的字体家族名称
- 正确的字重和样式设置
- 使用相对路径的字体引用
- 按字重排序的 `@font-face` 定义
生成的 CSS 示例:
```css
/* 自动生成的字体文件 */
/* 由 font_compressor.py 生成 */
/* Monocraft 字体家族 */
/* Monocraft Light */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
/* Monocraft Bold */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
```
## 🎯 实际使用示例
### 示例 1: 压缩 Monocraft 字体
```bash
cd frontend/src/assets/fonts
python font_compressor.py Monocraft -l basic -c ../styles/monocraft_fonts.css
```
这将:
1. 扫描 `Monocraft/ttf``Monocraft/otf` 目录
2. 将所有字体文件转换为 WOFF2
3.`frontend/src/assets/styles/monocraft_fonts.css` 生成 CSS 文件
### 示例 2: 批量处理多个字体目录
```bash
cd frontend/src/assets/fonts
# 压缩 Monocraft
python font_compressor.py Monocraft -l basic -c ../styles/monocraft_fonts.css
# 压缩 OpenSans
python font_compressor.py OpenSans -l basic -c ../styles/opensans_fonts.css
# 压缩 Hack已有 CSS只需生成新版本对比
python font_compressor.py Hack -l basic -c ../styles/hack_fonts_new.css
```
## 🔍 字体信息自动识别
工具会自动从文件名识别:
- **字重**Thin(100), Light(300), Regular(400), Medium(500), SemiBold(600), Bold(700), Black(900)
- **样式**normal, italic
- **字体家族**:自动去除字重和样式后缀
支持的命名格式:
- `FontName-Bold.ttf`
- `FontName_Bold.otf`
- `FontName-BoldItalic.ttf`
- `FontName_SemiBold_Italic.woff`
## 📞 获取帮助
```bash
python font_compressor.py --help
```

View File

@@ -0,0 +1,494 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
通用字体压缩工具
使用 fonttools 库将字体文件转换为 WOFF2 格式,减小文件大小
支持 TTF、OTF、WOFF 等格式的字体文件
"""
import os
import sys
import subprocess
import shutil
import argparse
import re
from pathlib import Path
from typing import List, Tuple, Dict, Optional
def check_dependencies():
"""检查必要的依赖是否已安装"""
missing_packages = []
# 检查 fonttools
try:
import fontTools
except ImportError:
missing_packages.append('fonttools')
# 检查 brotli
try:
import brotli
except ImportError:
missing_packages.append('brotli')
# 检查 pyftsubset 命令是否可用
try:
result = subprocess.run(['pyftsubset', '--help'], capture_output=True, text=True)
if result.returncode != 0:
missing_packages.append('fonttools[subset]')
except FileNotFoundError:
if 'fonttools' not in missing_packages:
missing_packages.append('fonttools[subset]')
if missing_packages:
print(f"缺少必要的依赖包: {', '.join(missing_packages)}")
print("请运行以下命令安装:")
print(f"pip install {' '.join(missing_packages)}")
return False
return True
def get_file_size(file_path: str) -> int:
"""获取文件大小(字节)"""
return os.path.getsize(file_path)
def format_file_size(size_bytes: int) -> str:
"""格式化文件大小显示"""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.2f} KB"
else:
return f"{size_bytes / (1024 * 1024):.2f} MB"
def compress_font(input_path: str, output_path: str, compression_level: str = "basic") -> bool:
"""
压缩单个字体文件
Args:
input_path: 输入字体文件路径
output_path: 输出字体文件路径
compression_level: 压缩级别 ("basic", "medium", "aggressive")
Returns:
bool: 压缩是否成功
"""
try:
# 基础压缩参数
base_args = [
"pyftsubset", input_path,
"--output-file=" + output_path,
"--flavor=woff2", # 输出为 WOFF2 格式,压缩率更高
"--with-zopfli", # 使用 Zopfli 算法进一步压缩
]
# 根据压缩级别设置不同的参数
if compression_level == "basic":
# 基础压缩:保留常用字符和功能
args = base_args + [
"--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F,U+2070-209F,U+20A0-20CF", # 基本拉丁字符、标点符号等
"--layout-features=*", # 保留所有布局特性
"--glyph-names", # 保留字形名称
"--symbol-cmap", # 保留符号映射
"--legacy-cmap", # 保留传统字符映射
"--notdef-glyph", # 保留 .notdef 字形
"--recommended-glyphs", # 保留推荐字形
"--name-IDs=*", # 保留所有名称ID
"--name-legacy", # 保留传统名称
]
elif compression_level == "medium":
# 中等压缩:移除一些不常用的功能
args = base_args + [
"--unicodes=U+0020-007F,U+00A0-00FF,U+2000-206F", # 减少字符范围
"--layout-features=kern,liga,clig", # 只保留关键布局特性
"--no-glyph-names", # 移除字形名称
"--notdef-glyph",
"--name-IDs=1,2,3,4,5,6", # 只保留基本名称ID
]
else: # aggressive
# 激进压缩:最大程度减小文件大小
args = base_args + [
"--unicodes=U+0020-007F", # 只保留基本ASCII字符
"--no-layout-features", # 移除所有布局特性
"--no-glyph-names", # 移除字形名称
"--no-symbol-cmap", # 移除符号映射
"--no-legacy-cmap", # 移除传统映射
"--notdef-glyph",
"--name-IDs=1,2", # 只保留最基本的名称
"--desubroutinize", # 去子程序化可能减小CFF字体大小
]
# 执行压缩命令
result = subprocess.run(args, capture_output=True, text=True)
if result.returncode == 0:
return True
else:
print(f"压缩失败: {result.stderr}")
return False
except Exception as e:
print(f"压缩过程中出现错误: {str(e)}")
return False
def find_font_files(directory: str, exclude_woff2: bool = False) -> List[str]:
"""查找目录中的所有字体文件"""
if exclude_woff2:
font_extensions = ['.ttf', '.otf', '.woff']
else:
font_extensions = ['.ttf', '.otf', '.woff', '.woff2']
font_files = []
for root, dirs, files in os.walk(directory):
for file in files:
if any(file.lower().endswith(ext) for ext in font_extensions):
font_files.append(os.path.join(root, file))
return font_files
def parse_font_info(filename: str) -> Dict[str, any]:
"""
从字体文件名解析字体信息(字重、样式等)
Args:
filename: 字体文件名(不含路径)
Returns:
包含字体信息的字典
"""
# 移除扩展名
name_without_ext = os.path.splitext(filename)[0]
# 字重映射
weight_mapping = {
'thin': (100, 'Thin'),
'extralight': (200, 'ExtraLight'),
'light': (300, 'Light'),
'regular': (400, 'Regular'),
'normal': (400, 'Regular'),
'medium': (500, 'Medium'),
'semibold': (600, 'SemiBold'),
'bold': (700, 'Bold'),
'extrabold': (800, 'ExtraBold'),
'black': (900, 'Black'),
'heavy': (900, 'Heavy'),
}
# 默认值
font_weight = 400
font_style = 'normal'
weight_name = 'Regular'
# 检查是否为斜体
if re.search(r'italic', name_without_ext, re.IGNORECASE):
font_style = 'italic'
# 检查字重
name_lower = name_without_ext.lower()
for weight_key, (weight_value, weight_label) in weight_mapping.items():
if weight_key in name_lower:
font_weight = weight_value
weight_name = weight_label
break
# 提取字体家族名称(移除字重和样式后缀)
family_name = name_without_ext
for weight_key, (_, weight_label) in weight_mapping.items():
family_name = re.sub(r'[-_]?' + weight_label, '', family_name, flags=re.IGNORECASE)
family_name = re.sub(r'[-_]?italic', '', family_name, flags=re.IGNORECASE)
family_name = family_name.strip('-_')
return {
'family': family_name,
'weight': font_weight,
'style': font_style,
'weight_name': weight_name,
'full_name': name_without_ext
}
def generate_css(font_files: List[str], output_css_path: str, css_base_path: str):
"""
生成CSS字体文件
Args:
font_files: 字体文件路径列表woff2文件
output_css_path: 输出CSS文件路径
css_base_path: CSS文件相对于字体文件的基础路径
"""
# 按字体家族分组
font_groups: Dict[str, List[Dict]] = {}
for font_file in font_files:
if not font_file.endswith('.woff2'):
continue
filename = os.path.basename(font_file)
font_info = parse_font_info(filename)
# 计算相对路径
font_dir = os.path.dirname(font_file)
css_dir = os.path.dirname(output_css_path)
try:
# 计算从CSS文件到字体文件的相对路径
rel_path = os.path.relpath(font_file, css_dir)
# 统一使用正斜杠适用于Web
rel_path = rel_path.replace('\\', '/')
except ValueError:
# 如果在不同驱动器上,使用绝对路径
rel_path = font_file.replace('\\', '/')
font_info['path'] = rel_path
family = font_info['family']
if family not in font_groups:
font_groups[family] = []
font_groups[family].append(font_info)
# 生成CSS内容
css_lines = ['/* 自动生成的字体文件 */', '/* 由 font_compressor.py 生成 */', '']
for family, fonts in sorted(font_groups.items()):
css_lines.append(f'/* {family} 字体家族 */')
css_lines.append('')
# 按字重排序
fonts.sort(key=lambda x: (x['weight'], x['style']))
for font in fonts:
css_lines.append(f"/* {family} {font['weight_name']}{' Italic' if font['style'] == 'italic' else ''} */")
css_lines.append('@font-face {')
css_lines.append(f" font-family: '{family}';")
css_lines.append(f" src: url('{font['path']}') format('woff2');")
css_lines.append(f" font-weight: {font['weight']};")
css_lines.append(f" font-style: {font['style']};")
css_lines.append(' font-display: swap;')
css_lines.append('}')
css_lines.append('')
# 写入CSS文件
with open(output_css_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(css_lines))
print(f"[OK] CSS文件已生成: {output_css_path}")
print(f" 包含 {sum(len(fonts) for fonts in font_groups.values())} 个字体定义")
print(f" 字体家族: {', '.join(sorted(font_groups.keys()))}")
def compress_fonts_batch(font_directory: str, compression_level: str = "basic") -> List[str]:
"""
批量压缩字体文件
Args:
font_directory: 字体文件目录
compression_level: 压缩级别
Returns:
生成的woff2文件路径列表
"""
if not os.path.exists(font_directory):
print(f"错误: 目录 {font_directory} 不存在")
return []
# 查找所有字体文件排除已经是woff2的
font_files = find_font_files(font_directory, exclude_woff2=True)
if not font_files:
print("未找到字体文件")
return []
print(f"找到 {len(font_files)} 个字体文件")
print(f"压缩级别: {compression_level}")
print(f"压缩后的文件将与源文件放在同一目录,扩展名为 .woff2")
print("-" * 60)
total_original_size = 0
total_compressed_size = 0
successful_compressions = 0
generated_woff2_files = []
for i, font_file in enumerate(font_files, 1):
print(f"[{i}/{len(font_files)}] 处理: {os.path.basename(font_file)}")
# 获取原始文件大小
original_size = get_file_size(font_file)
total_original_size += original_size
# 生成输出文件名(保持原文件名,只改变扩展名)
file_dir = os.path.dirname(font_file)
base_name = os.path.splitext(os.path.basename(font_file))[0]
output_file = os.path.join(file_dir, f"{base_name}.woff2")
# 压缩字体
if compress_font(font_file, output_file, compression_level):
if os.path.exists(output_file):
compressed_size = get_file_size(output_file)
total_compressed_size += compressed_size
successful_compressions += 1
generated_woff2_files.append(output_file)
# 计算压缩率
compression_ratio = (1 - compressed_size / original_size) * 100
print(f" [OK] 成功: {format_file_size(original_size)} -> {format_file_size(compressed_size)} "
f"(压缩 {compression_ratio:.1f}%)")
else:
print(f" [失败] 输出文件未生成")
else:
print(f" [失败] 压缩过程出错")
print()
# 显示总结
print("=" * 60)
print("压缩完成!")
print(f"成功压缩: {successful_compressions}/{len(font_files)} 个文件")
if successful_compressions > 0:
total_compression_ratio = (1 - total_compressed_size / total_original_size) * 100
print(f"总大小: {format_file_size(total_original_size)}{format_file_size(total_compressed_size)}")
print(f"总压缩率: {total_compression_ratio:.1f}%")
print(f"节省空间: {format_file_size(total_original_size - total_compressed_size)}")
return generated_woff2_files
def main():
"""主函数"""
# 解析命令行参数
parser = argparse.ArgumentParser(
description='通用字体压缩工具 - 将字体文件转换为 WOFF2 格式并生成CSS',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
使用示例:
%(prog)s # 交互式模式,处理当前目录
%(prog)s Monocraft # 处理相对路径目录
%(prog)s Monocraft -l basic # 使用基础压缩级别
%(prog)s Monocraft -l basic -c monocraft.css # 压缩并生成CSS文件
%(prog)s /path/to/fonts -l medium -c fonts.css # 使用绝对路径
压缩级别说明:
basic - 基础压缩:保留大部分功能,适合网页使用
medium - 中等压缩:平衡文件大小和功能
aggressive - 激进压缩:最小文件大小,可能影响显示效果
CSS生成说明:
使用 -c/--css 选项生成CSS文件自动使用相对路径引用字体文件
'''
)
parser.add_argument(
'directory',
nargs='?',
default=None,
help='字体文件目录路径(支持相对/绝对路径,默认为当前脚本所在目录)'
)
parser.add_argument(
'-l', '--level',
choices=['basic', 'medium', 'aggressive'],
default=None,
help='压缩级别basic基础、medium中等、aggressive激进'
)
parser.add_argument(
'-c', '--css',
default=None,
help='生成CSS文件路径相对于脚本位置或绝对路径'
)
parser.add_argument(
'--version',
action='version',
version='%(prog)s 2.0'
)
args = parser.parse_args()
print("=" * 60)
print("通用字体压缩工具 v2.0")
print("=" * 60)
# 检查依赖
if not check_dependencies():
return
# 获取脚本所在目录
script_dir = os.path.dirname(os.path.abspath(__file__))
# 确定字体目录
if args.directory:
# 支持相对路径和绝对路径
if os.path.isabs(args.directory):
font_directory = args.directory
else:
font_directory = os.path.join(script_dir, args.directory)
font_directory = os.path.abspath(font_directory)
else:
# 默认使用当前脚本所在目录
font_directory = script_dir
# 检查目录是否存在
if not os.path.exists(font_directory):
print(f"\n错误: 目录不存在: {font_directory}")
sys.exit(1)
print(f"\n字体目录: {font_directory}")
# 确定压缩级别
compression_level = args.level
if compression_level is None:
# 交互式选择压缩级别
print("\n请选择压缩级别:")
print("1. 基础压缩 (保留大部分功能,适合网页使用)")
print("2. 中等压缩 (平衡文件大小和功能)")
print("3. 激进压缩 (最小文件大小,可能影响显示效果)")
while True:
choice = input("\n请输入选择 (1-3): ").strip()
if choice == "1":
compression_level = "basic"
break
elif choice == "2":
compression_level = "medium"
break
elif choice == "3":
compression_level = "aggressive"
break
else:
print("无效选择,请输入 1、2 或 3")
# 开始批量压缩
print()
generated_files = compress_fonts_batch(font_directory, compression_level=compression_level)
# 生成CSS文件
if args.css and generated_files:
print()
print("=" * 60)
print("生成CSS文件...")
print("=" * 60)
# 确定CSS输出路径
if os.path.isabs(args.css):
css_path = args.css
else:
css_path = os.path.join(script_dir, args.css)
css_path = os.path.abspath(css_path)
# 确保输出目录存在
css_dir = os.path.dirname(css_path)
if css_dir and not os.path.exists(css_dir):
os.makedirs(css_dir)
# 生成CSS
generate_css(generated_files, css_path, script_dir)
elif args.css and not generated_files:
print("\n警告: 没有成功生成WOFF2文件跳过CSS生成")
print()
print("=" * 60)
print("全部完成!")
print("=" * 60)
if __name__ == "__main__":
main()

View File

@@ -1,7 +1,9 @@
/* 导入所有CSS文件 */
@import 'normalize.css';
@import 'variables.css';
@import "harmony_fonts.css";
@import 'scrollbar.css';
@import 'hack_fonts.css';
@import 'opensans_fonts.css';
@import 'opensans_fonts.css';
@import "monocraft_fonts.css";
@import 'variables.css';
@import 'scrollbar.css';
@import 'styles.css';

View File

@@ -0,0 +1,202 @@
/* 自动生成的字体文件 */
/* 由 font_compressor.py 生成 */
/* Monocraft 字体家族 */
/* Monocraft ExtraLight Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-ExtraLight-Italic.woff2') format('woff2');
font-weight: 200;
font-style: italic;
font-display: swap;
}
/* Monocraft ExtraLight Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-ExtraLight-Italic.woff2') format('woff2');
font-weight: 200;
font-style: italic;
font-display: swap;
}
/* Monocraft ExtraLight */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-ExtraLight.woff2') format('woff2');
font-weight: 200;
font-style: normal;
font-display: swap;
}
/* Monocraft ExtraLight */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-ExtraLight.woff2') format('woff2');
font-weight: 200;
font-style: normal;
font-display: swap;
}
/* Monocraft Light Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-Light-Italic.woff2') format('woff2');
font-weight: 300;
font-style: italic;
font-display: swap;
}
/* Monocraft Light Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Light-Italic.woff2') format('woff2');
font-weight: 300;
font-style: italic;
font-display: swap;
}
/* Monocraft Light */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-Light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
/* Monocraft Light */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
/* Monocraft Regular Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-Italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
/* Monocraft Regular Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
/* Monocraft SemiBold Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-SemiBold-Italic.woff2') format('woff2');
font-weight: 600;
font-style: italic;
font-display: swap;
}
/* Monocraft SemiBold Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-SemiBold-Italic.woff2') format('woff2');
font-weight: 600;
font-style: italic;
font-display: swap;
}
/* Monocraft SemiBold */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-SemiBold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
/* Monocraft SemiBold */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-SemiBold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
/* Monocraft Bold Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-Bold-Italic.woff2') format('woff2');
font-weight: 700;
font-style: italic;
font-display: swap;
}
/* Monocraft Bold Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Bold-Italic.woff2') format('woff2');
font-weight: 700;
font-style: italic;
font-display: swap;
}
/* Monocraft Bold */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* Monocraft Bold */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* Monocraft Black Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-Black-Italic.woff2') format('woff2');
font-weight: 900;
font-style: italic;
font-display: swap;
}
/* Monocraft Black Italic */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Black-Italic.woff2') format('woff2');
font-weight: 900;
font-style: italic;
font-display: swap;
}
/* Monocraft Black */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/otf/Monocraft-Black.woff2') format('woff2');
font-weight: 900;
font-style: normal;
font-display: swap;
}
/* Monocraft Black */
@font-face {
font-family: 'Monocraft';
src: url('../fonts/Monocraft/ttf/Monocraft-Black.woff2') format('woff2');
font-weight: 900;
font-style: normal;
font-display: swap;
}

View File

@@ -0,0 +1,3 @@
body {
background-color: var(--bg-primary);
}

View File

@@ -1,255 +1,266 @@
:root {
/* 编辑器区域 */
--text-primary: #9BB586; /* 内容区域字体颜色 */
/* 深色主题颜色变量 */
--dark-toolbar-bg: #2d2d2d;
--dark-toolbar-border: #404040;
--dark-toolbar-text: #ffffff;
--dark-toolbar-text-secondary: #cccccc;
--dark-toolbar-button-hover: #404040;
--dark-tab-active-line: linear-gradient(90deg, #007acc 0%, #0099ff 100%);
--dark-bg-secondary: #0E1217;
--dark-text-secondary: #a0aec0;
--dark-text-muted: #666;
--dark-border-color: #2d3748;
--dark-settings-bg: #2a2a2a;
--dark-settings-card-bg: #333333;
--dark-settings-text: #ffffff;
--dark-settings-text-secondary: #cccccc;
--dark-settings-border: #444444;
--dark-settings-input-bg: #3a3a3a;
--dark-settings-input-border: #555555;
--dark-settings-hover: #404040;
--dark-scrollbar-track: #2a2a2a;
--dark-scrollbar-thumb: #555555;
--dark-scrollbar-thumb-hover: #666666;
--dark-selection-bg: rgba(181, 206, 168, 0.1);
--dark-selection-text: #b5cea8;
--dark-danger-color: #ff6b6b;
--dark-bg-primary: #1a1a1a;
--dark-bg-hover: #2a2a2a;
--dark-loading-bg-gradient: radial-gradient(#222922, #000500);
--dark-loading-color: #fff;
--dark-loading-glow: 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5);
--dark-loading-done-color: #6f6;
--dark-loading-overlay: linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%);
/* 浅色主题颜色变量 */
--light-toolbar-bg: #f8f9fa;
--light-toolbar-border: #e9ecef;
--light-toolbar-text: #212529;
--light-toolbar-text-secondary: #495057;
--light-toolbar-button-hover: #e9ecef;
--light-tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
--light-bg-secondary: #f7fef7;
--light-text-secondary: #374151;
--light-text-muted: #6b7280;
--light-border-color: #e5e7eb;
--light-settings-bg: #ffffff;
--light-settings-card-bg: #f8f9fa;
--light-settings-text: #212529;
--light-settings-text-secondary: #6c757d;
--light-settings-border: #dee2e6;
--light-settings-input-bg: #ffffff;
--light-settings-input-border: #ced4da;
--light-settings-hover: #e9ecef;
--light-scrollbar-track: #f1f3f4;
--light-scrollbar-thumb: #c1c1c1;
--light-scrollbar-thumb-hover: #a8a8a8;
--light-selection-bg: rgba(59, 130, 246, 0.15);
--light-selection-text: #2563eb;
--light-danger-color: #dc3545;
--light-bg-primary: #ffffff;
--light-bg-hover: #f1f3f4;
--light-loading-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
--light-loading-color: #1a3c1a;
--light-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
--light-loading-done-color: #008800;
--light-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
/* 默认使用深色主题 */
--toolbar-bg: var(--dark-toolbar-bg);
--toolbar-border: var(--dark-toolbar-border);
--toolbar-text: var(--dark-toolbar-text);
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
--toolbar-button-hover: var(--dark-toolbar-button-hover);
--toolbar-separator: var(--dark-toolbar-button-hover);
--tab-active-line: var(--dark-tab-active-line);
--bg-secondary: var(--dark-bg-secondary);
--text-secondary: var(--dark-text-secondary);
--text-muted: var(--dark-text-muted);
--border-color: var(--dark-border-color);
--settings-bg: var(--dark-settings-bg);
--settings-card-bg: var(--dark-settings-card-bg);
--settings-text: var(--dark-settings-text);
--settings-text-secondary: var(--dark-settings-text-secondary);
--settings-border: var(--dark-settings-border);
--settings-input-bg: var(--dark-settings-input-bg);
--settings-input-border: var(--dark-settings-input-border);
--settings-hover: var(--dark-settings-hover);
--scrollbar-track: var(--dark-scrollbar-track);
--scrollbar-thumb: var(--dark-scrollbar-thumb);
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
--selection-bg: var(--dark-selection-bg);
--selection-text: var(--dark-selection-text);
--text-danger: var(--dark-danger-color);
--bg-primary: var(--dark-bg-primary);
--bg-hover: var(--dark-bg-hover);
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
--voidraft-loading-color: var(--dark-loading-color);
--voidraft-loading-glow: var(--dark-loading-glow);
--voidraft-loading-done-color: var(--dark-loading-done-color);
--voidraft-loading-overlay: var(--dark-loading-overlay);
--voidraft-mono-font: "HarmonyOS Sans Mono", monospace;
color-scheme: light dark;
--voidraft-font-mono: "HarmonyOS", SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
}
/* 监听系统深色主题 */
@media (prefers-color-scheme: dark) {
:root[data-theme="auto"] {
--toolbar-bg: var(--dark-toolbar-bg);
--toolbar-border: var(--dark-toolbar-border);
--toolbar-text: var(--dark-toolbar-text);
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
--toolbar-button-hover: var(--dark-toolbar-button-hover);
--toolbar-separator: var(--dark-toolbar-button-hover);
--tab-active-line: var(--dark-tab-active-line);
--bg-secondary: var(--dark-bg-secondary);
--text-secondary: var(--dark-text-secondary);
--text-muted: var(--dark-text-muted);
--border-color: var(--dark-border-color);
--settings-bg: var(--dark-settings-bg);
--settings-card-bg: var(--dark-settings-card-bg);
--settings-text: var(--dark-settings-text);
--settings-text-secondary: var(--dark-settings-text-secondary);
--settings-border: var(--dark-settings-border);
--settings-input-bg: var(--dark-settings-input-bg);
--settings-input-border: var(--dark-settings-input-border);
--settings-hover: var(--dark-settings-hover);
--scrollbar-track: var(--dark-scrollbar-track);
--scrollbar-thumb: var(--dark-scrollbar-thumb);
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
--selection-bg: var(--dark-selection-bg);
--selection-text: var(--dark-selection-text);
--text-danger: var(--dark-danger-color);
--bg-primary: var(--dark-bg-primary);
--bg-hover: var(--dark-bg-hover);
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
--voidraft-loading-color: var(--dark-loading-color);
--voidraft-loading-glow: var(--dark-loading-glow);
--voidraft-loading-done-color: var(--dark-loading-done-color);
--voidraft-loading-overlay: var(--dark-loading-overlay);
}
/* 默认/暗色主题 */
:root,
:root[data-theme="dark"],
:root[data-theme="auto"] {
color-scheme: dark;
--text-primary: #ffffff;
--toolbar-bg: #2d2d2d;
--toolbar-border: #404040;
--toolbar-text: #ffffff;
--toolbar-text-secondary: #cccccc;
--toolbar-button-hover: #404040;
--toolbar-separator: #404040;
--tab-active-line: linear-gradient(90deg, #007acc 0%, #0099ff 100%);
--bg-secondary: #0e1217;
--bg-primary: #1a1a1a;
--bg-hover: #2a2a2a;
--text-secondary: #a0aec0;
--text-muted: #666666;
--text-danger: #ff6b6b;
--border-color: #2d3748;
--settings-bg: #2a2a2a;
--settings-card-bg: #333333;
--settings-text: #ffffff;
--settings-text-secondary: #cccccc;
--settings-border: #444444;
--settings-input-bg: #3a3a3a;
--settings-input-border: #555555;
--settings-hover: #404040;
--scrollbar-track: #2a2a2a;
--scrollbar-thumb: #555555;
--scrollbar-thumb-hover: #666666;
--selection-bg: rgba(181, 206, 168, 0.1);
--selection-text: #b5cea8;
--voidraft-bg-gradient: radial-gradient(#222922, #000500);
--voidraft-loading-color: #ffffff;
--voidraft-loading-glow: 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5);
--voidraft-loading-done-color: #66ff66;
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%);
/* Markdown 代码块样式 - 暗色主题 */
--cm-codeblock-bg: rgba(46, 51, 69, 0.8);
--cm-codeblock-radius: 0.4rem;
/* Markdown 内联代码样式 */
--cm-inline-code-bg: oklch(28% 0.02 255);
/* Markdown 上标/下标样式 */
--cm-superscript-color: inherit;
--cm-subscript-color: inherit;
/* Markdown 高亮样式 */
--cm-highlight-background: rgba(250, 204, 21, 0.35);
/* Markdown 表格样式 - 暗色主题 */
--cm-table-bg: rgba(35, 40, 52, 0.5);
--cm-table-header-bg: rgba(46, 51, 69, 0.7);
--cm-table-border: rgba(75, 85, 99, 0.35);
--cm-table-row-hover: rgba(55, 62, 78, 0.5);
/* Search Panel - Dark Theme */
--search-panel-bg: #252526;
--search-panel-text: #cccccc;
--search-panel-border: #454545;
--search-input-bg: #3c3c3c;
--search-input-text: #cccccc;
--search-input-border: #3c3c3c;
--search-focus-border: #0078d4;
--search-btn-hover: rgba(255, 255, 255, 0.1);
--search-btn-active-bg: rgba(0, 120, 212, 0.4);
--search-btn-active-text: #ffffff;
--search-error-border: #f14c4c;
--search-error-bg: #5a1d1d;
/* Search Match Highlight - Dark Theme (VSCode style) */
--search-match-bg: rgba(250, 220, 81, 0.85);
--search-match-selected-bg: rgba(81, 175, 255, 0.5);
--search-match-selected-border: #74b0f4;
}
/* 监听系统浅色主题 */
/* 色主题 */
:root[data-theme="light"] {
color-scheme: light;
--text-primary: #000000;
--toolbar-bg: #f8f9fa;
--toolbar-border: #e9ecef;
--toolbar-text: #212529;
--toolbar-text-secondary: #495057;
--toolbar-button-hover: #e9ecef;
--toolbar-separator: #e9ecef;
--tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
--bg-secondary: #f7fef7;
--bg-primary: #ffffff;
--bg-hover: #f1f3f4;
--text-secondary: #374151;
--text-muted: #6b7280;
--text-danger: #dc3545;
--border-color: #e5e7eb;
--settings-bg: #ffffff;
--settings-card-bg: #f8f9fa;
--settings-text: #212529;
--settings-text-secondary: #6c757d;
--settings-border: #dee2e6;
--settings-input-bg: #ffffff;
--settings-input-border: #ced4da;
--settings-hover: #e9ecef;
--scrollbar-track: #f1f3f4;
--scrollbar-thumb: #c1c1c1;
--scrollbar-thumb-hover: #a8a8a8;
--selection-bg: rgba(59, 130, 246, 0.15);
--selection-text: #2563eb;
--voidraft-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
--voidraft-loading-color: #1a3c1a;
--voidraft-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
--voidraft-loading-done-color: #008800;
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
/* Markdown 代码块样式 - 亮色主题 */
--cm-codeblock-bg: #f3f3f3;
--cm-codeblock-radius: 0.4rem;
/* Markdown 内联代码样式 */
--cm-inline-code-bg: oklch(92.9% 0.013 255.508);
/* Markdown 上标/下标样式 */
--cm-superscript-color: inherit;
--cm-subscript-color: inherit;
/* Markdown 高亮样式 */
--cm-highlight-background: rgba(253, 224, 71, 0.45);
/* Markdown 表格样式 - 亮色主题 */
--cm-table-bg: oklch(97.5% 0.006 255);
--cm-table-header-bg: oklch(94% 0.01 255);
--cm-table-border: oklch(88% 0.008 255);
--cm-table-row-hover: oklch(95% 0.008 255);
/* Search Panel - Light Theme */
--search-panel-bg: #f3f3f3;
--search-panel-text: #616161;
--search-panel-border: #c8c8c8;
--search-input-bg: #ffffff;
--search-input-text: #616161;
--search-input-border: #cecece;
--search-focus-border: #0078d4;
--search-btn-hover: rgba(0, 0, 0, 0.1);
--search-btn-active-bg: rgba(0, 120, 212, 0.2);
--search-btn-active-text: #0078d4;
--search-error-border: #e51400;
--search-error-bg: #fdeceb;
/* Search Match Highlight - Light Theme (VSCode style) */
--search-match-bg: rgba(250, 220, 81, 0.85);
--search-match-selected-bg: rgba(38, 143, 255, 0.3);
--search-match-selected-border: #268fff;
}
/* 跟随系统的浅色偏好 */
@media (prefers-color-scheme: light) {
:root[data-theme="auto"] {
--toolbar-bg: var(--light-toolbar-bg);
--toolbar-border: var(--light-toolbar-border);
--toolbar-text: var(--light-toolbar-text);
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
--toolbar-button-hover: var(--light-toolbar-button-hover);
--toolbar-separator: var(--light-toolbar-button-hover);
--tab-active-line: var(--light-tab-active-line);
--bg-secondary: var(--light-bg-secondary);
--text-secondary: var(--light-text-secondary);
--text-muted: var(--light-text-muted);
--border-color: var(--light-border-color);
--settings-bg: var(--light-settings-bg);
--settings-card-bg: var(--light-settings-card-bg);
--settings-text: var(--light-settings-text);
--settings-text-secondary: var(--light-settings-text-secondary);
--settings-border: var(--light-settings-border);
--settings-input-bg: var(--light-settings-input-bg);
--settings-input-border: var(--light-settings-input-border);
--settings-hover: var(--light-settings-hover);
--scrollbar-track: var(--light-scrollbar-track);
--scrollbar-thumb: var(--light-scrollbar-thumb);
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
--selection-bg: var(--light-selection-bg);
--selection-text: var(--light-selection-text);
--text-danger: var(--light-danger-color);
--bg-primary: var(--light-bg-primary);
--bg-hover: var(--light-bg-hover);
--voidraft-bg-gradient: var(--light-loading-bg-gradient);
--voidraft-loading-color: var(--light-loading-color);
--voidraft-loading-glow: var(--light-loading-glow);
--voidraft-loading-done-color: var(--light-loading-done-color);
--voidraft-loading-overlay: var(--light-loading-overlay);
color-scheme: light;
--text-primary: #000000;
--toolbar-bg: #f8f9fa;
--toolbar-border: #e9ecef;
--toolbar-text: #212529;
--toolbar-text-secondary: #495057;
--toolbar-button-hover: #e9ecef;
--toolbar-separator: #e9ecef;
--tab-active-line: linear-gradient(90deg, #0066cc 0%, #0088ff 100%);
--bg-secondary: #f7fef7;
--bg-primary: #ffffff;
--bg-hover: #f1f3f4;
--text-secondary: #374151;
--text-muted: #6b7280;
--text-danger: #dc3545;
--border-color: #e5e7eb;
--settings-bg: #ffffff;
--settings-card-bg: #f8f9fa;
--settings-text: #212529;
--settings-text-secondary: #6c757d;
--settings-border: #dee2e6;
--settings-input-bg: #ffffff;
--settings-input-border: #ced4da;
--settings-hover: #e9ecef;
--scrollbar-track: #f1f3f4;
--scrollbar-thumb: #c1c1c1;
--scrollbar-thumb-hover: #a8a8a8;
--selection-bg: rgba(59, 130, 246, 0.15);
--selection-text: #2563eb;
--voidraft-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
--voidraft-loading-color: #1a3c1a;
--voidraft-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
--voidraft-loading-done-color: #008800;
--voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
/* Markdown 代码块样式 - 亮色主题 */
--cm-codeblock-bg: oklch(92.9% 0.013 255.508);
--cm-codeblock-radius: 0.4rem;
/* Markdown 内联代码样式 */
--cm-inline-code-bg: oklch(92.9% 0.013 255.508);
/* Markdown 上标/下标样式 */
--cm-superscript-color: inherit;
--cm-subscript-color: inherit;
/* Markdown 高亮样式 */
--cm-highlight-background: rgba(253, 224, 71, 0.45);
/* Markdown 表格样式 - 亮色主题 */
--cm-table-bg: oklch(97.5% 0.006 255);
--cm-table-header-bg: oklch(94% 0.01 255);
--cm-table-border: oklch(88% 0.008 255);
--cm-table-row-hover: oklch(95% 0.008 255);
/* Search Panel - Light Theme (auto) */
--search-panel-bg: #f3f3f3;
--search-panel-text: #616161;
--search-panel-border: #c8c8c8;
--search-input-bg: #ffffff;
--search-input-text: #616161;
--search-input-border: #cecece;
--search-focus-border: #0078d4;
--search-btn-hover: rgba(0, 0, 0, 0.1);
--search-btn-active-bg: rgba(0, 120, 212, 0.2);
--search-btn-active-text: #0078d4;
--search-error-border: #e51400;
--search-error-bg: #fdeceb;
/* Search Match Highlight - Light Theme auto (VSCode style) */
--search-match-bg: rgba(250, 220, 81, 0.85);
--search-match-selected-bg: rgba(38, 143, 255, 0.3);
--search-match-selected-border: #268fff;
}
}
/* 手动选择浅色主题 */
:root[data-theme="light"] {
--toolbar-bg: var(--light-toolbar-bg);
--toolbar-border: var(--light-toolbar-border);
--toolbar-text: var(--light-toolbar-text);
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
--toolbar-button-hover: var(--light-toolbar-button-hover);
--toolbar-separator: var(--light-toolbar-button-hover);
--tab-active-line: var(--light-tab-active-line);
--bg-secondary: var(--light-bg-secondary);
--text-secondary: var(--light-text-secondary);
--text-muted: var(--light-text-muted);
--border-color: var(--light-border-color);
--settings-bg: var(--light-settings-bg);
--settings-card-bg: var(--light-settings-card-bg);
--settings-text: var(--light-settings-text);
--settings-text-secondary: var(--light-settings-text-secondary);
--settings-border: var(--light-settings-border);
--settings-input-bg: var(--light-settings-input-bg);
--settings-input-border: var(--light-settings-input-border);
--settings-hover: var(--light-settings-hover);
--scrollbar-track: var(--light-scrollbar-track);
--scrollbar-thumb: var(--light-scrollbar-thumb);
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
--selection-bg: var(--light-selection-bg);
--selection-text: var(--light-selection-text);
--text-danger: var(--light-danger-color);
--bg-primary: var(--light-bg-primary);
--bg-hover: var(--light-bg-hover);
--voidraft-bg-gradient: var(--light-loading-bg-gradient);
--voidraft-loading-color: var(--light-loading-color);
--voidraft-loading-glow: var(--light-loading-glow);
--voidraft-loading-done-color: var(--light-loading-done-color);
--voidraft-loading-overlay: var(--light-loading-overlay);
}
/* 手动选择深色主题 */
:root[data-theme="dark"] {
--toolbar-bg: var(--dark-toolbar-bg);
--toolbar-border: var(--dark-toolbar-border);
--toolbar-text: var(--dark-toolbar-text);
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
--toolbar-button-hover: var(--dark-toolbar-button-hover);
--toolbar-separator: var(--dark-toolbar-button-hover);
--tab-active-line: var(--dark-tab-active-line);
--bg-secondary: var(--dark-bg-secondary);
--text-secondary: var(--dark-text-secondary);
--text-muted: var(--dark-text-muted);
--border-color: var(--dark-border-color);
--settings-bg: var(--dark-settings-bg);
--settings-card-bg: var(--dark-settings-card-bg);
--settings-text: var(--dark-settings-text);
--settings-text-secondary: var(--dark-settings-text-secondary);
--settings-border: var(--dark-settings-border);
--settings-input-bg: var(--dark-settings-input-bg);
--settings-input-border: var(--dark-settings-input-border);
--settings-hover: var(--dark-settings-hover);
--scrollbar-track: var(--dark-scrollbar-track);
--scrollbar-thumb: var(--dark-scrollbar-thumb);
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
--selection-bg: var(--dark-selection-bg);
--selection-text: var(--dark-selection-text);
--text-danger: var(--dark-danger-color);
--bg-primary: var(--dark-bg-primary);
--bg-hover: var(--dark-bg-hover);
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
--voidraft-loading-color: var(--dark-loading-color);
--voidraft-loading-glow: var(--dark-loading-glow);
--voidraft-loading-done-color: var(--dark-loading-done-color);
--voidraft-loading-overlay: var(--dark-loading-overlay);
}

View File

@@ -1,43 +1,19 @@
import {
AppConfig,
AppearanceConfig,
EditingConfig,
GeneralConfig,
AuthMethod,
LanguageType,
SystemThemeType,
TabType,
UpdatesConfig,
UpdateSourceType,
GitBackupConfig,
AuthMethod
UpdateSourceType
} from '@/../bindings/voidraft/internal/models/models';
import {FONT_OPTIONS} from './fonts';
// 配置键映射和限制的类型定义
export type GeneralConfigKeyMap = {
readonly [K in keyof GeneralConfig]: string;
};
export type EditingConfigKeyMap = {
readonly [K in keyof EditingConfig]: string;
};
export type AppearanceConfigKeyMap = {
readonly [K in keyof AppearanceConfig]: string;
};
export type UpdatesConfigKeyMap = {
readonly [K in keyof UpdatesConfig]: string;
};
export type BackupConfigKeyMap = {
readonly [K in keyof GitBackupConfig]: string;
};
export type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight';
export type ConfigSection = 'general' | 'editing' | 'appearance' | 'updates' | 'backup';
// 配置键映射
export const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
// 统一配置键映射(平级展开)
export const CONFIG_KEY_MAP = {
// general
alwaysOnTop: 'general.alwaysOnTop',
dataPath: 'general.dataPath',
enableSystemTray: 'general.enableSystemTray',
@@ -47,9 +23,7 @@ export const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
enableWindowSnap: 'general.enableWindowSnap',
enableLoadingAnimation: 'general.enableLoadingAnimation',
enableTabs: 'general.enableTabs',
} as const;
export const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
// editing
fontSize: 'editing.fontSize',
fontFamily: 'editing.fontFamily',
fontWeight: 'editing.fontWeight',
@@ -57,16 +31,12 @@ export const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
enableTabIndent: 'editing.enableTabIndent',
tabSize: 'editing.tabSize',
tabType: 'editing.tabType',
autoSaveDelay: 'editing.autoSaveDelay'
} as const;
export const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
autoSaveDelay: 'editing.autoSaveDelay',
// appearance
language: 'appearance.language',
systemTheme: 'appearance.systemTheme',
currentTheme: 'appearance.currentTheme'
} as const;
export const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
currentTheme: 'appearance.currentTheme',
// updates
version: 'updates.version',
autoUpdate: 'updates.autoUpdate',
primarySource: 'updates.primarySource',
@@ -74,10 +44,8 @@ export const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
backupBeforeUpdate: 'updates.backupBeforeUpdate',
updateTimeout: 'updates.updateTimeout',
github: 'updates.github',
gitea: 'updates.gitea'
} as const;
export const BACKUP_CONFIG_KEY_MAP: BackupConfigKeyMap = {
gitea: 'updates.gitea',
// backup
enabled: 'backup.enabled',
repo_url: 'backup.repo_url',
auth_method: 'backup.auth_method',
@@ -90,6 +58,8 @@ export const BACKUP_CONFIG_KEY_MAP: BackupConfigKeyMap = {
auto_backup: 'backup.auto_backup',
} as const;
export type ConfigKey = keyof typeof CONFIG_KEY_MAP;
// 配置限制
export const CONFIG_LIMITS = {
fontSize: {min: 12, max: 28, default: 13},

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,10 @@ export const FONT_OPTIONS = [
label: 'Open Sans',
value: '"Open Sans"'
},
{
label: 'Monocraft',
value: 'Monocraft'
},
// Common system fonts
{
label: 'Arial',
@@ -46,7 +50,7 @@ export const FONT_OPTIONS = [
label: 'System UI',
value: 'system-ui'
},
// Chinese fonts
{
label: 'Microsoft YaHei',
@@ -56,7 +60,7 @@ export const FONT_OPTIONS = [
label: 'PingFang SC',
value: '"PingFang SC"'
},
// Popular programming fonts
{
label: 'JetBrains Mono',

View File

@@ -1,49 +0,0 @@
/**
* 默认翻译配置
*/
export const DEFAULT_TRANSLATION_CONFIG = {
minSelectionLength: 2,
maxTranslationLength: 5000,
} as const;
/**
* 翻译相关的错误消息
*/
export const TRANSLATION_ERRORS = {
NO_TEXT: 'no text to translate',
TRANSLATION_FAILED: 'translation failed',
} as const;
/**
* 翻译结果接口
*/
export interface TranslationResult {
translatedText: string;
error?: string;
}
/**
* 语言信息接口
*/
export interface LanguageInfo {
Code: string; // 语言代码
Name: string; // 语言名称
}
/**
* 翻译器扩展配置
*/
export interface TranslatorConfig {
/** 最小选择字符数才显示翻译按钮 */
minSelectionLength: number;
/** 最大翻译字符数 */
maxTranslationLength: number;
}
/**
* 翻译图标SVG
*/
export const TRANSLATION_ICON_SVG = `
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<path d="M599.68 485.056h-8l30.592 164.672c20.352-7.04 38.72-17.344 54.912-31.104a271.36 271.36 0 0 1-40.704-64.64l32.256-4.032c8.896 17.664 19.072 33.28 30.592 46.72 23.872-27.968 42.24-65.152 55.04-111.744l-154.688 0.128z m121.92 133.76c18.368 15.36 39.36 26.56 62.848 33.472l14.784 4.416-8.64 30.336-14.72-4.352a205.696 205.696 0 0 1-76.48-41.728c-20.672 17.92-44.928 31.552-71.232 40.064l20.736 110.912H519.424l-9.984 72.512h385.152c18.112 0 32.704-14.144 32.704-31.616V295.424a32.128 32.128 0 0 0-32.704-31.552H550.528l35.2 189.696h79.424v-31.552h61.44v31.552h102.4v31.616h-42.688c-14.272 55.488-35.712 100.096-64.64 133.568zM479.36 791.68H193.472c-36.224 0-65.472-28.288-65.472-63.168V191.168C128 156.16 157.312 128 193.472 128h327.68l20.544 104.32h352.832c36.224 0 65.472 28.224 65.472 63.104v537.408c0 34.944-29.312 63.168-65.472 63.168H468.608l10.688-104.32zM337.472 548.352v-33.28H272.768v-48.896h60.16V433.28h-60.16v-41.728h64.704v-32.896h-102.4v189.632h102.4z m158.272 0V453.76c0-17.216-4.032-30.272-12.16-39.488-8.192-9.152-20.288-13.696-36.032-13.696a55.04 55.04 0 0 0-24.768 5.376 39.04 39.04 0 0 0-17.088 15.936h-1.984l-5.056-18.56h-28.352V548.48h37.12V480c0-17.088 2.304-29.376 6.912-36.736 4.608-7.424 12.16-11.072 22.528-11.072 7.616 0 13.248 2.56 16.64 7.872 3.52 5.248 5.312 13.056 5.312 23.488v84.736h36.928z" fill="currentColor"></path>
</svg>`;

View File

@@ -0,0 +1,329 @@
/**
* DOM Diff 算法单元测试
*/
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
import { morphNode, morphHTML, morphWithKeys } from './domDiff';
describe('DOM Diff Algorithm', () => {
let container: HTMLElement;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
describe('morphNode - 基础功能', () => {
test('应该更新文本节点内容', () => {
const fromNode = document.createTextNode('Hello');
const toNode = document.createTextNode('World');
container.appendChild(fromNode);
morphNode(fromNode, toNode);
expect(fromNode.nodeValue).toBe('World');
});
test('应该保持相同的文本节点不变', () => {
const fromNode = document.createTextNode('Hello');
const toNode = document.createTextNode('Hello');
container.appendChild(fromNode);
const originalNode = fromNode;
morphNode(fromNode, toNode);
expect(fromNode).toBe(originalNode);
expect(fromNode.nodeValue).toBe('Hello');
});
test('应该替换不同类型的节点', () => {
const fromNode = document.createElement('span');
fromNode.textContent = 'Hello';
const toNode = document.createElement('div');
toNode.textContent = 'World';
container.appendChild(fromNode);
morphNode(fromNode, toNode);
expect(container.firstChild?.nodeName).toBe('DIV');
expect(container.firstChild?.textContent).toBe('World');
});
});
describe('morphNode - 属性更新', () => {
test('应该添加新属性', () => {
const fromEl = document.createElement('div');
const toEl = document.createElement('div');
toEl.setAttribute('class', 'test');
toEl.setAttribute('id', 'myid');
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.getAttribute('class')).toBe('test');
expect(fromEl.getAttribute('id')).toBe('myid');
});
test('应该更新已存在的属性', () => {
const fromEl = document.createElement('div');
fromEl.setAttribute('class', 'old');
const toEl = document.createElement('div');
toEl.setAttribute('class', 'new');
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.getAttribute('class')).toBe('new');
});
test('应该删除不存在的属性', () => {
const fromEl = document.createElement('div');
fromEl.setAttribute('class', 'test');
fromEl.setAttribute('id', 'myid');
const toEl = document.createElement('div');
toEl.setAttribute('class', 'test');
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.getAttribute('class')).toBe('test');
expect(fromEl.hasAttribute('id')).toBe(false);
});
});
describe('morphNode - 子节点更新', () => {
test('应该添加新子节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = '<li>1</li><li>2</li>';
const toEl = document.createElement('ul');
toEl.innerHTML = '<li>1</li><li>2</li><li>3</li>';
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.children.length).toBe(3);
expect(fromEl.children[2].textContent).toBe('3');
});
test('应该删除多余的子节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = '<li>1</li><li>2</li><li>3</li>';
const toEl = document.createElement('ul');
toEl.innerHTML = '<li>1</li><li>2</li>';
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.children.length).toBe(2);
expect(fromEl.textContent).toBe('12');
});
test('应该更新子节点内容', () => {
const fromEl = document.createElement('div');
fromEl.innerHTML = '<p>Old</p>';
const toEl = document.createElement('div');
toEl.innerHTML = '<p>New</p>';
container.appendChild(fromEl);
const originalP = fromEl.querySelector('p');
morphNode(fromEl, toEl);
// 应该保持同一个 p 元素,只更新内容
expect(fromEl.querySelector('p')).toBe(originalP);
expect(fromEl.querySelector('p')?.textContent).toBe('New');
});
});
describe('morphHTML - HTML 字符串更新', () => {
test('应该从 HTML 字符串更新元素', () => {
const element = document.createElement('div');
element.innerHTML = '<p>Old</p>';
container.appendChild(element);
morphHTML(element, '<p>New</p>');
expect(element.innerHTML).toBe('<p>New</p>');
});
test('应该处理复杂的 HTML 结构', () => {
const element = document.createElement('div');
element.innerHTML = '<h1>Title</h1><p>Paragraph</p>';
container.appendChild(element);
morphHTML(element, '<h1>New Title</h1><p>New Paragraph</p><span>Extra</span>');
expect(element.children.length).toBe(3);
expect(element.querySelector('h1')?.textContent).toBe('New Title');
expect(element.querySelector('p')?.textContent).toBe('New Paragraph');
expect(element.querySelector('span')?.textContent).toBe('Extra');
});
});
describe('morphWithKeys - 基于 key 的智能 diff', () => {
test('应该保持相同 key 的节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="b">B</li>
<li data-key="c">C</li>
`;
const toEl = document.createElement('ul');
toEl.innerHTML = `
<li data-key="a">A Updated</li>
<li data-key="b">B</li>
<li data-key="c">C</li>
`;
container.appendChild(fromEl);
const originalA = fromEl.querySelector('[data-key="a"]');
morphWithKeys(fromEl, toEl);
expect(fromEl.querySelector('[data-key="a"]')).toBe(originalA);
expect(originalA?.textContent).toBe('A Updated');
});
test('应该重新排序节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="b">B</li>
<li data-key="c">C</li>
`;
const toEl = document.createElement('ul');
toEl.innerHTML = `
<li data-key="c">C</li>
<li data-key="a">A</li>
<li data-key="b">B</li>
`;
container.appendChild(fromEl);
morphWithKeys(fromEl, toEl);
const keys = Array.from(fromEl.children).map(child => child.getAttribute('data-key'));
expect(keys).toEqual(['c', 'a', 'b']);
});
test('应该添加新的 key 节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="b">B</li>
`;
const toEl = document.createElement('ul');
toEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="b">B</li>
<li data-key="c">C</li>
`;
container.appendChild(fromEl);
morphWithKeys(fromEl, toEl);
expect(fromEl.children.length).toBe(3);
expect(fromEl.querySelector('[data-key="c"]')?.textContent).toBe('C');
});
test('应该删除不存在的 key 节点', () => {
const fromEl = document.createElement('ul');
fromEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="b">B</li>
<li data-key="c">C</li>
`;
const toEl = document.createElement('ul');
toEl.innerHTML = `
<li data-key="a">A</li>
<li data-key="c">C</li>
`;
container.appendChild(fromEl);
morphWithKeys(fromEl, toEl);
expect(fromEl.children.length).toBe(2);
expect(fromEl.querySelector('[data-key="b"]')).toBeNull();
});
});
describe('性能测试', () => {
test('应该高效处理大量节点', () => {
const fromEl = document.createElement('ul');
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fromEl.appendChild(li);
}
const toEl = document.createElement('ul');
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Updated Item ${i}`;
toEl.appendChild(li);
}
container.appendChild(fromEl);
const startTime = performance.now();
morphNode(fromEl, toEl);
const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(100); // 应该在 100ms 内完成
expect(fromEl.children.length).toBe(1000);
expect(fromEl.children[0].textContent).toBe('Updated Item 0');
});
});
describe('边界情况', () => {
test('应该处理空节点', () => {
const fromEl = document.createElement('div');
const toEl = document.createElement('div');
container.appendChild(fromEl);
expect(() => morphNode(fromEl, toEl)).not.toThrow();
});
test('应该处理只有文本的节点', () => {
const fromEl = document.createElement('div');
fromEl.textContent = 'Hello';
const toEl = document.createElement('div');
toEl.textContent = 'World';
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.textContent).toBe('World');
});
test('应该处理嵌套的复杂结构', () => {
const fromEl = document.createElement('div');
fromEl.innerHTML = `
<div class="outer">
<div class="inner">
<span>Text</span>
</div>
</div>
`;
const toEl = document.createElement('div');
toEl.innerHTML = `
<div class="outer modified">
<div class="inner">
<span>Updated Text</span>
<strong>New</strong>
</div>
</div>
`;
container.appendChild(fromEl);
morphNode(fromEl, toEl);
expect(fromEl.querySelector('.outer')?.classList.contains('modified')).toBe(true);
expect(fromEl.querySelector('span')?.textContent).toBe('Updated Text');
expect(fromEl.querySelector('strong')?.textContent).toBe('New');
});
});
});

View File

@@ -0,0 +1,180 @@
/**
* 轻量级 DOM Diff 算法实现
* 基于 morphdom 思路,只更新变化的节点,保持未变化的节点不动
*/
/**
* 比较并更新两个 DOM 节点
* @param fromNode 原节点
* @param toNode 目标节点
*/
export function morphNode(fromNode: Node, toNode: Node): void {
// 节点类型不同,直接替换
if (fromNode.nodeType !== toNode.nodeType || fromNode.nodeName !== toNode.nodeName) {
fromNode.parentNode?.replaceChild(toNode.cloneNode(true), fromNode);
return;
}
// 文本节点:比较内容
if (fromNode.nodeType === Node.TEXT_NODE) {
if (fromNode.nodeValue !== toNode.nodeValue) {
fromNode.nodeValue = toNode.nodeValue;
}
return;
}
// 元素节点:更新属性和子节点
if (fromNode.nodeType === Node.ELEMENT_NODE) {
const fromEl = fromNode as Element;
const toEl = toNode as Element;
// 更新属性
morphAttributes(fromEl, toEl);
// 更新子节点
morphChildren(fromEl, toEl);
}
}
/**
* 更新元素属性
*/
function morphAttributes(fromEl: Element, toEl: Element): void {
// 移除旧属性
const fromAttrs = fromEl.attributes;
for (let i = fromAttrs.length - 1; i >= 0; i--) {
const attr = fromAttrs[i];
if (!toEl.hasAttribute(attr.name)) {
fromEl.removeAttribute(attr.name);
}
}
// 添加/更新新属性
const toAttrs = toEl.attributes;
for (let i = 0; i < toAttrs.length; i++) {
const attr = toAttrs[i];
const fromValue = fromEl.getAttribute(attr.name);
if (fromValue !== attr.value) {
fromEl.setAttribute(attr.name, attr.value);
}
}
}
/**
* 更新子节点(核心 diff 算法)
*/
function morphChildren(fromEl: Element, toEl: Element): void {
const fromChildren = Array.from(fromEl.childNodes);
const toChildren = Array.from(toEl.childNodes);
const fromLen = fromChildren.length;
const toLen = toChildren.length;
const minLen = Math.min(fromLen, toLen);
// 1. 更新公共部分
for (let i = 0; i < minLen; i++) {
morphNode(fromChildren[i], toChildren[i]);
}
// 2. 移除多余的旧节点
if (fromLen > toLen) {
for (let i = fromLen - 1; i >= toLen; i--) {
fromEl.removeChild(fromChildren[i]);
}
}
// 3. 添加新节点
if (toLen > fromLen) {
for (let i = fromLen; i < toLen; i++) {
fromEl.appendChild(toChildren[i].cloneNode(true));
}
}
}
/**
* 优化版:使用 key 进行更智能的 diff可选
* 适用于有 data-key 属性的元素
*/
export function morphWithKeys(fromEl: Element, toEl: Element): void {
const toChildren = Array.from(toEl.children) as Element[];
// 构建 from 的 key 映射
const fromKeyMap = new Map<string, Element>();
Array.from(fromEl.children).forEach((child) => {
const key = child.getAttribute('data-key');
if (key) {
fromKeyMap.set(key, child);
}
});
const processedKeys = new Set<string>();
// 按照 toChildren 的顺序处理
toChildren.forEach((toChild, toIndex) => {
const key = toChild.getAttribute('data-key');
if (!key) return;
processedKeys.add(key);
const fromChild = fromKeyMap.get(key);
if (fromChild) {
// 找到对应节点,更新内容
morphNode(fromChild, toChild);
// 确保节点在正确的位置
const currentNode = fromEl.children[toIndex];
if (currentNode !== fromChild) {
// 将 fromChild 移动到正确位置
fromEl.insertBefore(fromChild, currentNode);
}
} else {
// 新节点,插入到正确位置
const currentNode = fromEl.children[toIndex];
fromEl.insertBefore(toChild.cloneNode(true), currentNode || null);
}
});
// 删除不再存在的节点(从后往前删除,避免索引问题)
const childrenToRemove: Element[] = [];
fromKeyMap.forEach((child, key) => {
if (!processedKeys.has(key)) {
childrenToRemove.push(child);
}
});
childrenToRemove.forEach(child => {
fromEl.removeChild(child);
});
}
/**
* 高级 API直接从 HTML 字符串更新元素
*/
export function morphHTML(element: Element, htmlString: string): void {
const tempContainer = document.createElement('div');
tempContainer.innerHTML = htmlString;
// 更新元素的子节点列表
morphChildren(element, tempContainer);
}
/**
* 批量更新(使用 DocumentFragment
*/
export function batchMorph(element: Element, htmlString: string): void {
const tempContainer = document.createElement('div');
tempContainer.innerHTML = htmlString;
const fragment = document.createDocumentFragment();
Array.from(tempContainer.childNodes).forEach(node => {
fragment.appendChild(node);
});
// 清空原内容
while (element.firstChild) {
element.removeChild(element.firstChild);
}
// 批量插入
element.appendChild(fragment);
}

View File

@@ -0,0 +1,65 @@
/**
* Formatter utility functions
*/
export interface DateTimeFormatOptions {
locale?: string;
includeTime?: boolean;
hour12?: boolean;
}
/**
* Format date time string to localized format
* @param dateString - ISO date string or null
* @param options - Formatting options
* @returns Formatted date string or error message
*/
export const formatDateTime = (
dateString: string | null,
options: DateTimeFormatOptions = {}
): string => {
const {
locale = 'en-US',
includeTime = true,
hour12 = false
} = options;
if (!dateString) {
return 'Unknown time';
}
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
return 'Invalid date';
}
const formatOptions: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
};
if (includeTime) {
formatOptions.hour = '2-digit';
formatOptions.minute = '2-digit';
formatOptions.hour12 = hour12;
}
return date.toLocaleString(locale, formatOptions);
} catch {
return 'Time error';
}
};
/**
* Truncate string with ellipsis
* @param str - String to truncate
* @param maxLength - Maximum length before truncation
* @returns Truncated string with ellipsis if needed
*/
export const truncateString = (str: string, maxLength: number): string => {
if (!str) return '';
return str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
};

View File

@@ -0,0 +1,42 @@
/**
* Validation utility functions
*/
/**
* Validate document title
* @param title - The title to validate
* @param maxLength - Maximum allowed length (default: 50)
* @returns Error message if invalid, null if valid
*/
export const validateDocumentTitle = (title: string, maxLength: number = 50): string | null => {
const trimmed = title.trim();
if (!trimmed) {
return 'Document name cannot be empty';
}
if (trimmed.length > maxLength) {
return `Document name cannot exceed ${maxLength} characters`;
}
return null;
};
/**
* Check if a string is empty or whitespace only
* @param value - The string to check
* @returns true if empty or whitespace only
*/
export const isEmpty = (value: string | null | undefined): boolean => {
return !value || value.trim().length === 0;
};
/**
* Check if a string exceeds max length
* @param value - The string to check
* @param maxLength - Maximum allowed length
* @returns true if exceeds max length
*/
export const exceedsMaxLength = (value: string, maxLength: number): boolean => {
return value.trim().length > maxLength;
};

View File

@@ -142,7 +142,7 @@ onBeforeUnmount(() => {
display: flex;
align-items: center;
justify-content: center;
font-family: var(--voidraft-mono-font, monospace),serif;
font-family: Menlo, monospace,serif;
}
.loading-word {
@@ -175,4 +175,4 @@ onBeforeUnmount(() => {
top: 0;
pointer-events: none;
}
</style>
</style>

View File

@@ -1,35 +1,40 @@
<template>
<div
v-if="visible && canClose"
class="tab-context-menu"
:style="{
left: position.x + 'px',
top: position.y + 'px'
}"
@click.stop
v-if="visible && canClose"
v-click-outside="handleClose"
class="tab-context-menu"
:style="{
left: position.x + 'px',
top: position.y + 'px'
}"
@click.stop
>
<div v-if="canClose" class="menu-item" @click="handleMenuClick('close')">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
<span class="menu-text">{{ t('tabs.contextMenu.closeTab') }}</span>
</div>
<div v-if="hasOtherTabs" class="menu-item" @click="handleMenuClick('closeOthers')">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<path d="M9 9l6 6M15 9l-6 6"/>
</svg>
<span class="menu-text">{{ t('tabs.contextMenu.closeOthers') }}</span>
</div>
<div v-if="hasTabsToLeft" class="menu-item" @click="handleMenuClick('closeLeft')">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 18l-6-6 6-6"/>
<path d="M9 18l-6-6 6-6"/>
</svg>
<span class="menu-text">{{ t('tabs.contextMenu.closeLeft') }}</span>
</div>
<div v-if="hasTabsToRight" class="menu-item" @click="handleMenuClick('closeRight')">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 18l6-6-6-6"/>
<path d="M15 18l6-6-6-6"/>
</svg>
@@ -39,9 +44,9 @@
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useTabStore } from '@/stores/tabStore';
import {computed, onMounted, onUnmounted} from 'vue';
import {useI18n} from 'vue-i18n';
import {useTabStore} from '@/stores/tabStore';
interface Props {
visible: boolean;
@@ -54,7 +59,7 @@ const emit = defineEmits<{
close: [];
}>();
const { t } = useI18n();
const {t} = useI18n();
const tabStore = useTabStore();
// 计算属性
@@ -79,6 +84,9 @@ const hasTabsToLeft = computed(() => {
return index > 0;
});
const handleClose = () => {
emit('close');
};
// 处理菜单项点击
const handleMenuClick = (action: string) => {
if (!props.targetDocumentId) return;
@@ -97,34 +105,9 @@ const handleMenuClick = (action: string) => {
tabStore.closeTabsToRight(props.targetDocumentId);
break;
}
emit('close');
handleClose();
};
// 处理外部点击
const handleClickOutside = (_event: MouseEvent) => {
if (props.visible) {
emit('close');
}
};
// 处理ESC键
const handleEscapeKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && props.visible) {
emit('close');
}
};
// 生命周期
onMounted(() => {
document.addEventListener('click', handleClickOutside);
document.addEventListener('keydown', handleEscapeKey);
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
document.removeEventListener('keydown', handleEscapeKey);
});
</script>
<style scoped lang="scss">
@@ -147,15 +130,15 @@ onUnmounted(() => {
padding: 8px 12px;
cursor: pointer;
font-size: 12px;
color: var(--text-muted);
color: var(--text-primary);
transition: all 0.15s ease;
gap: 8px;
&:hover {
background-color: var(--toolbar-button-hover);
color: var(--text-primary);
}
&:active {
background-color: var(--border-color);
}
@@ -165,9 +148,9 @@ onUnmounted(() => {
flex-shrink: 0;
width: 12px;
height: 12px;
color: var(--text-muted);
color: var(--text-primary);
transition: color 0.15s ease;
.menu-item:hover & {
color: var(--text-primary);
}
@@ -178,4 +161,4 @@ onUnmounted(() => {
font-weight: 400;
flex: 1;
}
</style>
</style>

View File

@@ -1,13 +1,15 @@
<template>
<div class="linux-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
<div class="titlebar-content" @dblclick="handleToggleMaximize" @contextmenu.prevent>
<div class="titlebar-icon">
<img src="/appicon.png" alt="voidraft"/>
</div>
<div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
<div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title" :title="fullTitleText">
{{ titleText }}
</div>
<!-- 标签页容器区域 -->
<div class="titlebar-tabs" v-if="tabStore.isTabsEnabled && !isInSettings" style="--wails-draggable:drag">
<TabContainer />
<TabContainer/>
</div>
<!-- 设置页面标题 -->
<div v-if="isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
@@ -26,7 +28,7 @@
<button
class="titlebar-button maximize-button"
@click="toggleMaximize"
@click="handleToggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
>
<svg width="16" height="16" viewBox="0 0 16 16" v-if="!isMaximized">
@@ -55,81 +57,43 @@
import {computed, onMounted, ref} from 'vue';
import {useI18n} from 'vue-i18n';
import {useRoute} from 'vue-router';
import * as runtime from '@wailsio/runtime';
import {useDocumentStore} from '@/stores/documentStore';
import {useTabStore} from '@/stores/tabStore';
import TabContainer from '@/components/tabs/TabContainer.vue';
import {useTabStore} from "@/stores/tabStore";
import {
minimizeWindow,
toggleMaximize,
closeWindow,
getMaximizedState,
generateTitleText,
generateFullTitleText
} from './index';
const tabStore = useTabStore();
const {t} = useI18n();
const route = useRoute();
const isMaximized = ref(false);
const tabStore = useTabStore();
const documentStore = useDocumentStore();
// 判断是否在设置页面
const isMaximized = ref(false);
const isInSettings = computed(() => route.path.startsWith('/settings'));
// 计算标题文本
const titleText = computed(() => {
if (isInSettings.value) {
return `voidraft - ` + t('settings.title');
}
const currentDoc = documentStore.currentDocument;
if (currentDoc) {
// 限制文档标题长度,避免标题栏换行
const maxTitleLength = 30;
const truncatedTitle = currentDoc.title.length > maxTitleLength
? currentDoc.title.substring(0, maxTitleLength) + '...'
: currentDoc.title;
return `voidraft - ${truncatedTitle}`;
}
return 'voidraft';
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return generateTitleText(documentStore.currentDocument?.title);
});
// 计算完整标题文本用于tooltip
const fullTitleText = computed(() => {
if (isInSettings.value) {
return `voidraft - ` + t('settings.title');
}
const currentDoc = documentStore.currentDocument;
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return generateFullTitleText(documentStore.currentDocument?.title);
});
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);
}
const handleToggleMaximize = async () => {
await toggleMaximize();
isMaximized.value = await getMaximizedState();
};
onMounted(async () => {
await checkMaximizedState();
isMaximized.value = await getMaximizedState();
});
</script>
@@ -160,7 +124,7 @@ onMounted(async () => {
font-size: 13px;
font-weight: 500;
cursor: default;
min-width: 0; /* 允许内容收缩 */
min-width: 0;
-webkit-context-menu: none;
-moz-context-menu: none;
@@ -310,4 +274,4 @@ onMounted(async () => {
}
}
}
</style>
</style>

View File

@@ -1,10 +1,10 @@
<template>
<div class="macos-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
<button
class="titlebar-button close-button"
@click="closeWindow"
:title="t('titlebar.close')"
<button
class="titlebar-button close-button"
@click="closeWindow"
:title="t('titlebar.close')"
>
<div class="button-icon">
<svg width="6" height="6" viewBox="0 0 6 6" v-show="showControlIcons">
@@ -12,11 +12,11 @@
</svg>
</div>
</button>
<button
class="titlebar-button minimize-button"
@click="minimizeWindow"
:title="t('titlebar.minimize')"
<button
class="titlebar-button minimize-button"
@click="minimizeWindow"
:title="t('titlebar.minimize')"
>
<div class="button-icon">
<svg width="8" height="1" viewBox="0 0 8 1" v-show="showControlIcons">
@@ -24,11 +24,11 @@
</svg>
</div>
</button>
<button
class="titlebar-button maximize-button"
@click="toggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
<button
class="titlebar-button maximize-button"
@click="handleToggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
>
<div class="button-icon">
<svg width="6" height="6" viewBox="0 0 6 6" v-show="showControlIcons && !isMaximized">
@@ -42,98 +42,61 @@
</div>
</button>
</div>
<!-- 标签页容器区域 -->
<div class="titlebar-tabs" v-if="tabStore.isTabsEnabled && !isInSettings" style="--wails-draggable:drag">
<TabContainer />
<TabContainer/>
</div>
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent v-if="!tabStore.isTabsEnabled || isInSettings">
<div class="titlebar-content" @dblclick="handleToggleMaximize" @contextmenu.prevent
v-if="!tabStore.isTabsEnabled || isInSettings">
<div class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import * as runtime from '@wailsio/runtime';
import { useDocumentStore } from '@/stores/documentStore';
import {computed, onMounted, ref} from 'vue';
import {useI18n} from 'vue-i18n';
import {useRoute} from 'vue-router';
import {useDocumentStore} from '@/stores/documentStore';
import {useTabStore} from '@/stores/tabStore';
import TabContainer from '@/components/tabs/TabContainer.vue';
import { useTabStore } from "@/stores/tabStore";
import {
minimizeWindow,
toggleMaximize,
closeWindow,
getMaximizedState,
generateTitleText,
generateFullTitleText
} from './index';
const tabStore = useTabStore();
const { t } = useI18n();
const {t} = useI18n();
const route = useRoute();
const isMaximized = ref(false);
const showControlIcons = ref(false);
const tabStore = useTabStore();
const documentStore = useDocumentStore();
// 判断是否在设置页面
const isMaximized = ref(false);
const showControlIcons = ref(false);
const isInSettings = computed(() => route.path.startsWith('/settings'));
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);
}
};
// 计算标题文本
const titleText = computed(() => {
if (isInSettings.value) {
return `voidraft - ` + t('settings.title');
}
const currentDoc = documentStore.currentDocument;
if (currentDoc) {
// 限制文档标题长度,避免标题栏换行
const maxTitleLength = 30;
const truncatedTitle = currentDoc.title.length > maxTitleLength
? currentDoc.title.substring(0, maxTitleLength) + '...'
: currentDoc.title;
return `voidraft - ${truncatedTitle}`;
}
return 'voidraft';
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return generateTitleText(documentStore.currentDocument?.title);
});
// 计算完整标题文本用于tooltip
const fullTitleText = computed(() => {
if (isInSettings.value) {
return `voidraft - ` + t('settings.title');
}
const currentDoc = documentStore.currentDocument;
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return generateFullTitleText(documentStore.currentDocument?.title);
});
const handleToggleMaximize = async () => {
await toggleMaximize();
isMaximized.value = await getMaximizedState();
};
onMounted(async () => {
await checkMaximizedState();
isMaximized.value = await getMaximizedState();
});
</script>
@@ -147,11 +110,11 @@ onMounted(async () => {
-webkit-user-select: none;
width: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', system-ui, sans-serif;
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
&:hover {
.titlebar-button {
.button-icon {
@@ -168,7 +131,7 @@ onMounted(async () => {
padding-left: 8px;
gap: 8px;
flex-shrink: 0;
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
@@ -187,7 +150,7 @@ onMounted(async () => {
padding: 0;
margin: 0;
position: relative;
.button-icon {
opacity: 0;
transition: opacity 0.2s ease;
@@ -198,7 +161,7 @@ onMounted(async () => {
height: 100%;
color: rgba(0, 0, 0, 0.7);
}
&:hover .button-icon {
opacity: 1;
}
@@ -206,11 +169,11 @@ onMounted(async () => {
.close-button {
background: #ff5f57;
&:hover {
background: #ff453a;
}
&:active {
background: #d7463f;
}
@@ -218,11 +181,11 @@ onMounted(async () => {
.minimize-button {
background: #ffbd2e;
&:hover {
background: #ffb524;
}
&:active {
background: #e6a220;
}
@@ -230,11 +193,11 @@ onMounted(async () => {
.maximize-button {
background: #28ca42;
&:hover {
background: #1ebe36;
}
&:active {
background: #1ba932;
}
@@ -247,7 +210,7 @@ onMounted(async () => {
flex: 1;
cursor: default;
min-width: 0;
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
@@ -261,34 +224,32 @@ onMounted(async () => {
margin-left: 8px;
margin-right: 8px;
min-width: 0;
overflow: visible; /* 允许TabContainer内部处理滚动 */
/* 确保TabContainer能够正确处理滚动 */
overflow: visible;
:deep(.tab-container) {
width: 100%;
height: 100%;
}
:deep(.tab-bar) {
width: 100%;
height: 100%;
}
:deep(.tab-scroll-wrapper) {
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
/* 确保底部线条能够正确显示 */
:deep(.tab-item) {
position: relative;
&::after {
content: '';
position: absolute;
@@ -319,13 +280,13 @@ onMounted(async () => {
background: var(--toolbar-bg, #2d2d2d);
border-bottom-color: var(--toolbar-border, rgba(255, 255, 255, 0.1));
}
.titlebar-title {
color: var(--toolbar-text, #fff);
}
.titlebar-button .button-icon {
color: rgba(255, 255, 255, 0.8);
}
}
</style>
</style>

View File

@@ -1,13 +1,15 @@
<template>
<div class="windows-titlebar" style="--wails-draggable:drag">
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
<div class="titlebar-content" @dblclick="handleToggleMaximize" @contextmenu.prevent>
<div class="titlebar-icon">
<img src="/appicon.png" alt="voidraft"/>
</div>
<div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
<div v-if="!tabStore.isTabsEnabled && !isInSettings" class="titlebar-title" :title="fullTitleText">
{{ titleText }}
</div>
<!-- 标签页容器区域 -->
<div class="titlebar-tabs" v-if="tabStore.isTabsEnabled && !isInSettings" style="--wails-draggable:drag">
<TabContainer />
<TabContainer/>
</div>
<!-- 设置页面标题 -->
<div v-if="isInSettings" class="titlebar-title" :title="fullTitleText">{{ titleText }}</div>
@@ -24,7 +26,7 @@
<button
class="titlebar-button maximize-button"
@click="toggleMaximize"
@click="handleToggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
>
<span class="titlebar-icon" v-html="maximizeIcon"></span>
@@ -45,84 +47,44 @@
import {computed, onMounted, ref} from 'vue';
import {useI18n} from 'vue-i18n';
import {useRoute} from 'vue-router';
import * as runtime from '@wailsio/runtime';
import {useDocumentStore} from '@/stores/documentStore';
import {useTabStore} from '@/stores/tabStore';
import TabContainer from '@/components/tabs/TabContainer.vue';
import {useTabStore} from "@/stores/tabStore";
import {
minimizeWindow,
toggleMaximize,
closeWindow,
getMaximizedState,
generateTitleText,
generateFullTitleText
} from './index';
const tabStore = useTabStore();
const {t} = useI18n();
const route = useRoute();
const isMaximized = ref(false);
const tabStore = useTabStore();
const documentStore = useDocumentStore();
// 计算属性用于图标,减少重复渲染
const isMaximized = ref(false);
const maximizeIcon = computed(() => isMaximized.value ? '&#xE923;' : '&#xE922;');
// 判断是否在设置页面
const isInSettings = computed(() => route.path.startsWith('/settings'));
// 计算标题文本
const titleText = computed(() => {
if (isInSettings.value) {
return `voidraft - ` + t('settings.title');
}
const currentDoc = documentStore.currentDocument;
if (currentDoc) {
// 限制文档标题长度,避免标题栏换行
const maxTitleLength = 30;
const truncatedTitle = currentDoc.title.length > maxTitleLength
? currentDoc.title.substring(0, maxTitleLength) + '...'
: currentDoc.title;
return `voidraft - ${truncatedTitle}`;
}
return 'voidraft';
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return generateTitleText(documentStore.currentDocument?.title);
});
// 计算完整标题文本用于tooltip
const fullTitleText = computed(() => {
if (isInSettings.value) {
return `voidraft - ` + t('settings.title');
}
const currentDoc = documentStore.currentDocument;
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
if (isInSettings.value) return `voidraft - ${t('settings.title')}`;
return generateFullTitleText(documentStore.currentDocument?.title);
});
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);
}
const handleToggleMaximize = async () => {
await toggleMaximize();
isMaximized.value = await getMaximizedState();
};
onMounted(async () => {
await checkMaximizedState();
isMaximized.value = await getMaximizedState();
});
</script>
@@ -152,7 +114,7 @@ onMounted(async () => {
font-size: 12px;
font-weight: 400;
cursor: default;
min-width: 0; /* 允许内容收缩 */
min-width: 0;
-webkit-context-menu: none;
-moz-context-menu: none;
@@ -178,7 +140,6 @@ onMounted(async () => {
overflow: hidden;
margin-left: 8px;
min-width: 0;
//margin-right: 8px;
}
.titlebar-controls {
@@ -254,4 +215,4 @@ onMounted(async () => {
opacity: 1;
}
}
</style>
</style>

View File

@@ -0,0 +1,60 @@
import * as runtime from '@wailsio/runtime';
/**
* Titlebar utility functions
*/
// Window control functions
export const minimizeWindow = async () => {
try {
await runtime.Window.Minimise();
} catch (error) {
console.error('Failed to minimize window:', error);
}
};
export const toggleMaximize = async () => {
try {
await runtime.Window.ToggleMaximise();
} catch (error) {
console.error('Failed to toggle maximize:', error);
}
};
export const closeWindow = async () => {
try {
await runtime.Window.Close();
} catch (error) {
console.error('Failed to close window:', error);
}
};
export const getMaximizedState = async (): Promise<boolean> => {
try {
return await runtime.Window.IsMaximised();
} catch (error) {
console.error('Failed to check maximized state:', error);
return false;
}
};
/**
* Generate title text with optional truncation
*/
export const generateTitleText = (
title: string | undefined,
maxLength: number = 30
): string => {
if (!title) return 'voidraft';
const truncated = title.length > maxLength
? title.substring(0, maxLength) + '...'
: title;
return `voidraft - ${truncated}`;
};
/**
* Generate full title text (no truncation)
*/
export const generateFullTitleText = (title: string | undefined): string => {
return title ? `voidraft - ${title}` : 'voidraft';
};

View File

@@ -8,7 +8,7 @@ import { getActiveNoteBlock } from '@/views/editor/extensions/codeblock/state';
import { changeCurrentBlockLanguage } from '@/views/editor/extensions/codeblock/commands';
const { t } = useI18n();
const editorStore = readonly(useEditorStore());
const editorStore = useEditorStore();
// 组件状态
const showLanguageMenu = shallowRef(false);

View File

@@ -1,49 +1,63 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { useDocumentStore } from '@/stores/documentStore';
import { useTabStore } from '@/stores/tabStore';
import { useWindowStore } from '@/stores/windowStore';
import { useI18n } from 'vue-i18n';
import type { Document } from '@/../bindings/voidraft/internal/models/models';
import {computed, nextTick, reactive, ref, watch} from 'vue';
import {useDocumentStore} from '@/stores/documentStore';
import {useTabStore} from '@/stores/tabStore';
import {useWindowStore} from '@/stores/windowStore';
import {useI18n} from 'vue-i18n';
import {useConfirm} from '@/composables';
import {validateDocumentTitle} from '@/common/utils/validation';
import {formatDateTime, truncateString} from '@/common/utils/formatter';
import type {Document} from '@/../bindings/voidraft/internal/models/ent/models';
// 类型定义
interface DocumentItem extends Document {
isCreateOption?: boolean;
}
const documentStore = useDocumentStore();
const tabStore = useTabStore();
const windowStore = useWindowStore();
const { t } = useI18n();
const {t} = useI18n();
// DOM 引用
const inputRef = ref<HTMLInputElement>();
const editInputRef = ref<HTMLInputElement>();
// 组件状态
const inputValue = ref('');
const inputRef = ref<HTMLInputElement>();
const editingId = ref<number | null>(null);
const editingTitle = ref('');
const editInputRef = ref<HTMLInputElement>();
const deleteConfirmId = ref<number | null>(null);
const state = reactive({
isLoaded: false,
searchQuery: '',
editing: {
id: null as number | null,
title: ''
}
});
// 常量
const MAX_TITLE_LENGTH = 50;
const DELETE_CONFIRM_TIMEOUT = 3000;
// 计算属性
const currentDocName = computed(() => {
if (!documentStore.currentDocument) return t('toolbar.selectDocument');
const title = documentStore.currentDocument.title;
return title.length > 12 ? title.substring(0, 12) + '...' : title;
return truncateString(documentStore.currentDocument.title || '', 12);
});
const filteredItems = computed(() => {
const filteredItems = computed<DocumentItem[]>(() => {
const docs = documentStore.documentList;
const query = inputValue.value.trim();
const query = state.searchQuery.trim();
if (!query) return docs;
const filtered = docs.filter(doc =>
doc.title.toLowerCase().includes(query.toLowerCase())
(doc.title || '').toLowerCase().includes(query.toLowerCase())
);
// 如果输入的不是已存在文档的完整标题,添加创建选项
const exactMatch = docs.some(doc => doc.title.toLowerCase() === query.toLowerCase());
const exactMatch = docs.some(doc => (doc.title || '').toLowerCase() === query.toLowerCase());
if (!exactMatch && query.length > 0) {
return [
{ id: -1, title: t('toolbar.createDocument') + ` "${query}"`, isCreateOption: true } as any,
{id: -1, title: t('toolbar.createDocument') + ` "${query}"`, isCreateOption: true} as DocumentItem,
...filtered
];
}
@@ -51,53 +65,32 @@ const filteredItems = computed(() => {
return filtered;
});
// 工具函数
const validateTitle = (title: string): string | null => {
if (!title.trim()) return t('toolbar.documentNameRequired');
if (title.trim().length > MAX_TITLE_LENGTH) {
return t('toolbar.documentNameTooLong', { max: MAX_TITLE_LENGTH });
}
return null;
};
const formatTime = (dateString: string | null) => {
if (!dateString) return t('toolbar.unknownTime');
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) return t('toolbar.invalidDate');
const locale = t('locale') === 'zh-CN' ? 'zh-CN' : 'en-US';
return date.toLocaleString(locale, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
} catch {
return t('toolbar.timeError');
}
};
// 核心操作
const openMenu = async () => {
documentStore.openDocumentSelector();
await documentStore.getDocumentMetaList();
documentStore.openDocumentSelector();
state.isLoaded = true;
await nextTick();
inputRef.value?.focus();
};
// 删除确认
const {isConfirming: isDeleting, startConfirm: startDeleteConfirm, reset: resetDeleteConfirm} = useConfirm({
timeout: DELETE_CONFIRM_TIMEOUT
});
const closeMenu = () => {
state.isLoaded = false;
documentStore.closeDocumentSelector();
inputValue.value = '';
editingId.value = null;
editingTitle.value = '';
deleteConfirmId.value = null;
state.searchQuery = '';
state.editing.id = null;
state.editing.title = '';
resetDeleteConfirm();
};
const selectDoc = async (doc: Document) => {
if (doc.id === undefined) return;
// 如果选择的就是当前文档,直接关闭菜单
if (documentStore.currentDocument?.id === doc.id) {
closeMenu();
@@ -121,7 +114,7 @@ const selectDoc = async (doc: Document) => {
const createDoc = async (title: string) => {
const trimmedTitle = title.trim();
const error = validateTitle(trimmedTitle);
const error = validateDocumentTitle(trimmedTitle, MAX_TITLE_LENGTH);
if (error) return;
try {
@@ -132,20 +125,28 @@ const createDoc = async (title: string) => {
}
};
const selectItem = async (item: any) => {
const selectDocItem = async (item: any) => {
if (item.isCreateOption) {
await createDoc(inputValue.value.trim());
await createDoc(state.searchQuery.trim());
} else {
await selectDoc(item);
}
};
// 搜索框回车处理
const handleSearchEnter = () => {
const query = state.searchQuery.trim();
if (query && filteredItems.value.length > 0) {
selectDocItem(filteredItems.value[0]);
}
};
// 编辑操作
const startRename = (doc: Document, event: Event) => {
const renameDoc = (doc: Document, event: Event) => {
event.stopPropagation();
editingId.value = doc.id;
editingTitle.value = doc.title;
deleteConfirmId.value = null;
state.editing.id = doc.id ?? null;
state.editing.title = doc.title || '';
resetDeleteConfirm();
nextTick(() => {
editInputRef.value?.focus();
editInputRef.value?.select();
@@ -153,35 +154,41 @@ const startRename = (doc: Document, event: Event) => {
};
const saveEdit = async () => {
if (!editingId.value || !editingTitle.value.trim()) {
editingId.value = null;
editingTitle.value = '';
if (!state.editing.id || !state.editing.title.trim()) {
state.editing.id = null;
state.editing.title = '';
return;
}
const trimmedTitle = editingTitle.value.trim();
const error = validateTitle(trimmedTitle);
const trimmedTitle = state.editing.title.trim();
const error = validateDocumentTitle(trimmedTitle, MAX_TITLE_LENGTH);
if (error) return;
try {
await documentStore.updateDocumentMetadata(editingId.value, trimmedTitle);
await documentStore.updateDocumentMetadata(state.editing.id, trimmedTitle);
await documentStore.getDocumentMetaList();
// 如果tabs功能开启且该文档有标签页更新标签页标题
if (tabStore.isTabsEnabled && tabStore.hasTab(editingId.value)) {
tabStore.updateTabTitle(editingId.value, trimmedTitle);
if (tabStore.isTabsEnabled && tabStore.hasTab(state.editing.id)) {
tabStore.updateTabTitle(state.editing.id, trimmedTitle);
}
} catch (error) {
console.error('Failed to update document:', error);
} finally {
editingId.value = null;
editingTitle.value = '';
state.editing.id = null;
state.editing.title = '';
}
};
const cancelEdit = () => {
state.editing.id = null;
state.editing.title = '';
};
// 其他操作
const openInNewWindow = async (doc: Document, event: Event) => {
event.stopPropagation();
if (doc.id === undefined) return;
try {
await documentStore.openDocumentInNewWindow(doc.id);
} catch (error) {
@@ -191,13 +198,14 @@ const openInNewWindow = async (doc: Document, event: Event) => {
const handleDelete = async (doc: Document, event: Event) => {
event.stopPropagation();
if (doc.id === undefined) return;
if (deleteConfirmId.value === doc.id) {
if (isDeleting(doc.id)) {
// 确认删除前检查文档是否在其他窗口打开
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
if (hasOpen) {
documentStore.setError(doc.id, t('toolbar.alreadyOpenInNewWindow'));
deleteConfirmId.value = null;
resetDeleteConfirm();
return;
}
@@ -210,228 +218,181 @@ const handleDelete = async (doc: Document, event: Event) => {
if (firstDoc) await selectDoc(firstDoc);
}
}
deleteConfirmId.value = null;
resetDeleteConfirm();
} else {
// 进入确认状态
deleteConfirmId.value = doc.id;
editingId.value = null;
// 3秒后自动取消确认状态
setTimeout(() => {
if (deleteConfirmId.value === doc.id) {
deleteConfirmId.value = null;
}
}, 3000);
startDeleteConfirm(doc.id);
state.editing.id = null;
}
};
// 键盘事件处理
const createKeyHandler = (handlers: Record<string, () => void>) => (event: KeyboardEvent) => {
const handler = handlers[event.key];
if (handler) {
event.preventDefault();
event.stopPropagation();
handler();
}
};
const handleGlobalKeydown = createKeyHandler({
Escape: () => {
if (editingId.value) {
editingId.value = null;
editingTitle.value = '';
} else if (deleteConfirmId.value) {
deleteConfirmId.value = null;
} else {
closeMenu();
}
}
});
const handleInputKeydown = createKeyHandler({
Enter: () => {
const query = inputValue.value.trim();
if (query && filteredItems.value.length > 0) {
selectItem(filteredItems.value[0]);
}
},
Escape: closeMenu
});
const handleEditKeydown = createKeyHandler({
Enter: saveEdit,
Escape: () => {
editingId.value = null;
editingTitle.value = '';
}
});
// 点击外部关闭
const handleClickOutside = (event: Event) => {
const target = event.target as HTMLElement;
if (!target.closest('.document-selector')) {
// 切换菜单
const toggleMenu = () => {
if (documentStore.showDocumentSelector) {
closeMenu();
} else {
openMenu();
}
};
// 生命周期
onMounted(() => {
document.addEventListener('click', handleClickOutside);
document.addEventListener('keydown', handleGlobalKeydown);
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
document.removeEventListener('keydown', handleGlobalKeydown);
});
// 监听菜单状态变化
watch(() => documentStore.showDocumentSelector, (isOpen) => {
if (isOpen) {
if (isOpen && !state.isLoaded) {
openMenu();
}
});
</script>
<template>
<div class="document-selector">
<div class="document-selector" v-click-outside="closeMenu">
<!-- 选择器按钮 -->
<button class="doc-btn" @click="documentStore.toggleDocumentSelector">
<button class="doc-btn" @click="toggleMenu">
<span class="doc-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
<polyline points="14,2 14,8 20,8"></polyline>
</svg>
</span>
<span class="doc-name">{{ currentDocName }}</span>
<span class="arrow" :class="{ open: documentStore.showDocumentSelector }"></span>
<span class="arrow" :class="{ open: state.isLoaded }"></span>
</button>
<!-- 菜单 -->
<div v-if="documentStore.showDocumentSelector" class="doc-menu">
<!-- 输入框 -->
<div class="input-box">
<input
ref="inputRef"
v-model="inputValue"
type="text"
class="main-input"
:placeholder="t('toolbar.searchOrCreateDocument')"
:maxlength="MAX_TITLE_LENGTH"
@keydown="handleInputKeydown"
/>
<svg class="input-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
</div>
<Transition name="slide-up">
<div v-if="state.isLoaded" class="doc-menu">
<!-- 输入框 -->
<div class="input-box">
<input
ref="inputRef"
v-model="state.searchQuery"
type="text"
class="main-input"
:placeholder="t('toolbar.searchOrCreateDocument')"
:maxlength="MAX_TITLE_LENGTH"
@keydown.enter="handleSearchEnter"
@keydown.esc="closeMenu"
/>
<svg class="input-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
</div>
<!-- 项目列表 -->
<div class="item-list">
<div
v-for="item in filteredItems"
:key="item.id"
class="list-item"
:class="{
'active': !item.isCreateOption && documentStore.currentDocument?.id === item.id,
'create-item': item.isCreateOption
}"
@click="selectItem(item)"
>
<!-- 创建选项 -->
<div v-if="item.isCreateOption" class="create-option">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14"></path>
<path d="M12 5v14"></path>
</svg>
<span>{{ item.title }}</span>
</div>
<!-- 文档项 -->
<div v-else class="doc-item-content">
<!-- 普通显示 -->
<div v-if="editingId !== item.id" class="doc-info">
<div class="doc-title">{{ item.title }}</div>
<!-- 根据状态显示错误信息或时间 -->
<div v-if="documentStore.selectorError?.docId === item.id" class="doc-error">
{{ documentStore.selectorError?.message }}
</div>
<div v-else class="doc-date">{{ formatTime(item.updatedAt) }}</div>
<!-- 项目列表 -->
<div class="item-list">
<div
v-for="item in filteredItems"
:key="item.id"
class="list-item"
:class="{
'active': !item.isCreateOption && documentStore.currentDocument?.id === item.id,
'create-item': item.isCreateOption
}"
@click="selectDocItem(item)"
>
<!-- 创建选项 -->
<div v-if="item.isCreateOption" class="create-option">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14"></path>
<path d="M12 5v14"></path>
</svg>
<span>{{ item.title }}</span>
</div>
<!-- 编辑状态 -->
<div v-else class="doc-edit">
<input
:ref="el => editInputRef = el as HTMLInputElement"
v-model="editingTitle"
type="text"
class="edit-input"
:maxlength="MAX_TITLE_LENGTH"
@keydown="handleEditKeydown"
@blur="saveEdit"
@click.stop
/>
</div>
<!-- 文档项 -->
<div v-else class="doc-item-content">
<!-- 普通显示 -->
<div v-if="state.editing.id !== item.id" class="doc-info">
<div class="doc-title">{{ item.title }}</div>
<!-- 根据状态显示错误信息或时间 -->
<div v-if="documentStore.selectorError?.docId === item.id" class="doc-error">
{{ documentStore.selectorError?.message }}
</div>
<div v-else class="doc-date">{{ formatDateTime(item.updated_at) }}</div>
</div>
<!-- 操作按钮 -->
<div v-if="editingId !== item.id" class="doc-actions">
<!-- 只有非当前文档才显示在新窗口打开按钮 -->
<button
v-if="documentStore.currentDocument?.id !== item.id"
class="action-btn"
@click="openInNewWindow(item, $event)"
:title="t('toolbar.openInNewWindow')"
>
<svg width="12" height="12" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
fill="currentColor">
<path
d="M172.8 1017.6c-89.6 0-166.4-70.4-166.4-166.4V441.6c0-89.6 70.4-166.4 166.4-166.4h416c89.6 0 166.4 70.4 166.4 166.4v416c0 89.6-70.4 166.4-166.4 166.4l-416-6.4z m0-659.2c-51.2 0-89.6 38.4-89.6 89.6v416c0 51.2 38.4 89.6 89.6 89.6h416c51.2 0 89.6-38.4 89.6-89.6V441.6c0-51.2-38.4-89.6-89.6-89.6H172.8z"></path>
<path
d="M851.2 19.2H435.2C339.2 19.2 268.8 96 268.8 185.6v25.6h70.4v-25.6c0-51.2 38.4-89.6 89.6-89.6h409.6c51.2 0 89.6 38.4 89.6 89.6v409.6c0 51.2-38.4 89.6-89.6 89.6h-38.4V768h51.2c96 0 166.4-76.8 166.4-166.4V185.6c0-96-76.8-166.4-166.4-166.4z"></path>
</svg>
</button>
<button class="action-btn" @click="startRename(item, $event)" :title="t('toolbar.rename')">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path>
</svg>
</button>
<button
v-if="documentStore.documentList.length > 1 && item.id !== 1"
class="action-btn delete-btn"
:class="{ 'delete-confirm': deleteConfirmId === item.id }"
@click="handleDelete(item, $event)"
:title="deleteConfirmId === item.id ? t('toolbar.confirmDelete') : t('toolbar.delete')"
>
<svg v-if="deleteConfirmId !== item.id" xmlns="http://www.w3.org/2000/svg" width="12" height="12"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<polyline points="3,6 5,6 21,6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
<span v-else class="confirm-text">{{ t('toolbar.confirm') }}</span>
</button>
<!-- 编辑状态 -->
<div v-else class="doc-edit">
<input
:ref="el => editInputRef = el as HTMLInputElement"
v-model="state.editing.title"
type="text"
class="edit-input"
:maxlength="MAX_TITLE_LENGTH"
@keydown.enter="saveEdit"
@keydown.esc="cancelEdit"
@blur="saveEdit"
@click.stop
/>
</div>
<!-- 操作按钮 -->
<div v-if="state.editing.id !== item.id" class="doc-actions">
<!-- 只有非当前文档才显示在新窗口打开按钮 -->
<button
v-if="documentStore.currentDocument?.id !== item.id"
class="action-btn"
@click="openInNewWindow(item, $event)"
:title="t('toolbar.openInNewWindow')"
>
<svg width="12" height="12" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
fill="currentColor">
<path
d="M172.8 1017.6c-89.6 0-166.4-70.4-166.4-166.4V441.6c0-89.6 70.4-166.4 166.4-166.4h416c89.6 0 166.4 70.4 166.4 166.4v416c0 89.6-70.4 166.4-166.4 166.4l-416-6.4z m0-659.2c-51.2 0-89.6 38.4-89.6 89.6v416c0 51.2 38.4 89.6 89.6 89.6h416c51.2 0 89.6-38.4 89.6-89.6V441.6c0-51.2-38.4-89.6-89.6-89.6H172.8z"></path>
<path
d="M851.2 19.2H435.2C339.2 19.2 268.8 96 268.8 185.6v25.6h70.4v-25.6c0-51.2 38.4-89.6 89.6-89.6h409.6c51.2 0 89.6 38.4 89.6 89.6v409.6c0 51.2-38.4 89.6-89.6 89.6h-38.4V768h51.2c96 0 166.4-76.8 166.4-166.4V185.6c0-96-76.8-166.4-166.4-166.4z"></path>
</svg>
</button>
<button class="action-btn" @click="renameDoc(item, $event)" :title="t('toolbar.rename')">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path>
</svg>
</button>
<button
v-if="documentStore.documentList.length > 1 && item.id !== 1"
class="action-btn delete-btn"
:class="{ 'delete-confirm': isDeleting(item.id!) }"
@click="handleDelete(item, $event)"
:title="isDeleting(item.id!) ? t('toolbar.confirmDelete') : t('toolbar.delete')"
>
<svg v-if="!isDeleting(item.id!)" xmlns="http://www.w3.org/2000/svg" width="12" height="12"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<polyline points="3,6 5,6 21,6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
<span v-else class="confirm-text">{{ t('toolbar.confirm') }}</span>
</button>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="filteredItems.length === 0" class="empty">
{{ t('toolbar.noDocumentFound') }}
</div>
<!-- 加载状态 -->
<div v-if="documentStore.isLoading" class="loading">
{{ t('toolbar.loading') }}
<!-- 空状态 -->
<div v-if="filteredItems.length === 0" class="empty">
{{ t('toolbar.noDocumentFound') }}
</div>
</div>
</div>
</div>
</Transition>
</div>
</template>
<style scoped lang="scss">
.slide-up-enter-active,
.slide-up-leave-active {
transition: opacity 0.15s ease, transform 0.15s ease;
}
.slide-up-enter-from,
.slide-up-leave-to {
opacity: 0;
transform: translateY(8px);
}
.document-selector {
position: relative;
@@ -483,8 +444,8 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
border: 1px solid var(--border-color);
border-radius: 3px;
margin-bottom: 4px;
width: 260px;
max-height: calc(100vh - 40px); // 限制最大高度留出titlebar空间(32px)和一些边距
width: 300px;
max-height: calc(100vh - 40px);
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
overflow: hidden;
@@ -527,7 +488,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
}
.item-list {
max-height: calc(100vh - 100px); // 为输入框和边距预留空间
max-height: calc(100vh - 100px);
overflow-y: auto;
flex: 1;
@@ -594,7 +555,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
color: var(--text-muted);
opacity: 0.6;
}
.doc-error {
font-size: 10px;
color: var(--text-danger);
@@ -669,7 +630,7 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
}
}
.empty, .loading {
.empty {
padding: 16px 8px;
text-align: center;
font-size: 11px;
@@ -680,9 +641,17 @@ watch(() => documentStore.showDocumentSelector, (isOpen) => {
}
@keyframes fadeInOut {
0% { opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { opacity: 0; }
0% {
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
opacity: 0;
}
}
</style>
</style>

View File

@@ -14,11 +14,11 @@ import {getLanguage} from '@/views/editor/extensions/codeblock/lang-parser/langu
import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode';
import {createDebounce} from '@/common/utils/debounce';
const editorStore = readonly(useEditorStore());
const configStore = readonly(useConfigStore());
const updateStore = readonly(useUpdateStore());
const windowStore = readonly(useWindowStore());
const systemStore = readonly(useSystemStore());
const editorStore = useEditorStore();
const configStore = useConfigStore();
const updateStore = useUpdateStore();
const windowStore = useWindowStore();
const systemStore = useSystemStore();
const {t} = useI18n();
const router = useRouter();
@@ -33,6 +33,7 @@ const isCurrentWindowOnTop = computed(() => {
return config.value.general.alwaysOnTop || systemStore.isWindowOnTop;
});
// 切换窗口置顶状态
const toggleAlwaysOnTop = async () => {
const currentlyOnTop = isCurrentWindowOnTop.value;
@@ -60,9 +61,10 @@ const formatCurrentBlock = () => {
formatBlockContent(editorStore.editorView);
};
// 格式化按钮状态更新 - 使用更高效的检查逻辑
const updateFormatButtonState = () => {
const view = editorStore.editorView;
// 统一更新按钮状态
const updateButtonStates = () => {
const view: any = editorStore.editorView;
if (!view) {
canFormatCurrentBlock.value = false;
return;
@@ -78,17 +80,19 @@ const updateFormatButtonState = () => {
return;
}
const language = getLanguage(activeBlock.language.name as any);
const languageName = activeBlock.language.name;
const language = getLanguage(languageName as any);
canFormatCurrentBlock.value = Boolean(language?.prettier);
} catch (error) {
console.warn('Error checking format capability:', error);
console.warn('Error checking block capabilities:', error);
canFormatCurrentBlock.value = false;
}
};
// 创建带1s防抖的更新函数
const { debouncedFn: debouncedUpdateFormat, cancel: cancelDebounce } = createDebounce(
updateFormatButtonState,
const { debouncedFn: debouncedUpdateButtonStates, cancel: cancelDebounce } = createDebounce(
updateButtonStates,
{ delay: 1000 }
);
@@ -102,9 +106,9 @@ const setupEditorListeners = (view: any) => {
// 使用对象缓存事件处理器,避免重复创建
const eventHandlers = {
click: updateFormatButtonState,
keyup: debouncedUpdateFormat,
focus: updateFormatButtonState
click: updateButtonStates,
keyup: debouncedUpdateButtonStates,
focus: updateButtonStates
} as const;
const events = Object.entries(eventHandlers).map(([type, handler]) => ({
@@ -131,7 +135,7 @@ watch(
if (newView) {
// 初始更新状态
updateFormatButtonState();
updateButtonStates();
// 设置新监听器
cleanupListeners = setupEditorListeners(newView);
} else {
@@ -145,8 +149,8 @@ watch(
// 组件生命周期
onMounted(async () => {
isLoaded.value = true;
// 首次更新格式化状态
updateFormatButtonState();
// 首次更新按钮状态
updateButtonStates();
await systemStore.setWindowOnTop(isCurrentWindowOnTop.value);
});
@@ -512,6 +516,42 @@ const statsData = computed(() => ({
}
}
.preview-button {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
padding: 2px;
border-radius: 3px;
transition: all 0.2s ease;
&:hover {
background-color: var(--border-color);
opacity: 0.8;
}
&.active {
background-color: rgba(100, 149, 237, 0.2);
svg {
stroke: #6495ed;
}
}
svg {
width: 14px;
height: 14px;
stroke: var(--text-muted);
transition: stroke 0.2s ease;
}
&:hover svg {
stroke: var(--text-secondary);
}
}
.settings-btn {
background: none;
border: none;

View File

@@ -0,0 +1,5 @@
export { useConfirm } from './useConfirm';
export type { UseConfirmOptions } from './useConfirm';
export { usePolling } from './usePolling';
export type { UsePollingOptions, UsePollingReturn } from './usePolling';

View File

@@ -0,0 +1,174 @@
import { ref, readonly, onUnmounted, type Ref, type DeepReadonly } from 'vue';
export interface UseConfirmOptions<T extends string | number = string | number> {
/** Auto cancel timeout in ms (default: 3000, set 0 to disable) */
timeout?: number;
/** Callback when confirmed */
onConfirm?: (id: T) => void | Promise<void>;
/** Callback when cancelled (timeout or manual) */
onCancel?: (id: T) => void;
}
export interface UseConfirmReturn<T extends string | number = string | number> {
/** Current confirming id (readonly) */
confirmId: DeepReadonly<Ref<T | null>>;
/** Whether confirm action is executing */
isPending: DeepReadonly<Ref<boolean>>;
/** Check if a specific id is in confirming state */
isConfirming: (id: T) => boolean;
/** Start confirming state (with auto timeout) */
startConfirm: (id: T) => void;
/** Request confirmation (toggle between request and execute) */
requestConfirm: (id: T) => Promise<boolean>;
/** Manually confirm current id */
confirm: () => Promise<void>;
/** Cancel confirmation */
cancel: () => void;
/** Reset without triggering callbacks */
reset: () => void;
}
/**
* Composable for handling confirm actions (e.g., delete confirmation)
*
* @example
* ```ts
* // Basic usage
* const { isConfirming, requestConfirm } = useConfirm({
* timeout: 3000,
* onConfirm: async (id) => { await deleteItem(id) }
* })
*
* // In template
* <button @click="requestConfirm('delete')">
* {{ isConfirming('delete') ? 'Confirm?' : 'Delete' }}
* </button>
*
* // With loading state
* const { isPending, requestConfirm } = useConfirm({ ... })
* <button :disabled="isPending" @click="requestConfirm('id')">
* {{ isPending ? 'Processing...' : 'Delete' }}
* </button>
* ```
*/
export function useConfirm<T extends string | number = string | number>(
options: UseConfirmOptions<T> = {}
): UseConfirmReturn<T> {
const { timeout = 3000, onConfirm, onCancel } = options;
const confirmId = ref<T | null>(null) as Ref<T | null>;
const isPending = ref(false);
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const clearTimer = (): void => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
};
/**
* Check if a specific id is in confirming state
*/
const isConfirming = (id: T): boolean => {
return confirmId.value === id;
};
/**
* Start confirming state for an id (with auto timeout)
*/
const startConfirm = (id: T): void => {
clearTimer();
confirmId.value = id;
// Auto cancel after timeout (0 = disabled)
if (timeout > 0) {
timeoutId = setTimeout(() => {
if (confirmId.value === id) {
confirmId.value = null;
onCancel?.(id);
}
}, timeout);
}
};
/**
* Request confirmation for an id
* - First click: enter confirming state
* - Second click: execute confirm action
* @returns true if confirmed, false if entered confirming state
*/
const requestConfirm = async (id: T): Promise<boolean> => {
// Prevent action while pending
if (isPending.value) return false;
if (confirmId.value === id) {
// Already confirming, execute action
clearTimer();
isPending.value = true;
try {
await onConfirm?.(id);
return true;
} finally {
confirmId.value = null;
isPending.value = false;
}
} else {
// Enter confirming state
startConfirm(id);
return false;
}
};
/**
* Manually confirm the current id
*/
const confirm = async (): Promise<void> => {
if (confirmId.value === null || isPending.value) return;
clearTimer();
const id = confirmId.value;
isPending.value = true;
try {
await onConfirm?.(id);
} finally {
confirmId.value = null;
isPending.value = false;
}
};
/**
* Cancel the confirming state
*/
const cancel = (): void => {
if (confirmId.value === null) return;
const id = confirmId.value;
clearTimer();
confirmId.value = null;
onCancel?.(id);
};
/**
* Reset state without triggering callbacks
*/
const reset = (): void => {
clearTimer();
confirmId.value = null;
isPending.value = false;
};
// Cleanup on unmount
onUnmounted(clearTimer);
return {
confirmId: readonly(confirmId),
isPending: readonly(isPending),
isConfirming,
startConfirm,
requestConfirm,
confirm,
cancel,
reset
};
}

View File

@@ -0,0 +1,147 @@
import { ref, readonly, onUnmounted, type Ref, type DeepReadonly } from 'vue';
export interface UsePollingOptions<T> {
/** Polling interval in ms (default: 500) */
interval?: number;
/** Execute immediately when started (default: true) */
immediate?: boolean;
/** Auto-stop condition, return true to stop polling */
shouldStop?: (data: T) => boolean;
/** Callback on each successful poll */
onSuccess?: (data: T) => void;
/** Callback when error occurs */
onError?: (error: unknown) => void;
/** Callback when polling stops (either manual or auto) */
onStop?: () => void;
}
export interface UsePollingReturn<T> {
/** Latest fetched data (readonly) */
data: DeepReadonly<Ref<T | null>>;
/** Error message if any (readonly) */
error: DeepReadonly<Ref<string>>;
/** Whether polling is active (readonly) */
isActive: DeepReadonly<Ref<boolean>>;
/** Start polling */
start: () => void;
/** Stop polling */
stop: () => void;
/** Reset all state (also stops polling) */
reset: () => void;
}
/**
* Composable for polling async operations with auto-stop support
*
* @example
* ```ts
* // Basic usage
* const { data, isActive, start, stop } = usePolling(
* () => api.getProgress(),
* {
* interval: 200,
* shouldStop: (d) => d.progress >= 100 || !!d.error,
* onSuccess: (d) => console.log('Progress:', d.progress)
* }
* )
*
* // Start polling
* start()
*
* // With reactive data binding
* <div>{{ isActive ? `${data?.progress}%` : 'Idle' }}</div>
* ```
*/
export function usePolling<T>(
fetcher: () => Promise<T>,
options: UsePollingOptions<T> = {}
): UsePollingReturn<T> {
const {
interval = 500,
immediate = true,
shouldStop,
onSuccess,
onError,
onStop
} = options;
const data = ref<T | null>(null) as Ref<T | null>;
const error = ref('');
const isActive = ref(false);
let timerId = 0;
const clearTimer = (): void => {
if (timerId) {
clearInterval(timerId);
timerId = 0;
}
};
const poll = async (): Promise<void> => {
try {
const result = await fetcher();
data.value = result;
error.value = '';
onSuccess?.(result);
// Check auto-stop condition
if (shouldStop?.(result)) {
stop();
}
} catch (e) {
error.value = e instanceof Error ? e.message : String(e);
onError?.(e);
stop();
}
};
/**
* Start polling
*/
const start = (): void => {
if (isActive.value) return;
isActive.value = true;
error.value = '';
// Execute immediately if configured
if (immediate) {
poll();
}
timerId = window.setInterval(poll, interval);
};
/**
* Stop polling
*/
const stop = (): void => {
if (!isActive.value) return;
clearTimer();
isActive.value = false;
onStop?.();
};
/**
* Reset all state to initial values
*/
const reset = (): void => {
clearTimer();
data.value = null;
error.value = '';
isActive.value = false;
};
// Cleanup on unmount
onUnmounted(clearTimer);
return {
data: readonly(data),
error: readonly(error),
isActive: readonly(isActive),
start,
stop,
reset
};
}

View File

@@ -0,0 +1,37 @@
import type { Directive, DirectiveBinding } from 'vue';
type ClickOutsideHandler = (event: MouseEvent) => void;
interface ClickOutsideElement extends HTMLElement {
_clickOutsideHandler?: (event: MouseEvent) => void;
}
/**
* v-click-outside directive
* Triggers a callback when clicking outside the element
*
* Usage:
* <div v-click-outside="handleClickOutside">...</div>
*/
export const clickOutside: Directive<ClickOutsideElement, ClickOutsideHandler> = {
mounted(el: ClickOutsideElement, binding: DirectiveBinding<ClickOutsideHandler>) {
const handler = (event: MouseEvent) => {
const target = event.target as Node;
if (el && !el.contains(target)) {
binding.value(event);
}
};
el._clickOutsideHandler = handler;
document.addEventListener('click', handler);
},
unmounted(el: ClickOutsideElement) {
if (el._clickOutsideHandler) {
document.removeEventListener('click', el._clickOutsideHandler);
delete el._clickOutsideHandler;
}
}
};
export default clickOutside;

View File

@@ -0,0 +1,13 @@
import type { App } from 'vue';
import { clickOutside } from './clickOutside';
export { clickOutside };
/**
* Register all custom directives
*/
export function registerDirectives(app: App) {
app.directive('click-outside', clickOutside);
}
export default registerDirectives;

View File

@@ -19,6 +19,8 @@ export default {
searchLanguage: 'Search language...',
noLanguageFound: 'No language found',
formatHint: 'Click Format Block (Ctrl+Shift+F)',
previewMarkdown: 'Preview Markdown',
closePreview: 'Close Preview',
// Document selector
selectDocument: 'Select Document',
searchOrCreateDocument: 'Search or enter new document name...',
@@ -31,13 +33,6 @@ export default {
confirmDelete: 'Click again to confirm delete',
openInNewWindow: 'Open in New Window',
alreadyOpenInNewWindow: 'Already open in another window',
documentNameTooLong: 'Document name cannot exceed {max} characters',
documentNameRequired: 'Document name cannot be empty',
cannotDeleteLastDocument: 'Cannot delete the last document',
cannotDeleteDefaultDocument: 'Cannot delete the default document',
unknownTime: 'Unknown time',
invalidDate: 'Invalid date',
timeError: 'Time error',
},
languages: {
'zh-CN': 'Chinese',
@@ -51,7 +46,7 @@ export default {
keybindings: {
headers: {
shortcut: 'Shortcut',
category: 'Category',
extension: 'Extension',
description: 'Description'
},
commands: {
@@ -109,7 +104,6 @@ export default {
deleteCharForward: 'Delete character forward',
deleteGroupBackward: 'Delete group backward',
deleteGroupForward: 'Delete group forward',
textHighlightToggle: 'Toggle text highlight',
}
},
tabs: {
@@ -159,53 +153,6 @@ export default {
customThemeColors: 'Custom Theme Colors',
resetToDefault: 'Reset to Default',
colorValue: 'Color Value',
themeColors: {
basic: 'Basic Colors',
text: 'Text Colors',
syntax: 'Syntax Highlighting',
interface: 'Interface Elements',
border: 'Borders & Dividers',
search: 'Search & Matching',
// Base Colors
background: 'Main Background',
backgroundSecondary: 'Secondary Background',
surface: 'Panel Background',
dropdownBackground: 'Dropdown Background',
dropdownBorder: 'Dropdown Border',
// Text Colors
foreground: 'Primary Text',
foregroundSecondary: 'Secondary Text',
comment: 'Comments',
// Syntax Highlighting - Core
keyword: 'Keywords',
string: 'Strings',
function: 'Functions',
number: 'Numbers',
operator: 'Operators',
variable: 'Variables',
type: 'Types',
// Syntax Highlighting - Extended
constant: 'Constants',
storage: 'Storage Type',
parameter: 'Parameters',
class: 'Class Names',
heading: 'Headings',
invalid: 'Invalid/Error',
regexp: 'Regular Expressions',
// Interface Elements
cursor: 'Cursor',
selection: 'Selection Background',
selectionBlur: 'Unfocused Selection',
activeLine: 'Active Line Highlight',
lineNumber: 'Line Numbers',
activeLineNumber: 'Active Line Number',
// Borders & Dividers
borderColor: 'Border Color',
borderLight: 'Light Border',
// Search & Matching
searchMatch: 'Search Match',
matchingBracket: 'Matching Bracket'
},
lineHeight: 'Line Height',
tabSettings: 'Tab Settings',
tabSize: 'Tab Size',
@@ -280,14 +227,10 @@ export default {
sshKeyPassphrase: 'SSH Key Passphrase',
sshKeyPassphrasePlaceholder: 'Enter SSH key passphrase',
backupOperations: 'Backup Operations',
pushToRemote: 'Push to Remote',
pushing: 'Pushing...',
syncToRemote: 'Sync to Remote',
syncing: 'Syncing...',
actions: {
push: 'Push',
},
status: {
success: 'Success',
failed: 'Failed'
sync: 'Sync',
}
},
},
@@ -302,7 +245,7 @@ export default {
},
colorSelector: {
name: 'Color Selector',
description: 'Visual color picker and color value display'
description: 'CSS code block visual color picker and color value display'
},
translator: {
name: 'Text Translator',
@@ -320,23 +263,37 @@ export default {
name: 'Code Folding',
description: 'Collapse and expand code sections for better readability'
},
textHighlight: {
name: 'Text Highlight',
description: 'Highlight selected text content (Ctrl+Shift+H to toggle highlight)',
backgroundColor: 'Background Color',
opacity: 'Opacity'
},
checkbox: {
name: 'Checkbox',
description: 'Render [x] and [ ] as interactive checkboxes'
markdown: {
name: 'Markdown Renderer',
description: 'Render Markdown elements, "what you see is what you get"'
},
codeblock: {
name: 'Code Block',
description: 'Code block related functionality'
},
lineNumbers: {
name: 'Line Numbers',
description: 'Display line numbers on the left side of the editor and highlight the current line'
},
contextMenu: {
name: 'Context Menu',
description: 'Show context menu when right-clicking in the editor'
},
highlightWhitespace: {
name: 'Highlight Whitespace',
description: 'Display whitespace characters such as spaces and tabs in the editor'
},
highlightTrailingWhitespace: {
name: 'Highlight Trailing Whitespace',
description: 'Highlight trailing whitespace at the end of lines'
},
httpClient: {
name: 'HTTP Client',
description: 'Send HTTP requests directly in the editor and view responses'
}
},
monitor: {
memory: 'Memory',
clickToClean: 'Click to clean memory'
}
};
};

View File

@@ -19,6 +19,8 @@ export default {
searchLanguage: '搜索语言...',
noLanguageFound: '未找到匹配的语言',
formatHint: '点击格式化区块Ctrl+Shift+F',
previewMarkdown: '预览 Markdown',
closePreview: '关闭预览',
// 文档选择器
selectDocument: '选择文档',
searchOrCreateDocument: '搜索或输入新文档名...',
@@ -31,13 +33,6 @@ export default {
confirmDelete: '再次点击确认删除',
openInNewWindow: '在新窗口中打开',
alreadyOpenInNewWindow: '已在新窗口中打开',
documentNameTooLong: '文档名称不能超过{max}个字符',
documentNameRequired: '文档名称不能为空',
cannotDeleteLastDocument: '无法删除最后一个文档',
cannotDeleteDefaultDocument: '无法删除默认文档',
unknownTime: '未知时间',
invalidDate: '无效日期',
timeError: '时间错误',
},
languages: {
'zh-CN': '简体中文',
@@ -51,7 +46,7 @@ export default {
keybindings: {
headers: {
shortcut: '快捷键',
category: '分类',
extension: '扩展',
description: '描述'
},
commands: {
@@ -109,7 +104,6 @@ export default {
deleteCharForward: '向前删除字符',
deleteGroupBackward: '向后删除组',
deleteGroupForward: '向前删除组',
textHighlightToggle: '切换文本高亮',
}
},
tabs: {
@@ -200,54 +194,6 @@ export default {
customThemeColors: '自定义主题颜色',
resetToDefault: '重置为默认',
colorValue: '颜色值',
themeColors: {
basic: '基础色调',
text: '文本颜色',
syntax: '语法高亮',
interface: '界面元素',
border: '边框分割线',
search: '搜索匹配',
// 基础色调
background: '主背景色',
backgroundSecondary: '次要背景色',
surface: '面板背景',
dropdownBackground: '下拉菜单背景',
dropdownBorder: '下拉菜单边框',
// 文本颜色
foreground: '主文本色',
foregroundSecondary: '次要文本色',
comment: '注释色',
// 语法高亮 - 核心
keyword: '关键字',
string: '字符串',
function: '函数名',
number: '数字',
operator: '操作符',
variable: '变量',
type: '类型',
// 语法高亮 - 扩展
constant: '常量',
storage: '存储类型',
parameter: '参数',
class: '类名',
heading: '标题',
invalid: '无效内容',
regexp: '正则表达式',
// 界面元素
cursor: '光标',
selection: '选中背景',
selectionBlur: '失焦选中背景',
activeLine: '当前行高亮',
lineNumber: '行号',
activeLineNumber: '活动行号',
// 边框和分割线
borderColor: '边框色',
borderLight: '浅色边框',
// 搜索和匹配
searchMatch: '搜索匹配',
matchingBracket: '匹配括号'
},
hotkeyPreview: '预览:',
none: '无',
backup: {
@@ -283,14 +229,10 @@ export default {
sshKeyPassphrase: 'SSH密钥密码',
sshKeyPassphrasePlaceholder: '请输入SSH密钥密码',
backupOperations: '备份操作',
pushToRemote: '推送到远程',
pushing: '推送中...',
syncToRemote: '同步到远程',
syncing: '同步中...',
actions: {
push: '推送',
},
status: {
success: '成功',
failed: '失败'
sync: '同步',
}
},
},
@@ -305,7 +247,7 @@ export default {
},
colorSelector: {
name: '颜色选择器',
description: '颜色值的可视化和选择'
description: 'CSS代码块颜色值的可视化和选择'
},
translator: {
name: '划词翻译',
@@ -323,23 +265,37 @@ export default {
name: '代码折叠',
description: '折叠和展开代码段以提高代码可读性'
},
textHighlight: {
name: '文本高亮',
description: '高亮选中的文本内容 (Ctrl+Shift+H 切换高亮)',
backgroundColor: '背景颜色',
opacity: '透明度'
},
checkbox: {
name: '选择框',
description: '将 [x] 和 [ ] 渲染为可交互的选择框'
markdown: {
name: 'Markdown 渲染',
description: '渲染 Markdown 元素,“所见即所得”'
},
codeblock: {
name: '代码块',
description: '代码块相关功能'
},
lineNumbers: {
name: '行号显示',
description: '在编辑器左侧显示行号,并高亮当前行'
},
contextMenu: {
name: '上下文菜单',
description: '在编辑器中右键点击时显示上下文菜单'
},
highlightWhitespace: {
name: '显示空白字符',
description: '在编辑器中显示空格和制表符等空白字符'
},
highlightTrailingWhitespace: {
name: '高亮行尾空白',
description: '高亮显示行尾的多余空白字符'
},
httpClient: {
name: 'HTTP 客户端',
description: '在编辑器中直接发送 HTTP 请求并查看响应'
}
},
monitor: {
memory: '内存',
clickToClean: '点击清理内存'
}
};
};

View File

@@ -1,15 +1,18 @@
import {createApp} from 'vue';
import { createApp } from 'vue';
import App from './App.vue';
import '@/assets/styles/index.css';
import {createPinia} from 'pinia';
import { createPinia } from 'pinia';
import i18n from './i18n';
import router from './router';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import { registerDirectives } from './directives';
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
const app = createApp(App);
app.use(pinia)
app.use(pinia);
app.use(i18n);
app.use(router);
registerDirectives(app);
app.mount('#app');

View File

@@ -1,49 +1,31 @@
import { defineStore } from 'pinia';
import { ref, onScopeDispose } from 'vue';
import { ref } from 'vue';
import { BackupService } from '@/../bindings/voidraft/internal/services';
import { useConfigStore } from '@/stores/configStore';
import { createTimerManager } from '@/common/utils/timerUtils';
export const useBackupStore = defineStore('backup', () => {
const isPushing = ref(false);
const message = ref<string | null>(null);
const isError = ref(false);
const timer = createTimerManager();
const configStore = useConfigStore();
const isSyncing = ref(false);
const error = ref<string | null>(null);
onScopeDispose(() => timer.clear());
const pushToRemote = async () => {
const isConfigured = Boolean(configStore.config.backup.repo_url?.trim());
if (isPushing.value || !isConfigured) {
const sync = async (): Promise<void> => {
if (isSyncing.value) {
return;
}
isSyncing.value = true;
error.value = null;
try {
isPushing.value = true;
message.value = null;
timer.clear();
await BackupService.PushToRemote();
isError.value = false;
message.value = 'push successful';
timer.set(() => { message.value = null; }, 3000);
} catch (error) {
isError.value = true;
message.value = error instanceof Error ? error.message : 'backup operation failed';
timer.set(() => { message.value = null; }, 5000);
await BackupService.Sync();
} catch (e) {
error.value = e instanceof Error ? e.message : String(e);
} finally {
isPushing.value = false;
isSyncing.value = false;
}
};
return {
isPushing,
message,
isError,
pushToRemote
isSyncing,
error,
sync
};
});

View File

@@ -3,29 +3,23 @@ import {computed, reactive} from 'vue';
import {ConfigService, StartupService} from '@/../bindings/voidraft/internal/services';
import {
AppConfig,
AppearanceConfig,
AuthMethod,
EditingConfig,
GeneralConfig,
GitBackupConfig,
LanguageType,
SystemThemeType,
TabType,
UpdatesConfig
TabType
} from '@/../bindings/voidraft/internal/models/models';
import {useI18n} from 'vue-i18n';
import {ConfigUtils} from '@/common/utils/configUtils';
import {FONT_OPTIONS} from '@/common/constant/fonts';
import {SUPPORTED_LOCALES} from '@/common/constant/locales';
import {
APPEARANCE_CONFIG_KEY_MAP,
BACKUP_CONFIG_KEY_MAP,
CONFIG_KEY_MAP,
CONFIG_LIMITS,
ConfigKey,
ConfigSection,
DEFAULT_CONFIG,
EDITING_CONFIG_KEY_MAP,
GENERAL_CONFIG_KEY_MAP,
NumberConfigKey,
UPDATES_CONFIG_KEY_MAP
NumberConfigKey
} from '@/common/constant/config';
import * as runtime from '@wailsio/runtime';
@@ -42,86 +36,42 @@ export const useConfigStore = defineStore('config', () => {
// Font options (no longer localized)
const fontOptions = computed(() => FONT_OPTIONS);
// 计算属性 - 使用工厂函数简化
// 计算属性
const createLimitComputed = (key: NumberConfigKey) => computed(() => CONFIG_LIMITS[key]);
const limits = Object.fromEntries(
(['fontSize', 'tabSize', 'lineHeight'] as const).map(key => [key, createLimitComputed(key)])
) as Record<NumberConfigKey, ReturnType<typeof createLimitComputed>>;
// 通用配置更新方法
const updateGeneralConfig = async <K extends keyof GeneralConfig>(key: K, value: GeneralConfig[K]): Promise<void> => {
// 确保配置已加载
// 统一配置更新方法
const updateConfig = async <K extends ConfigKey>(key: K, value: any): Promise<void> => {
if (!state.configLoaded && !state.isLoading) {
await initConfig();
}
const backendKey = GENERAL_CONFIG_KEY_MAP[key];
const backendKey = CONFIG_KEY_MAP[key];
if (!backendKey) {
throw new Error(`No backend key mapping found for general.${key.toString()}`);
throw new Error(`No backend key mapping found for ${String(key)}`);
}
// 从 backendKey 提取 section例如 'general.alwaysOnTop' -> 'general'
const section = backendKey.split('.')[0] as ConfigSection;
await ConfigService.Set(backendKey, value);
state.config.general[key] = value;
(state.config[section] as any)[key] = value;
};
const updateEditingConfig = async <K extends keyof EditingConfig>(key: K, value: EditingConfig[K]): Promise<void> => {
// 确保配置已加载
if (!state.configLoaded && !state.isLoading) {
await initConfig();
}
const backendKey = EDITING_CONFIG_KEY_MAP[key];
if (!backendKey) {
throw new Error(`No backend key mapping found for editing.${key.toString()}`);
}
await ConfigService.Set(backendKey, value);
state.config.editing[key] = value;
// 只更新本地状态,不保存到后端
const updateConfigLocal = <K extends ConfigKey>(key: K, value: any): void => {
const backendKey = CONFIG_KEY_MAP[key];
const section = backendKey.split('.')[0] as ConfigSection;
(state.config[section] as any)[key] = value;
};
const updateAppearanceConfig = async <K extends keyof AppearanceConfig>(key: K, value: AppearanceConfig[K]): Promise<void> => {
// 确保配置已加载
if (!state.configLoaded && !state.isLoading) {
await initConfig();
}
const backendKey = APPEARANCE_CONFIG_KEY_MAP[key];
if (!backendKey) {
throw new Error(`No backend key mapping found for appearance.${key.toString()}`);
}
await ConfigService.Set(backendKey, value);
state.config.appearance[key] = value;
};
const updateUpdatesConfig = async <K extends keyof UpdatesConfig>(key: K, value: UpdatesConfig[K]): Promise<void> => {
// 确保配置已加载
if (!state.configLoaded && !state.isLoading) {
await initConfig();
}
const backendKey = UPDATES_CONFIG_KEY_MAP[key];
if (!backendKey) {
throw new Error(`No backend key mapping found for updates.${key.toString()}`);
}
await ConfigService.Set(backendKey, 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 saveConfig = async <K extends ConfigKey>(key: K): Promise<void> => {
const backendKey = CONFIG_KEY_MAP[key];
const section = backendKey.split('.')[0] as ConfigSection;
await ConfigService.Set(backendKey, (state.config[section] as any)[key]);
};
// 加载配置
@@ -155,22 +105,24 @@ export const useConfigStore = defineStore('config', () => {
const clamp = (value: number) => ConfigUtils.clamp(value, limit.min, limit.max);
return {
increase: async () => await updateEditingConfig(key, clamp(state.config.editing[key] + 1)),
decrease: async () => await updateEditingConfig(key, clamp(state.config.editing[key] - 1)),
set: async (value: number) => await updateEditingConfig(key, clamp(value)),
reset: async () => await updateEditingConfig(key, limit.default)
increase: async () => await updateConfig(key, clamp(state.config.editing[key] + 1)),
decrease: async () => await updateConfig(key, clamp(state.config.editing[key] - 1)),
set: async (value: number) => await updateConfig(key, clamp(value)),
reset: async () => await updateConfig(key, limit.default),
increaseLocal: () => updateConfigLocal(key, clamp(state.config.editing[key] + 1)),
decreaseLocal: () => updateConfigLocal(key, clamp(state.config.editing[key] - 1))
};
};
const createEditingToggler = <T extends keyof EditingConfig>(key: T) =>
async () => await updateEditingConfig(key, !state.config.editing[key] as EditingConfig[T]);
async () => await updateConfig(key as ConfigKey, !state.config.editing[key] as EditingConfig[T]);
// 枚举值切换器
const createEnumToggler = <T extends TabType>(key: 'tabType', values: readonly T[]) =>
async () => {
const currentIndex = values.indexOf(state.config.editing[key] as T);
const nextIndex = (currentIndex + 1) % values.length;
return await updateEditingConfig(key, values[nextIndex]);
return await updateConfig(key, values[nextIndex]);
};
// 重置配置
@@ -192,26 +144,24 @@ export const useConfigStore = defineStore('config', () => {
// 语言设置方法
const setLanguage = async (language: LanguageType): Promise<void> => {
await updateAppearanceConfig('language', language);
// 同步更新前端语言
await updateConfig('language', language);
const frontendLocale = ConfigUtils.backendLanguageToFrontend(language);
locale.value = frontendLocale as any;
};
// 系统主题设置方法
const setSystemTheme = async (systemTheme: SystemThemeType): Promise<void> => {
await updateAppearanceConfig('systemTheme', systemTheme);
await updateConfig('systemTheme', systemTheme);
};
// 当前主题设置方法
const setCurrentTheme = async (themeName: string): Promise<void> => {
await updateAppearanceConfig('currentTheme', themeName);
await updateConfig('currentTheme', themeName);
};
// 初始化语言设置
const initializeLanguage = async (): Promise<void> => {
const initLanguage = async (): Promise<void> => {
try {
// 如果配置未加载,先加载配置
if (!state.configLoaded) {
@@ -238,21 +188,12 @@ export const useConfigStore = defineStore('config', () => {
const togglers = {
tabIndent: createEditingToggler('enableTabIndent'),
alwaysOnTop: async () => {
await updateGeneralConfig('alwaysOnTop', !state.config.general.alwaysOnTop);
// 立即应用窗口置顶状态
await updateConfig('alwaysOnTop', !state.config.general.alwaysOnTop);
await runtime.Window.SetAlwaysOnTop(state.config.general.alwaysOnTop);
},
tabType: createEnumToggler('tabType', CONFIG_LIMITS.tabType.values)
};
// 字符串配置设置器
const setters = {
fontFamily: async (value: string) => await updateEditingConfig('fontFamily', value),
fontWeight: async (value: string) => await updateEditingConfig('fontWeight', value),
dataPath: async (value: string) => await updateGeneralConfig('dataPath', value),
autoSaveDelay: async (value: number) => await updateEditingConfig('autoSaveDelay', value)
};
return {
// 状态
config: computed(() => state.config),
@@ -269,7 +210,7 @@ export const useConfigStore = defineStore('config', () => {
// 语言相关方法
setLanguage,
initializeLanguage,
initLanguage,
// 主题相关方法
setSystemTheme,
@@ -281,10 +222,14 @@ export const useConfigStore = defineStore('config', () => {
decreaseFontSize: adjusters.fontSize.decrease,
resetFontSize: adjusters.fontSize.reset,
setFontSize: adjusters.fontSize.set,
// 字体大小操作
increaseFontSizeLocal: adjusters.fontSize.increaseLocal,
decreaseFontSizeLocal: adjusters.fontSize.decreaseLocal,
saveFontSize: () => saveConfig('fontSize'),
// Tab操作
toggleTabIndent: togglers.tabIndent,
setEnableTabIndent: (value: boolean) => updateEditingConfig('enableTabIndent', value),
setEnableTabIndent: (value: boolean) => updateConfig('enableTabIndent', value),
...adjusters.tabSize,
increaseTabSize: adjusters.tabSize.increase,
decreaseTabSize: adjusters.tabSize.decrease,
@@ -296,59 +241,53 @@ export const useConfigStore = defineStore('config', () => {
// 窗口操作
toggleAlwaysOnTop: togglers.alwaysOnTop,
setAlwaysOnTop: (value: boolean) => updateGeneralConfig('alwaysOnTop', value),
setAlwaysOnTop: (value: boolean) => updateConfig('alwaysOnTop', value),
// 字体操作
setFontFamily: setters.fontFamily,
setFontWeight: setters.fontWeight,
setFontFamily: (value: string) => updateConfig('fontFamily', value),
setFontWeight: (value: string) => updateConfig('fontWeight', value),
// 路径操作
setDataPath: setters.dataPath,
setDataPath: (value: string) => updateConfig('dataPath', value),
// 保存配置相关方法
setAutoSaveDelay: setters.autoSaveDelay,
setAutoSaveDelay: (value: number) => updateConfig('autoSaveDelay', value),
// 热键配置相关方法
setEnableGlobalHotkey: (value: boolean) => updateGeneralConfig('enableGlobalHotkey', value),
setGlobalHotkey: (hotkey: any) => updateGeneralConfig('globalHotkey', hotkey),
setEnableGlobalHotkey: (value: boolean) => updateConfig('enableGlobalHotkey', value),
setGlobalHotkey: (hotkey: any) => updateConfig('globalHotkey', hotkey),
// 系统托盘配置相关方法
setEnableSystemTray: (value: boolean) => updateGeneralConfig('enableSystemTray', value),
setEnableSystemTray: (value: boolean) => updateConfig('enableSystemTray', value),
// 开机启动配置相关方法
setStartAtLogin: async (value: boolean) => {
// 先更新配置文件
await updateGeneralConfig('startAtLogin', value);
// 再调用系统设置API
await updateConfig('startAtLogin', value);
await StartupService.SetEnabled(value);
},
// 窗口吸附配置相关方法
setEnableWindowSnap: async (value: boolean) => await updateGeneralConfig('enableWindowSnap', value),
setEnableWindowSnap: (value: boolean) => updateConfig('enableWindowSnap', value),
// 加载动画配置相关方法
setEnableLoadingAnimation: async (value: boolean) => await updateGeneralConfig('enableLoadingAnimation', value),
setEnableLoadingAnimation: (value: boolean) => updateConfig('enableLoadingAnimation', value),
// 标签页配置相关方法
setEnableTabs: async (value: boolean) => await updateGeneralConfig('enableTabs', value),
setEnableTabs: (value: boolean) => updateConfig('enableTabs', value),
// 更新配置相关方法
setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value),
setAutoUpdate: (value: boolean) => updateConfig('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),
setEnableBackup: (value: boolean) => updateConfig('enabled', value),
setAutoBackup: (value: boolean) => updateConfig('auto_backup', value),
setRepoUrl: (value: string) => updateConfig('repo_url', value),
setAuthMethod: (value: AuthMethod) => updateConfig('auth_method', value),
setUsername: (value: string) => updateConfig('username', value),
setPassword: (value: string) => updateConfig('password', value),
setToken: (value: string) => updateConfig('token', value),
setSshKeyPath: (value: string) => updateConfig('ssh_key_path', value),
setSshKeyPassphrase: (value: string) => updateConfig('ssh_key_passphrase', value),
setBackupInterval: (value: number) => updateConfig('backup_interval', value),
};
});

View File

@@ -2,17 +2,20 @@ import {defineStore} from 'pinia';
import {computed, ref} from 'vue';
import {DocumentService} from '@/../bindings/voidraft/internal/services';
import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice';
import {Document} from '@/../bindings/voidraft/internal/models/models';
import {Document} from '@/../bindings/voidraft/internal/models/ent/models';
import {useTabStore} from "@/stores/tabStore";
import type {EditorViewState} from '@/stores/editorStore';
export const useDocumentStore = defineStore('document', () => {
const DEFAULT_DOCUMENT_ID = ref<number>(1); // 默认草稿文档ID
// === 核心状态 ===
const documents = ref<Record<number, Document>>({});
const currentDocumentId = ref<number | null>(null);
const currentDocument = ref<Document | null>(null);
// === 编辑器状态持久化 ===
const documentStates = ref<Record<number, EditorViewState>>({});
// === UI状态 ===
const showDocumentSelector = ref(false);
const selectorError = ref<{ docId: number; message: string } | null>(null);
@@ -21,15 +24,18 @@ export const useDocumentStore = defineStore('document', () => {
// === 计算属性 ===
const documentList = computed(() =>
Object.values(documents.value).sort((a, b) => {
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
const timeA = a.updated_at ? new Date(a.updated_at).getTime() : 0;
const timeB = b.updated_at ? new Date(b.updated_at).getTime() : 0;
return timeB - timeA;
})
);
// === 私有方法 ===
const setDocuments = (docs: Document[]) => {
documents.value = {};
docs.forEach(doc => {
documents.value[doc.id] = doc;
if (doc.id !== undefined) {
documents.value[doc.id] = doc;
}
});
};
@@ -59,15 +65,7 @@ export const useDocumentStore = defineStore('document', () => {
clearError();
};
const toggleDocumentSelector = () => {
if (showDocumentSelector.value) {
closeDocumentSelector();
} else {
openDocumentSelector();
}
};
// === 文档操作方法 ===
// 在新窗口中打开文档
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
@@ -89,7 +87,7 @@ export const useDocumentStore = defineStore('document', () => {
const createNewDocument = async (title: string): Promise<Document | null> => {
try {
const doc = await DocumentService.CreateDocument(title);
if (doc) {
if (doc && doc.id !== undefined) {
documents.value[doc.id] = doc;
return doc;
}
@@ -118,8 +116,6 @@ export const useDocumentStore = defineStore('document', () => {
// 打开文档
const openDocument = async (docId: number): Promise<boolean> => {
try {
closeDocumentSelector();
// 获取完整文档数据
const doc = await DocumentService.GetDocumentByID(docId);
if (!doc) {
@@ -145,14 +141,18 @@ export const useDocumentStore = defineStore('document', () => {
const doc = documents.value[docId];
if (doc) {
doc.title = title;
doc.updatedAt = new Date().toISOString();
doc.updated_at = new Date().toISOString();
}
if (currentDocument.value?.id === docId) {
currentDocument.value.title = title;
currentDocument.value.updatedAt = new Date().toISOString();
currentDocument.value.updated_at = new Date().toISOString();
}
// 同步更新标签页标题
const tabStore = useTabStore();
tabStore.updateTabTitle(docId, title);
return true;
} catch (error) {
console.error('Failed to update document metadata:', error);
@@ -163,20 +163,21 @@ export const useDocumentStore = defineStore('document', () => {
// 删除文档
const deleteDocument = async (docId: number): Promise<boolean> => {
try {
// 检查是否是默认文档使用ID判断
if (docId === DEFAULT_DOCUMENT_ID.value) {
return false;
}
await DocumentService.DeleteDocument(docId);
// 更新本地状态
delete documents.value[docId];
// 同步清理标签页
const tabStore = useTabStore();
if (tabStore.hasTab(docId)) {
tabStore.closeTab(docId);
}
// 如果删除的是当前文档,切换到第一个可用文档
if (currentDocumentId.value === docId) {
const availableDocs = Object.values(documents.value);
if (availableDocs.length > 0) {
if (availableDocs.length > 0 && availableDocs[0].id !== undefined) {
await openDocument(availableDocs[0].id);
} else {
currentDocumentId.value = null;
@@ -203,8 +204,10 @@ export const useDocumentStore = defineStore('document', () => {
// 如果URL中没有指定文档ID则使用持久化的文档ID
await openDocument(currentDocumentId.value);
} else {
// 否则打开默认文档
await openDocument(DEFAULT_DOCUMENT_ID.value);
// 否则打开第一个文档
if (documents.value[0].id) {
await openDocument(documents.value[0].id);
}
}
} catch (error) {
console.error('Failed to initialize document store:', error);
@@ -212,12 +215,12 @@ export const useDocumentStore = defineStore('document', () => {
};
return {
DEFAULT_DOCUMENT_ID,
// 状态
documents,
documentList,
currentDocumentId,
currentDocument,
documentStates,
showDocumentSelector,
selectorError,
isLoading,
@@ -231,7 +234,6 @@ export const useDocumentStore = defineStore('document', () => {
deleteDocument,
openDocumentSelector,
closeDocumentSelector,
toggleDocumentSelector,
setError,
clearError,
initialize,
@@ -240,6 +242,6 @@ export const useDocumentStore = defineStore('document', () => {
persist: {
key: 'voidraft-document',
storage: localStorage,
pick: ['currentDocumentId', 'documents']
pick: ['currentDocumentId', 'documents', 'documentStates']
}
});

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