59 Commits

Author SHA1 Message Date
f37c659c89 🐛 Adjusted error message and icon clearing logic 2025-07-17 11:03:42 +08:00
9fff7bcfca Added the backup feature 2025-07-17 00:12:00 +08:00
b4b0ad9bba 🎨 Optimize storage logic 2025-07-13 22:32:58 +08:00
6d8fdf62f1 💡 Update docs 2025-07-13 11:58:53 +08:00
9f53d7421d Merge remote-tracking branch 'github/master' 2025-07-12 23:56:43 +08:00
80c8ecb4cf 💡 Update docs 2025-07-12 23:56:04 +08:00
d10059a82d Create CNAME 2025-07-12 22:25:19 +08:00
737f83cd5f 💡 Add docs 2025-07-12 22:14:35 +08:00
a720a4cfb8 Complete the custom editor theme 2025-07-11 23:03:28 +08:00
b5510d605c Add multi-window document functionality 2025-07-10 18:45:51 +08:00
4d62da912a ⬆️ Upgrade wails v3 from Alpha 9 to Alpha 10 2025-07-10 10:01:52 +08:00
b52e067d50 🍎 Fix build issues 2025-07-08 17:36:45 +08:00
8dce06c30e 🐛 Fixed the reboot issue on different platforms 2025-07-08 12:41:30 +08:00
b404434b5b 🎨 Updated 2025-07-07 16:09:09 +08:00
685897e828 🎨 Updated 2025-07-07 15:55:48 +08:00
4f1d70135e 📝 README.md 2025-07-07 15:41:46 +08:00
4dc424781b 📄 Change the license 2025-07-07 13:39:54 +08:00
7fcfc5e992 🎨 Refactoring the extension service and the keybinding service 2025-07-07 13:19:59 +08:00
7c2318a13f Add translation features 2025-07-06 23:41:15 +08:00
a2a332e735 Added context menu 2025-07-04 14:37:35 +08:00
ebee33ea7c Add self-updating service 2025-07-03 15:21:01 +08:00
81eb2c94ac 🐛 Fixed some issues 2025-07-02 12:10:46 +08:00
25e1a98932 🐛 Fixed block formatting issue 2025-07-01 23:55:15 +08:00
1ccee779ae 🐛 Fixed extension management issues 2025-07-01 23:25:24 +08:00
3e45e6aa9b Performance optimization 2025-07-01 20:11:27 +08:00
1604564e63 Complete multi-document mode 2025-07-01 18:16:05 +08:00
70d88dabba Use SQLite instead of JSON storage 2025-06-29 23:42:06 +08:00
6f8775472d Add selection box extension 2025-06-25 23:50:57 +08:00
a9b967aba4 🐛 Fixed the issue of text highlighting expansion 2025-06-25 22:53:00 +08:00
69957a16cf 🎨 binding keymap and extension 2025-06-25 21:09:21 +08:00
650884cb85 Improve extension management 2025-06-25 17:50:50 +08:00
8e91e3cf7c Add extension management page 2025-06-24 19:18:30 +08:00
f3bcb87828 Add extension management service 2025-06-24 14:16:53 +08:00
ea025e3f5d Add update service 2025-06-23 19:42:48 +08:00
4f8272e290 Add configuration merge service 2025-06-23 12:03:56 +08:00
d6dd34db87 ♻️ Refactor and clean up the code 2025-06-22 21:30:45 +08:00
eb9b037f8e 🎨 Optimize code 2025-06-22 15:08:38 +08:00
35c89e086e 🎨 Updated 2025-06-22 12:08:50 +08:00
77287bccfa 🎨 Updated 2025-06-21 19:05:08 +08:00
a92e5486b2 🐛 Fixed theme and packaging issues 2025-06-21 15:33:50 +08:00
1153c0a652 🐛 Fixed bug 2025-06-21 15:04:04 +08:00
145b868a44 🐛 Fixed the issue of deleting blocks 2025-06-21 00:34:22 +08:00
6acab678d0 Unified management of keymap 2025-06-21 00:28:35 +08:00
cb3d369aef 🎨 Updated 2025-06-20 13:46:13 +08:00
85544ba1e4 Added key binding service 2025-06-20 13:37:48 +08:00
13072a00a1 Add formatting method 2025-06-19 20:23:20 +08:00
25858cb42b Use guesslang to detect the language 2025-06-19 16:55:02 +08:00
f2894b2a89 🐛 Fixed the issue of block creation 2025-06-19 13:40:25 +08:00
a62ea251cd Cancel use HTTP services and WebSockets 2025-06-19 10:44:27 +08:00
3c880199ae ♻️ Refactor code 2025-06-19 01:05:08 +08:00
9204315c7b Improve code block function 2025-06-18 21:19:19 +08:00
cce9cf7e92 Add code blocks and rainbow bracket extensions 2025-06-18 18:16:56 +08:00
87fe9d48b1 🎨 Update 2025-06-17 19:15:59 +08:00
1d6cf7cf68 Added text highlight and minimap 2025-06-17 17:31:01 +08:00
0927b921c3 🎨 Update 2025-06-12 00:58:35 +08:00
4844ccdf58 Added hyperlink extension 2025-06-10 21:28:50 +08:00
c79cba48c2 Added search and replace 2025-06-10 19:17:29 +08:00
ceb177114d 🐛 Fixed the bug of obtaining the operating system 2025-06-09 14:18:25 +08:00
5203784b63 Added window title bars for different operating systems 2025-06-09 03:02:07 +08:00
235 changed files with 32777 additions and 8447 deletions

83
LICENSE
View File

@@ -1,73 +1,18 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
MIT License
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Copyright (c) 2025 landaiqing
1. Definitions.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright 2025 landaiqing
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.

174
README.md
View File

@@ -1,59 +1,163 @@
# Welcome to Your New Wails3 Project!
# <img src="./frontend/public/appicon.png" alt="VoidRaft Logo" width="32" height="32" style="vertical-align: middle;"> VoidRaft
Congratulations on generating your Wails3 application! This README will guide you through the next steps to get your project up and running.
[中文](README_ZH.md) | **English**
## Getting Started
> *An elegant text snippet recording tool designed for developers.*
1. Navigate to your project directory in the terminal.
VoidRaft is a modern developer-focused text editor that allows you to record, organize, and manage various text snippets anytime, anywhere. Whether it's temporary code snippets, API responses, meeting notes, or daily to-do lists, VoidRaft provides a smooth and elegant editing experience.
2. To run your application in development mode, use the following command:
## Core Features
```
wails3 dev
```
### Developer-Friendly
This will start your application and enable hot-reloading for both frontend and backend changes.
- Multi-language code block support - Syntax highlighting for 30+ programming languages
- Smart language detection - Automatically recognizes code block language types
- Code formatting - Built-in Prettier support for one-click code beautification
- Block editing mode - Split content into independent code blocks, each with different language settings
- Multi-window support - edit multiple documents at the same time
- Support for custom themes - Custom editor themes
3. To build your application for production, use:
### Modern Interface
```
wails3 build
```
- Clean and elegant design - Simple yet sophisticated user interface
- Dark/Light themes - Adapts to different usage scenarios
- Multi-language support - Built-in internationalization with multiple language switching
This will create a production-ready executable in the `build` directory.
### Extension System
## Exploring Wails3 Features
- Modular architecture - Extensible editor based on CodeMirror 6
- Rich extensions - Includes various practical editor extensions
- Rainbow bracket matching
- VSCode-style search and replace
- Color picker
- Built-in translation tool
- Text highlighting
- Code folding
- Hyperlink support
- Checkbox support
- Minimap
Now that you have your project set up, it's time to explore the features that Wails3 offers:
## Quick Start
1. **Check out the examples**: The best way to learn is by example. Visit the `examples` directory in the `v3/examples` directory to see various sample applications.
### Download and Use
2. **Run an example**: To run any of the examples, navigate to the example's directory and use:
1. Visit the releases page: https://github.com/landaiqing/voidraft/releases
2. Select and download the appropriate version
3. Run the installer and start using
```
go run .
```
### Development Environment
Note: Some examples may be under development during the alpha phase.
```bash
# Clone the project
git clone https://github.com/landaiqing/voidraft
cd voidraft
3. **Explore the documentation**: Visit the [Wails3 documentation](https://v3alpha.wails.io/) for in-depth guides and API references.
# Install frontend dependencies
cd frontend
npm install
npm run build
cd ..
4. **Join the community**: Have questions or want to share your progress? Join the [Wails Discord](https://discord.gg/JDdSxwjhGf) or visit the [Wails discussions on GitHub](https://github.com/wailsapp/wails/discussions).
# Start development server
wails3 dev
```
### Production Build
```bash
# Build application
wails3 package
```
After building, the executable will be generated in the `bin` directory.
## Technical Architecture
### Core Technologies
| Layer | Technology Stack |
|-------|------------------|
| Desktop Framework | Wails3 |
| Backend Language | Go 1.21+ |
| Frontend Framework | Vue 3 + TypeScript |
| Editor Core | CodeMirror 6 |
| Build Tool | Vite |
| Data Storage | SQLite |
## Project Structure
Take a moment to familiarize yourself with your project structure:
```
Voidraft/
├── frontend/ # Vue 3 frontend application
│ ├── src/
│ │ ├── views/editor/ # Editor core views
│ │ │ ├── extensions/ # Editor extension system
│ │ │ │ ├── codeblock/ # Code block support
│ │ │ │ ├── translator/ # Translation tool
│ │ │ │ ├── minimap/ # Code minimap
│ │ │ │ ├── vscodeSearch/ # VSCode-style search
│ │ │ │ └── ... # More extensions
│ │ │ └── ...
│ │ ├── components/ # Reusable components
│ │ ├── stores/ # Pinia state management
│ │ └── utils/ # Utility functions
│ └── public/ # Static assets
├── internal/ # Go backend core
│ ├── services/ # Core business services
│ ├── models/ # Data model definitions
│ └── events/ # Event handling mechanisms
└── main.go # Application entry point
```
- `frontend/`: Contains your frontend code (HTML, CSS, JavaScript/TypeScript)
- `main.go`: The entry point of your Go backend
- `app.go`: Define your application structure and methods here
- `wails.json`: Configuration file for your Wails project
## Development Roadmap
## Next Steps
### Platform Extension Plans
1. Modify the frontend in the `frontend/` directory to create your desired UI.
2. Add backend functionality in `main.go`.
3. Use `wails3 dev` to see your changes in real-time.
4. When ready, build your application with `wails3 build`.
| Platform | Status | Expected Time |
|----------|--------|---------------|
| macOS | Planned | Future versions |
| Linux | Planned | Future versions |
Happy coding with Wails3! If you encounter any issues or have questions, don't hesitate to consult the documentation or reach out to the Wails community.
### Planned Features
- ✅ Custom themes - Customize editor themes
- ✅ Multi-window support - Support editing multiple documents simultaneously
- ✅ Data synchronization - Cloud backup for documents
- [ ] Enhanced clipboard - Monitor and manage clipboard history
- [ ] Extension system - Support for custom plugins
## Acknowledgments
> Standing on the shoulders of giants, paying tribute to the open source spirit
The birth of VoidRaft is inseparable from the following excellent open source projects:
### Special Thanks
- **[Heynote](https://github.com/heyman/heynote/)** - VoidRaft is a feature-enhanced version based on Heynote's concept
- Inherits Heynote's elegant block editing philosophy
- Adds more practical features on the original foundation
- Rebuilt with modern technology stack
- Provides richer extension system and customization options
- Thanks to the Heynote team for bringing innovative ideas to the developer community
### Core Technology Stack
| Technology | Purpose | Link |
|------------|---------|------|
| Wails3 | Cross-platform desktop application framework | [wails.io](https://v3alpha.wails.io/) |
| Vue.js | Progressive frontend framework | [vuejs.org](https://vuejs.org/) |
| CodeMirror 6 | Modern code editor | [codemirror.net](https://codemirror.net/) |
| Prettier | Code formatting tool | [prettier.io](https://prettier.io/) |
| TypeScript | Type-safe JavaScript | [typescriptlang.org](https://www.typescriptlang.org/) |
## License
This project is open source under the [MIT License](LICENSE).
Welcome to Fork, Star, and contribute code.
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub stars](https://img.shields.io/github/stars/landaiqing/Voidraft.svg?style=social&label=Star)](https://github.com/yourusername/Voidraft)
[![GitHub forks](https://img.shields.io/github/forks/landaiqing/Voidraft.svg?style=social&label=Fork)](https://github.com/yourusername/Voidraft)
*Made with ❤️ by landaiqing*

165
README_ZH.md Normal file
View File

@@ -0,0 +1,165 @@
# <img src="./frontend/public/appicon.png" alt="Voidraft Logo" width="32" height="32" style="vertical-align: middle;"> Voidraft
**中文** | [English](README.md)
> *一个专为开发者打造的优雅文本片段记录工具。*
Voidraft 是一个现代化的开发者专用文本编辑器让你能够随时随地记录、整理和管理各种文本片段。无论是临时的代码片段、API 响应、会议笔记还是日常的待办事项Voidraft 都能为你提供流畅而优雅的编辑体验。
## 核心特性
### 开发者友好
- 多语言代码块支持 - 支持 30+ 种编程语言的语法高亮
- 智能语言检测 - 自动识别代码块语言类型
- 代码格式化 - 内置 Prettier 支持,一键美化代码
- 块状编辑模式 - 将内容分割为独立的代码块,每个块可设置不同语言
- 支持多窗口 - 同时编辑多个文档
- 支持自定义主题 - 自定义编辑器主题
### 现代化界面
- 清新脱俗的设计 - 简洁而不失优雅的用户界面
- 深色/浅色主题 - 适应不同使用场景
- 多语言支持 - 内置国际化功能,支持多种语言切换
### 扩展系统
- 模块化架构 - 基于 CodeMirror 6 的可扩展编辑器
- 丰富的扩展 - 包含多种实用编辑器扩展
- 彩虹括号匹配
- VSCode 风格搜索替换
- 颜色选择器
- 内置翻译工具
- 文本高亮
- 代码折叠
- 超链接支持
- 复选框支持
- 小地图
## 快速开始
### 下载使用
1. 访问发布页面https://github.com/landaiqing/voidraft/releases
2. 选择适合的版本下载
3. 运行安装程序,开始使用
### 开发环境
```bash
# 克隆项目
git clone https://github.com/landaiqing/voidraft
cd voidraft
# 安装前端依赖
cd frontend
npm install
npm run build
cd ..
# 启动开发服务器
wails3 dev
```
### 生产构建
```bash
# 构建应用
wails3 package
```
构建完成后,可执行文件将生成在 `bin` 目录中。
## 技术架构
### 核心技术
| 层级 | 技术栈 |
|------|--------|
| 桌面框架 | Wails3 |
| 后端语言 | Go 1.21+ |
| 前端框架 | Vue 3 + TypeScript |
| 编辑器核心 | CodeMirror 6 |
| 构建工具 | Vite |
| 数据存储 | SQLite |
## 项目结构
```
Voidraft/
├── frontend/ # Vue 3 前端应用
│ ├── src/
│ │ ├── views/editor/ # 编辑器核心视图
│ │ │ ├── extensions/ # 编辑器扩展系统
│ │ │ │ ├── codeblock/ # 代码块支持
│ │ │ │ ├── translator/ # 翻译工具
│ │ │ │ ├── minimap/ # 代码小地图
│ │ │ │ ├── vscodeSearch/ # VSCode风格搜索
│ │ │ │ └── ... # 更多扩展
│ │ │ └── ...
│ │ ├── components/ # 可复用组件
│ │ ├── stores/ # Pinia 状态管理
│ │ └── utils/ # 工具函数库
│ └── public/ # 静态资源文件
├── internal/ # Go 后端核心
│ ├── services/ # 核心业务服务
│ ├── models/ # 数据模型定义
│ └── events/ # 事件处理机制
└── main.go # 应用程序入口
```
## 开发路线图
### 平台扩展计划
| 平台 | 状态 | 预期时间 |
|------|------|----------|
| macOS | 计划中 | 后续版本 |
| Linux | 计划中 | 后续版本 |
### 计划添加的功能
- ✅ 自定义主题 - 自定义编辑器主题
- ✅ 多窗口支持 - 支持同时编辑多个文档
- ✅ 数据同步 - 文档云端备份
- [ ] 剪切板增强 - 监听和管理剪切板历史
- [ ] 扩展系统 - 支持自定义插件
## 致谢
> 站在巨人的肩膀上,致敬开源精神
Voidraft 的诞生离不开以下优秀的开源项目:
### 特别感谢
- **[Heynote](https://github.com/heyman/heynote/)** - Voidraft 是基于 Heynote 概念的功能增强版本
- 继承了 Heynote 优雅的块状编辑理念
- 在原有基础上增加了更多实用功能
- 采用现代化技术栈重新构建
- 提供更丰富的扩展系统和自定义选项
- 感谢 Heynote 团队为开发者社区带来的创新思路
### 核心技术栈
| 技术 | 用途 | 链接 |
|------|------|------------------------------------------------------|
| Wails3 | 跨平台桌面应用框架 | [wails.io](https://v3alpha.wails.io/) |
| Vue.js | 渐进式前端框架 | [vuejs.org](https://vuejs.org/) |
| CodeMirror 6 | 现代化代码编辑器 | [codemirror.net](https://codemirror.net/) |
| Prettier | 代码格式化工具 | [prettier.io](https://prettier.io/) |
| TypeScript | 类型安全的 JavaScript | [typescriptlang.org](https://www.typescriptlang.org/) |
## 许可证
本项目采用 [MIT 许可证](LICENSE) 开源。
欢迎 Fork、Star 和贡献代码。
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub stars](https://img.shields.io/github/stars/landaiqing/Voidraft.svg?style=social&label=Star)](https://github.com/yourusername/Voidraft)
[![GitHub forks](https://img.shields.io/github/forks/landaiqing/Voidraft.svg?style=social&label=Fork)](https://github.com/yourusername/Voidraft)
*Made with ❤️ by landaiqing*

View File

@@ -8,9 +8,9 @@ info:
companyName: "Voidraft" # The name of the company
productName: "Voidraft" # The name of the application
productIdentifier: "landaiqing" # The unique product identifier
description: "Your Inspiration Catcher - Instant thought-capturing tool with minimalist design" # The application description
description: "Voidraft" # The application description
copyright: "© 2025 Voidraft. All rights reserved." # Copyright text
comments: "Effortlessly capture and organize fleeting ideas with minimal design" # Comments
comments: "Voidraft" # Comments
version: "0.0.1.0" # The application version
# Dev mode configuration

View File

@@ -6,13 +6,13 @@
<key>CFBundleName</key>
<string>Voidraft</string>
<key>CFBundleExecutable</key>
<string>voidraft</string>
<string>Voidraft</string>
<key>CFBundleIdentifier</key>
<string>landaiqing</string>
<key>CFBundleVersion</key>
<string>0.0.1.0</string>
<key>CFBundleGetInfoString</key>
<string>Effortlessly capture and organize fleeting ideas with minimal design</string>
<string>Voidraft</string>
<key>CFBundleShortVersionString</key>
<string>0.0.1.0</string>
<key>CFBundleIconFile</key>

View File

@@ -6,13 +6,13 @@
<key>CFBundleName</key>
<string>Voidraft</string>
<key>CFBundleExecutable</key>
<string>voidraft</string>
<string>Voidraft</string>
<key>CFBundleIdentifier</key>
<string>landaiqing</string>
<key>CFBundleVersion</key>
<string>0.0.1.0</string>
<key>CFBundleGetInfoString</key>
<string>Effortlessly capture and organize fleeting ideas with minimal design</string>
<string>Voidraft</string>
<key>CFBundleShortVersionString</key>
<string>0.0.1.0</string>
<key>CFBundleIconFile</key>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

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

View File

@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=voidraft
Exec=voidraft
Icon=appicon
Categories=Development;
Terminal=false
Keywords=wails
Version=1.0
StartupNotify=false

View File

@@ -6,10 +6,10 @@
"0000": {
"ProductVersion": "0.0.1.0",
"CompanyName": "Voidraft",
"FileDescription": "Your Inspiration Catcher - Instant thought-capturing tool with minimalist design",
"FileDescription": "Voidraft",
"LegalCopyright": "© 2025 Voidraft. All rights reserved.",
"ProductName": "Voidraft",
"Comments": "Effortlessly capture and organize fleeting ideas with minimal design"
"Comments": "Voidraft"
}
}
}

View File

@@ -5,7 +5,7 @@
!include "FileFunc.nsh"
!ifndef INFO_PROJECTNAME
!define INFO_PROJECTNAME "voidraft"
!define INFO_PROJECTNAME "Voidraft"
!endif
!ifndef INFO_COMPANYNAME
!define INFO_COMPANYNAME "Voidraft"

1
docs/CNAME Normal file
View File

@@ -0,0 +1 @@
voidraft.landaiqing.cn

75
docs/changelog.html Normal file
View File

@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VoidRaft - Changelog</title>
<link rel="stylesheet" href="css/styles.css">
<link rel="stylesheet" href="css/changelog.css">
<link rel="icon" href="img/favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Space+Mono&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body class="theme-dark">
<div class="container">
<!-- 主卡片 -->
<div class="card">
<div class="card-header">
<h1 class="card-title" data-en="VoidRaft Changelog" data-zh="VoidRaft 更新日志">VoidRaft Changelog</h1>
<div class="card-controls">
<button id="theme-toggle" class="btn btn-secondary" title="切换主题">
<i class="fas fa-sun"></i> <span data-en="Theme" data-zh="主题">Theme</span>
</button>
<button id="lang-toggle" class="btn btn-secondary">
<i class="fas fa-language"></i> 中/EN
</button>
</div>
</div>
<div class="card-content">
<!-- 导航区域 -->
<div class="nav-links">
<a href="index.html" class="btn btn-secondary">
<i class="fas fa-home"></i> <span data-en="Home" data-zh="首页">Home</span>
</a>
<a href="https://github.com/landaiqing/voidraft" class="btn btn-secondary">
<i class="fab fa-github"></i> <span data-en="Source Code" data-zh="源代码">Source Code</span>
</a>
</div>
<!-- 加载中提示 -->
<div id="loading" class="loading-container">
<div class="loading-spinner"></div>
<p data-en="Loading releases..." data-zh="正在加载版本信息...">Loading releases...</p>
</div>
<!-- 更新日志内容 -->
<div id="changelog" class="changelog-container">
<!-- 通过JavaScript动态填充内容 -->
</div>
<!-- 错误信息 -->
<div id="error-message" class="error-container" style="display: none;">
<i class="fas fa-exclamation-triangle"></i>
<p data-en="Failed to load release information. Please try again later."
data-zh="加载版本信息失败,请稍后再试。">Failed to load release information. Please try again later.</p>
</div>
</div>
<!-- 页脚 -->
<footer class="footer">
<p class="footer-text" data-en="© 2025 VoidRaft - An elegant text snippet recording tool designed for developers" data-zh="© 2025 VoidRaft - 专为开发者打造的优雅文本片段记录工具">© 2023-2024 VoidRaft - An elegant text snippet recording tool designed for developers</p>
<div class="footer-links">
<a href="https://github.com/landaiqing/voidraft" target="_blank" class="footer-link">GitHub</a>
<a href="https://github.com/landaiqing/voidraft/issues" target="_blank" class="footer-link" data-en="Issues" data-zh="问题反馈">Issues</a>
<a href="https://github.com/landaiqing/voidraft/releases" target="_blank" class="footer-link" data-en="Releases" data-zh="版本发布">Releases</a>
</div>
</footer>
</div>
</div>
<script src="js/script.js"></script>
<script src="js/changelog.js"></script>
</body>
</html>

347
docs/css/changelog.css Normal file
View File

@@ -0,0 +1,347 @@
/* 更新日志页面样式 */
.nav-links {
margin-bottom: 30px;
display: flex;
gap: 15px;
}
.loading-container {
text-align: center;
padding: 40px 0;
background-color: transparent;
}
.loading-spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: var(--primary-color);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
.theme-dark .loading-spinner {
border-color: rgba(255, 255, 255, 0.1);
border-left-color: var(--primary-color);
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-container {
text-align: center;
color: var(--error-color);
padding: 20px;
border: 2px dashed var(--error-color);
margin: 20px 0;
border-radius: 4px;
background-color: rgba(var(--card-bg-rgb), 0.7);
}
.error-container i {
font-size: 24px;
margin-bottom: 10px;
}
/* 更新日志容器 */
.changelog-container {
display: none;
position: relative;
z-index: 1;
}
.release {
margin-bottom: 40px;
border-left: 4px solid var(--primary-color);
padding-left: 20px;
background-color: rgba(var(--card-bg-rgb), 0.5);
padding: 15px 20px;
border-radius: 4px;
}
.release-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.release-version {
font-size: 24px;
font-weight: bold;
color: var(--primary-color);
}
.release-date {
color: var(--text-color);
opacity: 0.7;
font-size: 14px;
}
.release-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 12px;
font-size: 12px;
margin-left: 10px;
background-color: var(--primary-color);
color: #000;
}
.release-badge.pre-release {
background-color: var(--warning-color);
}
.release-description {
margin-bottom: 20px;
line-height: 1.6;
}
.release-assets {
background-color: rgba(var(--light-bg-rgb), 0.7);
padding: 15px;
border-radius: 4px;
margin-top: 15px;
}
.release-assets-title {
font-size: 16px;
margin-bottom: 10px;
}
.asset-list {
list-style-type: none;
padding: 0;
margin: 0;
}
.asset-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid rgba(128, 128, 128, 0.2);
}
.asset-item:last-child {
border-bottom: none;
}
.asset-icon {
margin-right: 10px;
color: var(--accent-color);
}
.asset-name {
flex-grow: 1;
}
.asset-size {
font-size: 12px;
color: var(--text-color);
opacity: 0.7;
}
/* 资源下载按钮 */
.download-btn {
margin-left: 10px;
padding: 3px 10px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
text-decoration: none;
font-size: 12px;
transition: all 0.2s ease;
display: inline-block;
text-align: center;
}
.download-btn:hover {
background-color: var(--secondary-color);
}
.markdown-content {
line-height: 1.8;
overflow-wrap: break-word;
background-color: transparent;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3 {
margin-top: 20px;
margin-bottom: 10px;
}
.markdown-content ul,
.markdown-content ol {
padding-left: 20px;
margin: 10px 0;
}
.markdown-content li {
margin-bottom: 8px;
}
.markdown-content li:last-child {
margin-bottom: 0;
}
.markdown-content hr {
border: none;
border-top: 2px dashed var(--border-color);
margin: 20px 0;
}
.markdown-content br {
display: block;
content: "";
margin-top: 10px;
}
.markdown-content code {
font-family: 'IBM Plex Mono', monospace;
background-color: rgba(128, 128, 128, 0.1);
padding: 2px 4px;
border-radius: 3px;
font-size: 90%;
}
.markdown-content pre {
background-color: rgba(128, 128, 128, 0.1);
padding: 15px;
border-radius: 4px;
overflow-x: auto;
margin: 15px 0;
}
.markdown-content pre code {
background-color: transparent;
padding: 0;
}
.markdown-content a {
color: var(--primary-color);
text-decoration: none;
}
.markdown-content a:hover {
text-decoration: underline;
}
.data-source {
padding: 10px 15px;
margin-bottom: 20px;
background-color: rgba(var(--light-bg-rgb), 0.7);
border-radius: 4px;
font-size: 14px;
text-align: right;
opacity: 0.7;
}
.data-source a {
color: var(--primary-color);
text-decoration: none;
font-weight: bold;
}
.data-source a:hover {
text-decoration: underline;
}
/* Markdown内容样式增强 */
.markdown-content blockquote {
border-left: 4px solid var(--primary-color);
padding: 10px 15px;
margin: 15px 0;
background-color: rgba(var(--light-bg-rgb), 0.5);
border-radius: 0 4px 4px 0;
}
.markdown-content ul,
.markdown-content ol {
padding-left: 20px;
margin: 10px 0;
}
/* 移动设备响应式优化 */
@media (max-width: 768px) {
.release-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.release-assets {
padding: 12px 8px;
}
.asset-item {
flex-wrap: wrap;
padding: 12px 0;
position: relative;
}
.asset-name {
width: 100%;
margin-bottom: 8px;
word-break: break-all;
}
.asset-size {
margin-left: 25px;
}
.download-btn {
margin-left: 10px;
padding: 5px 12px;
}
}
@media (max-width: 480px) {
.release {
padding-left: 12px;
}
.asset-item {
flex-direction: column;
align-items: flex-start;
}
.asset-icon {
margin-bottom: 5px;
}
.asset-size {
margin-left: 0;
margin-top: 5px;
}
.download-btn {
margin-left: 0;
margin-top: 10px;
width: 100%;
text-align: center;
padding: 8px;
}
.markdown-content pre {
padding: 10px;
margin: 10px 0;
}
}
/* 确保日志页面页脚样式一致 */
.footer {
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.footer-text {
margin: 0 0 15px 0;
}

View File

@@ -0,0 +1,45 @@
/* cyrillic-ext */
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iIq129k.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1isq129k.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* vietnamese */
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iAq129k.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iEq129k.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1i8q1w.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

View File

@@ -0,0 +1,27 @@
/* vietnamese */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYE58RWq7.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYE98RWq7.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYEF8RQ.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

717
docs/css/styles.css Normal file
View File

@@ -0,0 +1,717 @@
@import url('./space-mono-font.css');
@import url('./ibm-plex-mono-font.css');
/* 浅色主题 */
:root {
--bg-color: #fefefe;
--text-color: #000000;
--primary-color: #F08080;
--primary-color-rgb: 240, 128, 128;
--secondary-color: #ff006e;
--accent-color: #073B4C;
--card-bg: #ffffff;
--card-bg-rgb: 255, 255, 255;
--border-color: #000000;
--light-bg: #f0f0f0;
--light-bg-rgb: 240, 240, 240;
--shadow-color: rgba(240, 128, 128, 0.5);
--success-color: #27c93f;
--warning-color: #FFD166;
--error-color: #ff006e;
--info-color: #118ab2;
--code-bg: #ffffff;
--code-bg-rgb: 255, 255, 255;
--preview-header-bg: #f0f0f0;
--preview-header-bg-rgb: 240, 240, 240;
--grid-color-1: rgba(0, 0, 0, 0.08);
--grid-color-2: rgba(0, 0, 0, 0.05);
--header-title-color: #000000;
}
/* 暗色主题变量 */
.theme-dark {
--bg-color: #121212;
--text-color: #ffffff;
--primary-color: #F08080;
--primary-color-rgb: 240, 128, 128;
--secondary-color: #ff006e;
--accent-color: #118ab2;
--card-bg: #1e1e1e;
--card-bg-rgb: 30, 30, 30;
--border-color: #ffffff;
--light-bg: #2a2a2a;
--light-bg-rgb: 42, 42, 42;
--shadow-color: rgba(240, 128, 128, 0.5);
--success-color: #27c93f;
--warning-color: #FFD166;
--error-color: #ff006e;
--info-color: #118ab2;
--code-bg: #1e1e1e;
--code-bg-rgb: 30, 30, 30;
--preview-header-bg: #252526;
--preview-header-bg-rgb: 37, 37, 38;
--grid-color-1: rgba(255, 255, 255, 0.08);
--grid-color-2: rgba(255, 255, 255, 0.05);
--header-title-color: #000000;
}
/* 主题切换和语言切换的过渡效果 */
.theme-transition,
.theme-transition *,
.lang-transition,
.lang-transition * {
transition: all 0.3s ease !important;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@keyframes gridMove {
0% {
background-position: 0px 0px, 0px 0px, 0px 0px, 0px 0px;
}
100% {
background-position: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
}
}
body {
background-color: var(--bg-color);
background-image:
linear-gradient(var(--grid-color-1) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color-1) 1px, transparent 1px),
linear-gradient(var(--grid-color-2) 0.5px, transparent 0.5px),
linear-gradient(90deg, var(--grid-color-2) 0.5px, transparent 0.5px);
background-size: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
background-position: center;
animation: gridMove 40s linear infinite;
font-family: 'Space Mono', monospace;
color: var(--text-color);
line-height: 1.6;
padding: 20px;
transition: background-color 0.3s ease, color 0.3s ease;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 卡片容器 */
.card {
background-color: var(--card-bg);
background-image:
linear-gradient(var(--grid-color-1) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color-1) 1px, transparent 1px),
linear-gradient(var(--grid-color-2) 0.5px, transparent 0.5px),
linear-gradient(90deg, var(--grid-color-2) 0.5px, transparent 0.5px);
background-size: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
background-position: center;
border: 4px solid var(--border-color);
box-shadow: 12px 12px 0 var(--shadow-color);
margin-bottom: 40px;
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease;
position: relative;
z-index: 10;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 16px 16px 0 var(--shadow-color);
}
/* 卡片头部 */
.card-header {
background-color: rgba(var(--primary-color-rgb), 0.9);
border-bottom: 4px solid var(--border-color);
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 1;
}
.card-title {
font-size: 24px;
font-weight: bold;
margin: 0;
color: var(--header-title-color);
}
.card-controls {
display: flex;
gap: 10px;
}
.btn {
display: inline-block;
padding: 10px 20px;
background: var(--secondary-color);
color: #fff;
text-decoration: none;
font-weight: bold;
border: 3px solid var(--border-color);
box-shadow: 4px 4px 0 var(--shadow-color);
transition: all 0.2s ease;
cursor: pointer;
font-family: 'Space Mono', monospace;
font-size: 14px;
}
.btn:hover {
background: var(--card-bg);
color: var(--primary-color);
border: 3px solid var(--primary-color);
box-shadow: none;
}
.btn-secondary {
background: var(--light-bg);
color: var(--text-color);
}
.btn-secondary:hover {
background: var(--card-bg);
color: var(--primary-color);
border: 3px solid var(--primary-color);
}
/* 卡片内容 */
.card-content {
padding: 30px;
position: relative;
z-index: 1;
background-color: rgba(var(--card-bg-rgb), 0.5);
}
/* Logo区域 */
.logo-container {
text-align: center;
margin-bottom: 40px;
}
.logo-frame {
width: 150px;
height: 150px;
background: var(--card-bg);
border: 4px solid var(--border-color);
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
.logo-image {
width: 130px;
height: 130px;
object-fit: contain;
border: 2px solid var(--border-color);
}
.logo-text {
font-size: 32px;
font-weight: bold;
margin: 0;
}
.tagline {
font-size: 16px;
margin: 10px 0 0;
color: var(--accent-color);
}
/* 介绍区域 */
.intro-box {
border: 2px dashed var(--border-color);
padding: 20px;
background-color: rgba(var(--light-bg-rgb), 0.7);
margin-bottom: 30px;
text-align: center;
}
.intro-text {
font-size: 16px;
margin-bottom: 0;
}
/* 按钮组 */
.button-group {
display: flex;
justify-content: center;
gap: 20px;
margin: 30px 0;
}
/* 特性网格 */
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 30px;
margin: 40px 0;
}
/* 特性卡片 */
.feature-card {
background-color: rgba(var(--card-bg-rgb), 0.8);
border: 3px solid var(--border-color);
box-shadow: 5px 5px 0 var(--shadow-color);
padding: 20px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.feature-card:hover {
transform: translateY(-3px);
box-shadow: 7px 7px 0 var(--shadow-color);
}
.feature-icon {
font-size: 24px;
margin-bottom: 15px;
color: var(--secondary-color);
}
.feature-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
}
.feature-desc {
font-size: 14px;
}
/* 预览区域 */
.preview-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin: 30px 0;
}
@media (max-width: 768px) {
.preview-container {
grid-template-columns: 1fr;
}
}
/* 预览窗口 */
.preview-window {
border: 3px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
margin: 10px;
flex: 1;
min-width: 300px;
background-color: rgba(var(--card-bg-rgb), 0.7);
display: flex;
flex-direction: column;
box-shadow: 5px 5px 0 var(--shadow-color);
}
/* 预览头部 */
.preview-header {
background-color: rgba(var(--preview-header-bg-rgb), 0.9);
padding: 10px;
display: flex;
align-items: center;
border-bottom: 2px solid var(--border-color);
}
.preview-controls {
display: flex;
gap: 6px;
margin-right: 15px;
}
.preview-btn {
width: 12px;
height: 12px;
border-radius: 50%;
border: 0.5px solid rgba(0, 0, 0, 0.1);
}
.preview-btn:nth-child(1) {
background-color: #ff5f56;
}
.preview-btn:nth-child(2) {
background-color: #ffbd2e;
}
.preview-btn:nth-child(3) {
background-color: #27c93f;
}
.preview-title {
font-size: 13px;
opacity: 0.8;
color: var(--text-color);
font-weight: normal;
}
/* 预览内容 */
.preview-content {
padding: 15px;
flex-grow: 1;
overflow: auto;
background-color: rgba(var(--code-bg-rgb), 0.5);
}
/* 代码块容器 */
.code-block-wrapper {
background-color: rgba(var(--code-bg-rgb), 0.8);
border: 2px solid var(--border-color);
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
/* 块头部 */
.block-header {
background-color: rgba(var(--light-bg-rgb), 0.8);
padding: 8px 12px;
border-bottom: 2px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.block-language {
color: rgba(128, 128, 128, 0.8);
font-family: 'IBM Plex Mono', monospace;
display: flex;
align-items: center;
}
.block-language::before {
content: '';
display: inline-block;
width: 12px;
height: 12px;
margin-right: 5px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23888'%3E%3Cpath d='M9.7,16.7L5.3,12.3C4.9,11.9 4.9,11.1 5.3,10.7C5.7,10.3 6.3,10.3 6.7,10.7L10.5,14.5L17.3,7.7C17.7,7.3 18.3,7.3 18.7,7.7C19.1,8.1 19.1,8.7 18.7,9.1L11.3,16.7C10.9,17.1 10.1,17.1 9.7,16.7Z'/%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
}
.code-block {
font-family: 'IBM Plex Mono', monospace;
font-size: 13px;
line-height: 1.6;
margin: 0;
white-space: pre;
tab-size: 4;
-moz-tab-size: 4;
padding: 10px;
}
.theme-dark .code-block-wrapper {
border-color: rgba(255, 255, 255, 0.15);
}
.theme-dark .block-header {
background-color: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.15);
}
.theme-dark .block-language {
color: rgba(255, 255, 255, 0.6);
}
.theme-dark .block-language::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23aaa'%3E%3Cpath d='M9.7,16.7L5.3,12.3C4.9,11.9 4.9,11.1 5.3,10.7C5.7,10.3 6.3,10.3 6.7,10.7L10.5,14.5L17.3,7.7C17.7,7.3 18.3,7.3 18.7,7.7C19.1,8.1 19.1,8.7 18.7,9.1L11.3,16.7C10.9,17.1 10.1,17.1 9.7,16.7Z'/%3E%3C/svg%3E");
}
.theme-dark .code-block {
color: #d4d4d4;
}
/* 代码高亮 */
.theme-dark .keyword { color: #c586c0; }
.theme-dark .function { color: #dcdcaa; }
.theme-dark .variable { color: #9cdcfe; }
.theme-dark .string { color: #ce9178; }
.theme-dark .comment { color: #6a9955; }
.theme-dark .class { color: #4ec9b0; }
.theme-dark .parameter { color: #9cdcfe; }
.theme-dark .built-in { color: #4ec9b0; }
/* 浅色主题代码高亮 */
.keyword { color: #af00db; }
.function { color: #795e26; }
.variable { color: #001080; }
.string { color: #a31515; }
.comment { color: #008000; }
.class { color: #267f99; }
.parameter { color: #001080; }
.built-in { color: #267f99; }
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
border: none;
transition: opacity 0.3s ease;
}
.theme-dark .light-theme-img {
display: none !important;
}
.theme-dark .dark-theme-img {
display: block;
}
body:not(.theme-dark) .dark-theme-img {
display: none !important;
}
body:not(.theme-dark) .light-theme-img {
display: block !important;
}
/* 技术栈列表 */
.tech-list {
list-style: none;
padding: 0;
margin: 0;
}
/* 技术栈列表 */
.tech-item {
padding: 15px;
margin-bottom: 15px;
border: 2px solid var(--border-color);
background-color: rgba(var(--light-bg-rgb), 0.7);
display: flex;
align-items: center;
}
.tech-icon {
margin-right: 15px;
color: var(--secondary-color);
font-size: 20px;
width: 30px;
text-align: center;
}
.tech-name {
font-weight: bold;
margin-right: 10px;
}
.tech-desc {
font-size: 14px;
color: var(--accent-color);
}
/* 页脚 */
.footer {
border-top: 2px solid var(--border-color);
padding: 20px 0;
margin-top: 40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
background-color: transparent;
position: relative;
z-index: 1;
}
.footer-text {
margin: 0 0 15px 0;
font-size: 14px;
opacity: 0.7;
}
.footer-links {
display: flex;
gap: 15px;
justify-content: center;
}
.footer-link {
color: var(--secondary-color);
text-decoration: none;
font-size: 14px;
transition: color 0.3s;
}
.footer-link:hover {
color: var(--primary-color);
text-decoration: underline;
}
/* 响应式设计 */
@media (max-width: 768px) {
.button-group {
flex-direction: column;
align-items: center;
}
.btn {
width: 100%;
text-align: center;
}
.features-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.card-header {
flex-direction: column;
gap: 15px;
}
.card-controls {
width: 100%;
}
.logo-frame {
width: 120px;
height: 120px;
}
.logo-image {
width: 100px;
height: 100px;
}
}
/* 针对移动设备的响应式优化 */
@media (max-width: 768px) {
body {
padding: 10px;
}
.container {
padding: 10px;
}
.card {
margin-bottom: 30px;
}
.card-header {
flex-direction: column;
gap: 15px;
text-align: center;
}
.card-controls {
width: 100%;
justify-content: center;
}
.button-group {
flex-wrap: wrap;
gap: 15px;
}
/* 预览区域优化 */
.preview-content {
max-width: 100%;
overflow-x: auto;
}
.code-block {
white-space: pre-wrap;
word-break: break-word;
font-size: 13px;
line-height: 1.4;
}
.block-header {
padding: 6px 10px;
}
/* 日志界面导航链接优化 */
.nav-links {
flex-direction: column;
gap: 10px;
align-items: stretch;
}
.nav-links .btn {
width: 100%;
text-align: center;
}
}
@media (max-width: 480px) {
/* 特性卡片优化 */
.features-grid {
grid-template-columns: 1fr;
gap: 20px;
}
/* 预览窗口优化 */
.preview-container {
flex-direction: column;
}
.preview-window {
margin-bottom: 20px;
width: 100%;
}
/* 技术栈列表小屏幕优化 */
.tech-item {
flex-wrap: wrap;
}
.tech-desc {
width: 100%;
padding-left: 40px; /* 图标宽度+右边距 */
margin-top: 5px;
}
/* 日志界面资源列表项优化 */
.asset-item {
flex-wrap: wrap;
padding: 15px 0;
}
.asset-name {
width: 100%;
word-break: break-all;
margin-bottom: 10px;
}
.asset-size {
order: 2;
margin-top: 10px;
}
.download-btn {
order: 3;
margin-left: 0;
margin-top: 10px;
width: 100%;
text-align: center;
padding: 8px;
}
/* 页脚链接优化 */
.footer {
flex-direction: column;
text-align: center;
}
.footer-links {
margin-top: 15px;
justify-content: center;
}
}

Binary file not shown.

BIN
docs/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
docs/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

256
docs/index.html Normal file
View File

@@ -0,0 +1,256 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VoidRaft - An elegant text snippet recording tool designed for developers.</title>
<meta name="description" content="VoidRaft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
<meta name="keywords" content="text editor, code snippets, developer tools, syntax highlighting, code formatting, multi-language, VoidRaft">
<meta name="author" content="VoidRaft Team">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://landaiqing.github.io/voidraft/">
<!-- Internationalization / hreflang -->
<link rel="alternate" hreflang="en" href="https://landaiqing.github.io/voidraft/">
<link rel="alternate" hreflang="zh" href="https://landaiqing.github.io/voidraft/?lang=zh">
<link rel="alternate" hreflang="x-default" href="https://landaiqing.github.io/voidraft/">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://landaiqing.github.io/voidraft/">
<meta property="og:title" content="VoidRaft - An elegant text snippet recording tool designed for developers">
<meta property="og:description" content="VoidRaft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
<meta property="og:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
<meta property="og:site_name" content="VoidRaft">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://landaiqing.github.io/voidraft/">
<meta property="twitter:title" content="VoidRaft - An elegant text snippet recording tool designed for developers">
<meta property="twitter:description" content="VoidRaft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
<meta property="twitter:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
<link rel="stylesheet" href="./css/styles.css">
<link rel="icon" href="./img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "VoidRaft",
"description": "An elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.",
"url": "https://landaiqing.github.io/voidraft/",
"downloadUrl": "https://github.com/landaiqing/voidraft/releases",
"author": {
"@type": "Organization",
"name": "VoidRaft"
},
"operatingSystem": ["Windows", "macOS", "Linux"],
"applicationCategory": "DeveloperApplication",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"screenshot": "https://landaiqing.github.io/voidraft/img/screenshot-dark.png",
"softwareVersion": "Latest",
"programmingLanguage": ["Go", "TypeScript", "Vue.js"],
"codeRepository": "https://github.com/landaiqing/voidraft"
}
</script>
</head>
<body class="theme-dark">
<div class="container">
<!-- 主卡片 -->
<div class="card">
<div class="card-header">
<h1 class="card-title">VoidRaft</h1>
<div class="card-controls">
<button id="theme-toggle" class="btn btn-secondary" title="切换主题">
<i class="fas fa-sun"></i> <span data-en="Theme" data-zh="主题">Theme</span>
</button>
<button id="lang-toggle" class="btn btn-secondary">
<i class="fas fa-language"></i> 中/EN
</button>
</div>
</div>
<div class="card-content">
<!-- Logo和介绍 -->
<div class="logo-container">
<div class="logo-frame">
<img src="img/logo.png" alt="VoidRaft Logo" class="logo-image">
</div>
<h2 class="logo-text" data-en="VoidRaft" data-zh="VoidRaft">VoidRaft</h2>
<p class="tagline" data-en="An elegant text snippet recording tool" data-zh="优雅的文本片段记录工具">An elegant text snippet recording tool</p>
</div>
<div class="intro-box">
<p class="intro-text" data-en="Designed for developers to record, organize, and manage various text snippets anytime, anywhere." data-zh="专为开发者打造,随时随地记录、整理和管理各种文本片段。">Designed for developers to record, organize, and manage various text snippets anytime, anywhere.</p>
</div>
<div class="button-group">
<a href="https://github.com/landaiqing/voidraft/releases" class="btn" data-en="Download" data-zh="下载">
<i class="fas fa-download"></i> Download
</a>
<a href="https://github.com/landaiqing/voidraft" class="btn btn-secondary" data-en="Source Code" data-zh="源代码">
<i class="fab fa-github"></i> Source Code
</a>
<a href="changelog.html" class="btn btn-secondary" data-en="Changelog" data-zh="更新日志">
<i class="fas fa-history"></i> Changelog
</a>
</div>
<!-- 特性部分 -->
<h2 data-en="Core Features" data-zh="核心特性">Core Features</h2>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-code"></i>
</div>
<h3 class="feature-title" data-en="Developer-Friendly" data-zh="开发者友好">Developer-Friendly</h3>
<p class="feature-desc" data-en="Multi-language code blocks with syntax highlighting for 30+ programming languages" data-zh="多语言代码块支持为30+种编程语言提供语法高亮">Multi-language code blocks with syntax highlighting for 30+ programming languages</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-magic"></i>
</div>
<h3 class="feature-title" data-en="Code Formatting" data-zh="代码格式化">Code Formatting</h3>
<p class="feature-desc" data-en="Built-in Prettier support for one-click code beautification" data-zh="内置Prettier支持一键美化代码">Built-in Prettier support for one-click code beautification</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-palette"></i>
</div>
<h3 class="feature-title" data-en="Custom Themes" data-zh="自定义主题">Custom Themes</h3>
<p class="feature-desc" data-en="Dark/Light themes with full customization options" data-zh="深色/浅色主题,支持完全自定义">Dark/Light themes with full customization options</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-clone"></i>
</div>
<h3 class="feature-title" data-en="Multi-Window" data-zh="多窗口支持">Multi-Window</h3>
<p class="feature-desc" data-en="Edit multiple documents simultaneously" data-zh="同时编辑多个文档">Edit multiple documents simultaneously</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-layer-group"></i>
</div>
<h3 class="feature-title" data-en="Block Editing" data-zh="块状编辑">Block Editing</h3>
<p class="feature-desc" data-en="Split content into independent code blocks with different language settings" data-zh="将内容分割为独立的代码块,每个块可设置不同语言">Split content into independent code blocks with different language settings</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-puzzle-piece"></i>
</div>
<h3 class="feature-title" data-en="Extensions" data-zh="丰富扩展">Extensions</h3>
<p class="feature-desc" data-en="Rainbow brackets, VSCode-style search, color picker, translation tool, and more" data-zh="彩虹括号、VSCode风格搜索、颜色选择器、翻译工具等多种扩展">Rainbow brackets, VSCode-style search, color picker, translation tool, and more</p>
</div>
</div>
<!-- 预览部分 -->
<h2 data-en="Preview" data-zh="预览">Preview</h2>
<div class="preview-container">
<div class="preview-window">
<div class="preview-header">
<div class="preview-controls">
<span class="preview-btn"></span>
<span class="preview-btn"></span>
<span class="preview-btn"></span>
</div>
<div class="preview-title">voidraft</div>
</div>
<div class="preview-content">
<div class="code-block-wrapper">
<div class="block-header">
<div class="block-language">javascript</div>
</div>
<pre class="code-block">
<span class="keyword">function</span> <span class="function">createDocument</span>() {
<span class="keyword">const</span> <span class="variable">doc</span> = <span class="keyword">new</span> <span class="class">Document</span>();
<span class="variable">doc</span>.<span class="function">addCodeBlock</span>(<span class="string">'javascript'</span>, <span class="string">`
<span class="keyword">function</span> <span class="function">greeting</span>(<span class="parameter">name</span>) {
<span class="keyword">return</span> <span class="string">`Hello, </span>${<span class="parameter">name</span>}<span class="string">!`</span>;
}
<span class="built-in">console</span>.<span class="function">log</span>(<span class="function">greeting</span>(<span class="string">'World'</span>));
`</span>);
<span class="keyword">return</span> <span class="variable">doc</span>;
}</pre>
</div>
<div class="code-block-wrapper" style="margin-top: 10px;">
<div class="block-header">
<div class="block-language">text</div>
</div>
<pre class="code-block">
<span class="comment">// VoidRaft - An elegant text snippet recording tool</span>
<span class="comment">// Multi-language support | Code formatting | Custom themes</span>
<span class="comment">// A modern text editor designed for developers</span></pre>
</div>
</div>
</div>
<div class="preview-window">
<img src="img/screenshot-dark.png" alt="VoidRaft 界面预览" class="preview-image dark-theme-img">
<img src="img/screenshot-light.png" alt="VoidRaft 界面预览" class="preview-image light-theme-img" style="display: none;">
</div>
</div>
<!-- 技术栈部分 -->
<h2 data-en="Technical Stack" data-zh="技术栈">Technical Stack</h2>
<ul class="tech-list">
<li class="tech-item">
<div class="tech-icon"><i class="fas fa-desktop"></i></div>
<span class="tech-name">Wails3</span>
<span class="tech-desc" data-en="Cross-platform desktop application framework" data-zh="跨平台桌面应用框架">Cross-platform desktop application framework</span>
</li>
<li class="tech-item">
<div class="tech-icon"><i class="fas fa-cogs"></i></div>
<span class="tech-name">Go 1.21+</span>
<span class="tech-desc" data-en="Fast and efficient backend language" data-zh="快速高效的后端语言">Fast and efficient backend language</span>
</li>
<li class="tech-item">
<div class="tech-icon"><i class="fab fa-vuejs"></i></div>
<span class="tech-name">Vue 3 + TypeScript</span>
<span class="tech-desc" data-en="Modern frontend framework" data-zh="现代化前端框架">Modern frontend framework</span>
</li>
<li class="tech-item">
<div class="tech-icon"><i class="fas fa-edit"></i></div>
<span class="tech-name">CodeMirror 6</span>
<span class="tech-desc" data-en="Modern code editor with extension support" data-zh="支持扩展的现代化代码编辑器">Modern code editor with extension support</span>
</li>
<li class="tech-item">
<div class="tech-icon"><i class="fas fa-database"></i></div>
<span class="tech-name">SQLite</span>
<span class="tech-desc" data-en="Lightweight database for document storage" data-zh="轻量级文档存储数据库">Lightweight database for document storage</span>
</li>
</ul>
</div>
<!-- 页脚 -->
<footer class="footer">
<p class="footer-text" data-en="© 2025 VoidRaft - An elegant text snippet recording tool designed for developers" data-zh="© 2025 VoidRaft - 专为开发者打造的优雅文本片段记录工具">© 2025 VoidRaft - An elegant text snippet recording tool designed for developers</p>
<div class="footer-links">
<a href="https://github.com/landaiqing/voidraft" target="_blank" class="footer-link">GitHub</a>
<a href="https://github.com/landaiqing/voidraft/issues" target="_blank" class="footer-link" data-en="Issues" data-zh="问题反馈">Issues</a>
<a href="https://github.com/landaiqing/voidraft/releases" target="_blank" class="footer-link" data-en="Releases" data-zh="版本发布">Releases</a>
</div>
</footer>
</div>
</div>
<script src="js/script.js"></script>
</body>
</html>

705
docs/js/changelog.js Normal file
View File

@@ -0,0 +1,705 @@
/**
* VoidRaft - Changelog Script
* 从GitHub API获取发布信息支持Gitea备用源
*/
/**
* 仓库配置类
*/
class RepositoryConfig {
constructor() {
this.repos = {
github: {
owner: 'landaiqing',
name: 'voidraft',
apiUrl: 'https://api.github.com/repos/landaiqing/voidraft/releases',
releasesUrl: 'https://github.com/landaiqing/voidraft/releases'
},
gitea: {
owner: 'landaiqing',
name: 'voidraft',
domain: 'git.landaiqing.cn',
apiUrl: 'https://git.landaiqing.cn/api/v1/repos/landaiqing/voidraft/releases',
releasesUrl: 'https://git.landaiqing.cn/landaiqing/voidraft/releases'
}
};
}
/**
* 获取仓库配置
* @param {string} source - 'github' 或 'gitea'
*/
getRepo(source) {
return this.repos[source];
}
/**
* 获取所有仓库配置
*/
getAllRepos() {
return this.repos;
}
}
/**
* 国际化消息管理类
*/
class I18nMessages {
constructor() {
this.messages = {
loading: {
en: 'Loading releases...',
zh: '正在加载版本信息...'
},
noReleases: {
en: 'No release information found',
zh: '没有找到版本发布信息'
},
fetchError: {
en: 'Failed to load release information. Please try again later.',
zh: '无法获取版本信息,请稍后再试'
},
githubApiError: {
en: 'GitHub API returned an error status: ',
zh: 'GitHub API返回错误状态: '
},
giteaApiError: {
en: 'Gitea API returned an error status: ',
zh: 'Gitea API返回错误状态: '
},
dataSource: {
en: 'Data source: ',
zh: '数据来源: '
},
downloads: {
en: 'Downloads',
zh: '下载资源'
},
download: {
en: 'Download',
zh: '下载'
},
preRelease: {
en: 'Pre-release',
zh: '预发布'
}
};
}
/**
* 获取消息
* @param {string} key - 消息键
* @param {string} lang - 语言代码
*/
getMessage(key, lang = 'en') {
return this.messages[key] && this.messages[key][lang] || this.messages[key]['en'] || '';
}
/**
* 获取当前语言
*/
getCurrentLang() {
return window.currentLang || 'en';
}
}
/**
* API客户端类
*/
class APIClient {
constructor(repositoryConfig, i18nMessages) {
this.repositoryConfig = repositoryConfig;
this.i18nMessages = i18nMessages;
}
/**
* 从指定源获取发布信息
* @param {string} source - 'github' 或 'gitea'
*/
async fetchReleases(source) {
const repo = this.repositoryConfig.getRepo(source);
const errorMessageKey = source === 'github' ? 'githubApiError' : 'giteaApiError';
const options = {
headers: { 'Accept': 'application/json' }
};
if (source === 'github') {
return this.fetchFromGitHub(repo, options, errorMessageKey);
} else {
return this.fetchFromGitea(repo, options, errorMessageKey);
}
}
/**
* 从GitHub获取数据
* @param {Object} repo - 仓库配置
* @param {Object} options - 请求选项
* @param {string} errorMessageKey - 错误消息键
*/
async fetchFromGitHub(repo, options, errorMessageKey) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
options.signal = controller.signal;
options.headers['Accept'] = 'application/vnd.github.v3+json';
try {
const response = await fetch(repo.apiUrl, options);
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`${this.i18nMessages.getMessage(errorMessageKey, this.i18nMessages.getCurrentLang())}${response.status}`);
}
const releases = await response.json();
if (!releases || releases.length === 0) {
throw new Error(this.i18nMessages.getMessage('noReleases', this.i18nMessages.getCurrentLang()));
}
return releases;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
/**
* 从Gitea获取数据
* @param {Object} repo - 仓库配置
* @param {Object} options - 请求选项
* @param {string} errorMessageKey - 错误消息键
*/
async fetchFromGitea(repo, options, errorMessageKey) {
const response = await fetch(repo.apiUrl, options);
if (!response.ok) {
throw new Error(`${this.i18nMessages.getMessage(errorMessageKey, this.i18nMessages.getCurrentLang())}${response.status}`);
}
const releases = await response.json();
if (!releases || releases.length === 0) {
throw new Error(this.i18nMessages.getMessage('noReleases', this.i18nMessages.getCurrentLang()));
}
return releases;
}
}
/**
* UI管理类
*/
class UIManager {
constructor(i18nMessages) {
this.i18nMessages = i18nMessages;
this.elements = {
loading: document.getElementById('loading'),
changelog: document.getElementById('changelog'),
error: document.getElementById('error-message')
};
}
/**
* 显示加载状态
*/
showLoading() {
this.elements.loading.style.display = 'block';
this.elements.error.style.display = 'none';
this.elements.changelog.innerHTML = '';
}
/**
* 隐藏加载状态
*/
hideLoading() {
this.elements.loading.style.display = 'none';
}
/**
* 显示错误消息
* @param {string} message - 错误消息
*/
showError(message) {
const errorMessageElement = this.elements.error.querySelector('p');
if (errorMessageElement) {
errorMessageElement.textContent = message;
} else {
this.elements.error.textContent = message;
}
this.elements.error.style.display = 'block';
this.hideLoading();
}
/**
* 显示发布信息
* @param {Array} releases - 发布信息数组
* @param {string} source - 数据源
*/
displayReleases(releases, source) {
this.hideLoading();
// 清除现有内容
this.elements.changelog.innerHTML = '';
// 创建数据源元素
const sourceElement = this.createSourceElement(source);
this.elements.changelog.appendChild(sourceElement);
// 创建发布信息元素
releases.forEach(release => {
const releaseElement = this.createReleaseElement(release, source);
this.elements.changelog.appendChild(releaseElement);
});
this.elements.changelog.style.display = 'block';
}
/**
* 创建数据源元素
* @param {string} source - 数据源
*/
createSourceElement(source) {
const sourceElement = document.createElement('div');
sourceElement.className = 'data-source';
// 创建带有国际化支持的源标签
const sourceLabel = document.createElement('span');
sourceLabel.setAttribute('data-en', this.i18nMessages.getMessage('dataSource', 'en'));
sourceLabel.setAttribute('data-zh', this.i18nMessages.getMessage('dataSource', 'zh'));
sourceLabel.textContent = this.i18nMessages.getMessage('dataSource', this.i18nMessages.getCurrentLang());
// 创建链接
const sourceLink = document.createElement('a');
const repositoryConfig = new RepositoryConfig();
sourceLink.href = repositoryConfig.getRepo(source).releasesUrl;
sourceLink.textContent = source === 'github' ? 'GitHub' : 'Gitea';
sourceLink.target = '_blank';
// 组装元素
sourceElement.appendChild(sourceLabel);
sourceElement.appendChild(sourceLink);
return sourceElement;
}
/**
* 创建发布信息元素
* @param {Object} release - 发布信息对象
* @param {string} source - 数据源
*/
createReleaseElement(release, source) {
const releaseElement = document.createElement('div');
releaseElement.className = 'release';
// 格式化发布日期
const releaseDate = new Date(release.published_at || release.created_at);
const formattedDate = DateFormatter.formatDate(releaseDate);
// 创建头部
const headerElement = this.createReleaseHeader(release, formattedDate);
releaseElement.appendChild(headerElement);
// 添加发布说明
if (release.body) {
const descriptionElement = document.createElement('div');
descriptionElement.className = 'release-description markdown-content';
descriptionElement.innerHTML = MarkdownParser.parseMarkdown(release.body);
releaseElement.appendChild(descriptionElement);
}
// 添加下载资源
const assets = AssetManager.getAssetsFromRelease(release, source);
if (assets && assets.length > 0) {
const assetsElement = this.createAssetsElement(assets);
releaseElement.appendChild(assetsElement);
}
return releaseElement;
}
/**
* 创建发布信息头部
*/
createReleaseHeader(release, formattedDate) {
const headerElement = document.createElement('div');
headerElement.className = 'release-header';
// 版本元素
const versionElement = document.createElement('div');
versionElement.className = 'release-version';
// 版本文本
const versionText = document.createElement('span');
versionText.textContent = release.name || release.tag_name;
versionElement.appendChild(versionText);
// 预发布标记
if (release.prerelease) {
const preReleaseTag = document.createElement('span');
preReleaseTag.className = 'release-badge pre-release';
preReleaseTag.setAttribute('data-en', this.i18nMessages.getMessage('preRelease', 'en'));
preReleaseTag.setAttribute('data-zh', this.i18nMessages.getMessage('preRelease', 'zh'));
preReleaseTag.textContent = this.i18nMessages.getMessage('preRelease', this.i18nMessages.getCurrentLang());
versionElement.appendChild(preReleaseTag);
}
// 日期元素
const dateElement = document.createElement('div');
dateElement.className = 'release-date';
dateElement.textContent = formattedDate;
headerElement.appendChild(versionElement);
headerElement.appendChild(dateElement);
return headerElement;
}
/**
* 创建资源文件元素
* @param {Array} assets - 资源文件数组
*/
createAssetsElement(assets) {
const assetsElement = document.createElement('div');
assetsElement.className = 'release-assets';
// 资源标题
const assetsTitle = document.createElement('div');
assetsTitle.className = 'release-assets-title';
assetsTitle.setAttribute('data-en', this.i18nMessages.getMessage('downloads', 'en'));
assetsTitle.setAttribute('data-zh', this.i18nMessages.getMessage('downloads', 'zh'));
assetsTitle.textContent = this.i18nMessages.getMessage('downloads', this.i18nMessages.getCurrentLang());
// 资源列表
const assetList = document.createElement('ul');
assetList.className = 'asset-list';
// 添加每个资源
assets.forEach(asset => {
const assetItem = this.createAssetItem(asset);
assetList.appendChild(assetItem);
});
assetsElement.appendChild(assetsTitle);
assetsElement.appendChild(assetList);
return assetsElement;
}
/**
* 创建资源文件项
* @param {Object} asset - 资源文件对象
*/
createAssetItem(asset) {
const assetItem = document.createElement('li');
assetItem.className = 'asset-item';
// 文件图标
const iconElement = document.createElement('i');
iconElement.className = `asset-icon fas fa-${FileIconHelper.getFileIcon(asset.name)}`;
// 文件名
const nameElement = document.createElement('span');
nameElement.className = 'asset-name';
nameElement.textContent = asset.name;
// 文件大小
const sizeElement = document.createElement('span');
sizeElement.className = 'asset-size';
sizeElement.textContent = FileSizeFormatter.formatFileSize(asset.size);
// 下载链接
const downloadLink = document.createElement('a');
downloadLink.className = 'download-btn';
downloadLink.href = asset.browser_download_url;
downloadLink.target = '_blank';
downloadLink.setAttribute('data-en', this.i18nMessages.getMessage('download', 'en'));
downloadLink.setAttribute('data-zh', this.i18nMessages.getMessage('download', 'zh'));
downloadLink.textContent = this.i18nMessages.getMessage('download', this.i18nMessages.getCurrentLang());
// 组装资源项
assetItem.appendChild(iconElement);
assetItem.appendChild(nameElement);
assetItem.appendChild(sizeElement);
assetItem.appendChild(downloadLink);
return assetItem;
}
}
/**
* 资源管理器类
*/
class AssetManager {
/**
* 从发布信息中获取资源文件
* @param {Object} release - 发布信息对象
* @param {string} source - 数据源
*/
static getAssetsFromRelease(release, source) {
let assets = [];
if (source === 'github') {
assets = release.assets || [];
} else { // Gitea
assets = release.assets || [];
// 检查Gitea特定的资源结构
if (!assets.length && release.attachments) {
assets = release.attachments.map(attachment => ({
name: attachment.name,
size: attachment.size,
browser_download_url: attachment.browser_download_url
}));
}
}
return assets;
}
}
/**
* 文件图标助手类
*/
class FileIconHelper {
/**
* 根据文件扩展名获取图标
* @param {string} filename - 文件名
*/
static getFileIcon(filename) {
const extension = filename.split('.').pop().toLowerCase();
const iconMap = {
'exe': 'download',
'msi': 'download',
'dmg': 'download',
'pkg': 'download',
'deb': 'download',
'rpm': 'download',
'tar': 'file-archive',
'gz': 'file-archive',
'zip': 'file-archive',
'7z': 'file-archive',
'rar': 'file-archive',
'pdf': 'file-pdf',
'txt': 'file-alt',
'md': 'file-alt',
'json': 'file-code',
'xml': 'file-code',
'yml': 'file-code',
'yaml': 'file-code'
};
return iconMap[extension] || 'file';
}
}
/**
* 文件大小格式化器类
*/
class FileSizeFormatter {
/**
* 格式化文件大小
* @param {number} bytes - 字节数
*/
static formatFileSize(bytes) {
if (!bytes) return '';
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
}
/**
* 日期格式化器类
*/
class DateFormatter {
/**
* 格式化日期
* @param {Date} date - 日期对象
*/
static formatDate(date) {
const options = {
year: 'numeric',
month: 'long',
day: 'numeric'
};
const lang = window.currentLang || 'en';
const locale = lang === 'zh' ? 'zh-CN' : 'en-US';
return date.toLocaleDateString(locale, options);
}
}
/**
* Markdown解析器类
*/
class MarkdownParser {
/**
* 简单的Markdown解析
* @param {string} markdown - Markdown文本
*/
static parseMarkdown(markdown) {
if (!markdown) return '';
// 预处理:保留原始换行符,用特殊标记替换
const preservedLineBreaks = '___LINE_BREAK___';
markdown = markdown.replace(/\n/g, preservedLineBreaks);
// 引用块 - > text
markdown = markdown.replace(/&gt;\s*(.*?)(?=&gt;|$)/g, '<blockquote>$1</blockquote>');
markdown = markdown.replace(/>\s*(.*?)(?=>|$)/g, '<blockquote>$1</blockquote>');
// 链接 - [text](url)
markdown = markdown.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
// 标题 - # Heading
markdown = markdown.replace(/^### (.*?)(?=___LINE_BREAK___|$)/gm, '<h3>$1</h3>');
markdown = markdown.replace(/^## (.*?)(?=___LINE_BREAK___|$)/gm, '<h2>$1</h2>');
markdown = markdown.replace(/^# (.*?)(?=___LINE_BREAK___|$)/gm, '<h1>$1</h1>');
// 粗体 - **text**
markdown = markdown.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// 斜体 - *text*
markdown = markdown.replace(/\*(.*?)\*/g, '<em>$1</em>');
// 代码块 - ```code```
markdown = markdown.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
// 行内代码 - `code`
markdown = markdown.replace(/`([^`]+)`/g, '<code>$1</code>');
// 处理列表项
// 先将每个列表项转换为HTML
markdown = markdown.replace(/- (.*?)(?=___LINE_BREAK___- |___LINE_BREAK___$|$)/g, '<li>$1</li>');
markdown = markdown.replace(/\* (.*?)(?=___LINE_BREAK___\* |___LINE_BREAK___$|$)/g, '<li>$1</li>');
markdown = markdown.replace(/\d+\. (.*?)(?=___LINE_BREAK___\d+\. |___LINE_BREAK___$|$)/g, '<li>$1</li>');
// 然后将连续的列表项包装在ul或ol中
const listItemRegex = /<li>.*?<\/li>/g;
const listItems = markdown.match(listItemRegex) || [];
if (listItems.length > 0) {
// 将连续的列表项组合在一起
let lastIndex = 0;
let result = '';
let inList = false;
listItems.forEach(item => {
const itemIndex = markdown.indexOf(item, lastIndex);
// 添加列表项之前的内容
if (itemIndex > lastIndex) {
result += markdown.substring(lastIndex, itemIndex);
}
// 如果不在列表中,开始一个新列表
if (!inList) {
result += '<ul>';
inList = true;
}
// 添加列表项
result += item;
// 更新lastIndex
lastIndex = itemIndex + item.length;
// 检查下一个内容是否是列表项
const nextItemIndex = markdown.indexOf('<li>', lastIndex);
if (nextItemIndex === -1 || nextItemIndex > lastIndex + 20) { // 如果下一个列表项不紧邻
result += '</ul>';
inList = false;
}
});
// 添加剩余内容
if (lastIndex < markdown.length) {
result += markdown.substring(lastIndex);
}
markdown = result;
}
// 处理水平分隔线
markdown = markdown.replace(/---/g, '<hr>');
// 恢复换行符
markdown = markdown.replace(/___LINE_BREAK___/g, '<br>');
// 处理段落
markdown = markdown.replace(/<br><br>/g, '</p><p>');
// 包装在段落标签中
if (!markdown.startsWith('<p>')) {
markdown = `<p>${markdown}</p>`;
}
return markdown;
}
}
/**
* 更新日志主应用类
*/
class ChangelogApp {
constructor() {
this.repositoryConfig = new RepositoryConfig();
this.i18nMessages = new I18nMessages();
this.apiClient = new APIClient(this.repositoryConfig, this.i18nMessages);
this.uiManager = new UIManager(this.i18nMessages);
this.init();
}
/**
* 初始化应用
*/
init() {
this.uiManager.showLoading();
// 首先尝试GitHub API
this.apiClient.fetchReleases('github')
.then(releases => {
this.uiManager.displayReleases(releases, 'github');
})
.catch(() => {
// GitHub失败时尝试Gitea
return this.apiClient.fetchReleases('gitea')
.then(releases => {
this.uiManager.displayReleases(releases, 'gitea');
});
})
.catch(error => {
console.error('获取发布信息失败:', error);
this.uiManager.showError(this.i18nMessages.getMessage('fetchError', this.i18nMessages.getCurrentLang()));
});
// 监听语言变化事件
document.addEventListener('languageChanged', () => this.updateUI());
}
/**
* 更新UI元素当语言变化时
*/
updateUI() {
const elementsToUpdate = document.querySelectorAll('[data-en][data-zh]');
const currentLang = this.i18nMessages.getCurrentLang();
elementsToUpdate.forEach(element => {
const text = element.getAttribute(`data-${currentLang}`);
if (text) {
element.textContent = text;
}
});
}
}
// 当DOM加载完成时初始化应用
document.addEventListener('DOMContentLoaded', () => {
new ChangelogApp();
});

443
docs/js/script.js Normal file
View File

@@ -0,0 +1,443 @@
/**
* VoidRaft - Website Script
*/
/**
* 主题管理类
*/
class ThemeManager {
constructor() {
this.themeToggle = document.getElementById('theme-toggle');
this.currentTheme = this.getInitialTheme();
this.init();
}
/**
* 获取初始主题
*/
getInitialTheme() {
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
const savedTheme = localStorage.getItem('theme');
return savedTheme || (prefersDarkScheme.matches ? 'dark' : 'light');
}
/**
* 初始化主题管理器
*/
init() {
if (!this.themeToggle) return;
this.setTheme(this.currentTheme);
this.bindEvents();
}
/**
* 绑定事件
*/
bindEvents() {
this.themeToggle.addEventListener('click', () => {
this.toggleTheme();
});
}
/**
* 切换主题
*/
toggleTheme() {
document.body.classList.add('theme-transition');
const newTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
this.setTheme(newTheme);
this.saveTheme(newTheme);
setTimeout(() => document.body.classList.remove('theme-transition'), 300);
}
/**
* 设置主题
* @param {string} theme - 'dark' 或 'light'
*/
setTheme(theme) {
this.currentTheme = theme;
const isDark = theme === 'dark';
document.body.classList.toggle('theme-dark', isDark);
document.body.classList.toggle('theme-light', !isDark);
this.updateToggleIcon(isDark);
}
/**
* 更新切换按钮图标
* @param {boolean} isDark - 是否为暗色主题
*/
updateToggleIcon(isDark) {
if (this.themeToggle) {
const icon = this.themeToggle.querySelector('i');
if (icon) {
icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
}
}
}
/**
* 保存主题到本地存储
* @param {string} theme - 主题名称
*/
saveTheme(theme) {
localStorage.setItem('theme', theme);
}
}
/**
* 语言管理类
*/
class LanguageManager {
constructor() {
this.langToggle = document.getElementById('lang-toggle');
this.currentLang = this.getInitialLanguage();
this.init();
}
/**
* 获取初始语言
*/
getInitialLanguage() {
const urlParams = new URLSearchParams(window.location.search);
const urlLang = urlParams.get('lang');
const savedLang = localStorage.getItem('lang');
const browserLang = navigator.language.startsWith('zh') ? 'zh' : 'en';
return urlLang || savedLang || browserLang;
}
/**
* 初始化语言管理器
*/
init() {
if (!this.langToggle) return;
window.currentLang = this.currentLang;
this.setLanguage(this.currentLang);
this.bindEvents();
}
/**
* 绑定事件
*/
bindEvents() {
this.langToggle.addEventListener('click', () => {
this.toggleLanguage();
});
}
/**
* 切换语言
*/
toggleLanguage() {
document.body.classList.add('lang-transition');
const newLang = this.currentLang === 'zh' ? 'en' : 'zh';
this.setLanguage(newLang);
this.saveLanguage(newLang);
this.updateURL(newLang);
this.notifyLanguageChange(newLang);
setTimeout(() => document.body.classList.remove('lang-transition'), 300);
}
/**
* 设置页面语言
* @param {string} lang - 'zh' 或 'en'
*/
setLanguage(lang) {
this.currentLang = lang;
window.currentLang = lang;
this.updatePageElements(lang);
this.updateHTMLLang(lang);
this.updateToggleButton(lang);
}
/**
* 更新页面元素文本
* @param {string} lang - 语言代码
*/
updatePageElements(lang) {
document.querySelectorAll('[data-zh][data-en]').forEach(el => {
el.textContent = el.getAttribute(`data-${lang}`);
});
}
/**
* 更新HTML语言属性
* @param {string} lang - 语言代码
*/
updateHTMLLang(lang) {
document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
}
/**
* 更新切换按钮文本
* @param {string} lang - 语言代码
*/
updateToggleButton(lang) {
if (this.langToggle) {
const text = lang === 'zh' ? 'EN/中' : '中/EN';
this.langToggle.innerHTML = `<i class="fas fa-language"></i> ${text}`;
}
}
/**
* 保存语言到本地存储
* @param {string} lang - 语言代码
*/
saveLanguage(lang) {
localStorage.setItem('lang', lang);
}
/**
* 更新URL参数
* @param {string} lang - 语言代码
*/
updateURL(lang) {
const newUrl = new URL(window.location);
if (lang === 'zh') {
newUrl.searchParams.set('lang', 'zh');
} else {
newUrl.searchParams.delete('lang');
}
window.history.replaceState({}, '', newUrl);
}
/**
* 通知语言变更
* @param {string} lang - 语言代码
*/
notifyLanguageChange(lang) {
window.dispatchEvent(new CustomEvent('languageChanged', { detail: { lang } }));
}
/**
* 获取当前语言
*/
getCurrentLanguage() {
return this.currentLang;
}
}
/**
* SEO管理类
*/
class SEOManager {
constructor(languageManager) {
this.languageManager = languageManager;
this.metaTexts = {
en: {
description: 'VoidRaft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.',
title: 'VoidRaft - An elegant text snippet recording tool designed for developers.',
ogTitle: 'VoidRaft - An elegant text snippet recording tool designed for developers'
},
zh: {
description: 'VoidRaft 是专为开发者打造的优雅文本片段记录工具。支持多语言代码块、语法高亮、代码格式化、自定义主题等功能。',
title: 'VoidRaft - 专为开发者打造的优雅文本片段记录工具',
ogTitle: 'VoidRaft - 专为开发者打造的优雅文本片段记录工具'
}
};
this.init();
}
/**
* 初始化SEO管理器
*/
init() {
this.bindEvents();
this.updateMetaTags(this.languageManager.getCurrentLanguage());
}
/**
* 绑定事件
*/
bindEvents() {
window.addEventListener('languageChanged', (event) => {
this.updateMetaTags(event.detail.lang);
});
}
/**
* 更新SEO元标签
* @param {string} lang - 当前语言
*/
updateMetaTags(lang) {
const texts = this.metaTexts[lang];
this.updateMetaDescription(texts.description);
this.updateOpenGraphTags(texts.ogTitle, texts.description);
this.updateTwitterCardTags(texts.ogTitle, texts.description);
this.updatePageTitle(texts.title);
}
/**
* 更新meta描述
* @param {string} description - 描述文本
*/
updateMetaDescription(description) {
const metaDesc = document.querySelector('meta[name="description"]');
if (metaDesc) {
metaDesc.content = description;
}
}
/**
* 更新Open Graph标签
* @param {string} title - 标题
* @param {string} description - 描述
*/
updateOpenGraphTags(title, description) {
const ogTitle = document.querySelector('meta[property="og:title"]');
const ogDesc = document.querySelector('meta[property="og:description"]');
if (ogTitle) ogTitle.content = title;
if (ogDesc) ogDesc.content = description;
}
/**
* 更新Twitter Card标签
* @param {string} title - 标题
* @param {string} description - 描述
*/
updateTwitterCardTags(title, description) {
const twitterTitle = document.querySelector('meta[property="twitter:title"]');
const twitterDesc = document.querySelector('meta[property="twitter:description"]');
if (twitterTitle) twitterTitle.content = title;
if (twitterDesc) twitterDesc.content = description;
}
/**
* 更新页面标题
* @param {string} title - 标题
*/
updatePageTitle(title) {
document.title = title;
}
}
/**
* UI效果管理类
*/
class UIEffects {
constructor() {
this.init();
}
/**
* 初始化UI效果
*/
init() {
this.initCardEffects();
}
/**
* 初始化卡片悬停效果
*/
initCardEffects() {
const cards = document.querySelectorAll('.feature-card');
cards.forEach(card => {
card.addEventListener('mouseenter', () => {
this.animateCardHover(card, true);
});
card.addEventListener('mouseleave', () => {
this.animateCardHover(card, false);
});
});
}
/**
* 卡片悬停动画
* @param {Element} card - 卡片元素
* @param {boolean} isHover - 是否悬停
*/
animateCardHover(card, isHover) {
if (isHover) {
card.style.transform = 'translateY(-8px)';
card.style.boxShadow = '7px 7px 0 var(--shadow-color)';
} else {
card.style.transform = 'translateY(0)';
card.style.boxShadow = '5px 5px 0 var(--shadow-color)';
}
}
}
/**
* VoidRaft主应用类
*/
class VoidRaftApp {
constructor() {
this.themeManager = null;
this.languageManager = null;
this.seoManager = null;
this.uiEffects = null;
this.init();
}
/**
* 初始化应用
*/
init() {
this.initializeManagers();
this.showConsoleBranding();
}
/**
* 初始化各个管理器
*/
initializeManagers() {
this.themeManager = new ThemeManager();
this.languageManager = new LanguageManager();
this.seoManager = new SEOManager(this.languageManager);
this.uiEffects = new UIEffects();
}
/**
* 显示控制台品牌信息
*/
showConsoleBranding() {
console.log('%c VoidRaft', 'color: #ff006e; font-size: 20px; font-family: "Space Mono", monospace;');
console.log('%c An elegant text snippet recording tool designed for developers.', 'color: #073B4C; font-family: "Space Mono", monospace;');
}
/**
* 获取主题管理器
*/
getThemeManager() {
return this.themeManager;
}
/**
* 获取语言管理器
*/
getLanguageManager() {
return this.languageManager;
}
/**
* 获取SEO管理器
*/
getSEOManager() {
return this.seoManager;
}
/**
* 获取UI效果管理器
*/
getUIEffects() {
return this.uiEffects;
}
}
// 当DOM加载完成时初始化应用
document.addEventListener('DOMContentLoaded', () => {
window.voidRaftApp = new VoidRaftApp();
});

View File

@@ -11,15 +11,55 @@ import * as slog$0 from "../../../../../../log/slog/models.js";
export class App {
/**
* The main application menu
* Manager pattern for organized API
*/
"ApplicationMenu": Menu | null;
"Window": WindowManager | null;
"ContextMenu": ContextMenuManager | null;
"KeyBinding": KeyBindingManager | null;
"Browser": BrowserManager | null;
"Env": EnvironmentManager | null;
"Dialog": DialogManager | null;
"Event": EventManager | null;
"Menu": MenuManager | null;
"Screen": ScreenManager | null;
"Clipboard": ClipboardManager | null;
"SystemTray": SystemTrayManager | null;
"Logger": slog$0.Logger | null;
/** Creates a new App instance. */
constructor($$source: Partial<App> = {}) {
if (!("ApplicationMenu" in $$source)) {
this["ApplicationMenu"] = null;
if (!("Window" in $$source)) {
this["Window"] = null;
}
if (!("ContextMenu" in $$source)) {
this["ContextMenu"] = null;
}
if (!("KeyBinding" in $$source)) {
this["KeyBinding"] = null;
}
if (!("Browser" in $$source)) {
this["Browser"] = null;
}
if (!("Env" in $$source)) {
this["Env"] = null;
}
if (!("Dialog" in $$source)) {
this["Dialog"] = null;
}
if (!("Event" in $$source)) {
this["Event"] = null;
}
if (!("Menu" in $$source)) {
this["Menu"] = null;
}
if (!("Screen" in $$source)) {
this["Screen"] = null;
}
if (!("Clipboard" in $$source)) {
this["Clipboard"] = null;
}
if (!("SystemTray" in $$source)) {
this["SystemTray"] = null;
}
if (!("Logger" in $$source)) {
this["Logger"] = null;
@@ -34,31 +74,308 @@ export class App {
static createFrom($$source: any = {}): App {
const $$createField0_0 = $$createType1;
const $$createField1_0 = $$createType3;
const $$createField2_0 = $$createType5;
const $$createField3_0 = $$createType7;
const $$createField4_0 = $$createType9;
const $$createField5_0 = $$createType11;
const $$createField6_0 = $$createType13;
const $$createField7_0 = $$createType15;
const $$createField8_0 = $$createType17;
const $$createField9_0 = $$createType19;
const $$createField10_0 = $$createType21;
const $$createField11_0 = $$createType23;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("ApplicationMenu" in $$parsedSource) {
$$parsedSource["ApplicationMenu"] = $$createField0_0($$parsedSource["ApplicationMenu"]);
if ("Window" in $$parsedSource) {
$$parsedSource["Window"] = $$createField0_0($$parsedSource["Window"]);
}
if ("ContextMenu" in $$parsedSource) {
$$parsedSource["ContextMenu"] = $$createField1_0($$parsedSource["ContextMenu"]);
}
if ("KeyBinding" in $$parsedSource) {
$$parsedSource["KeyBinding"] = $$createField2_0($$parsedSource["KeyBinding"]);
}
if ("Browser" in $$parsedSource) {
$$parsedSource["Browser"] = $$createField3_0($$parsedSource["Browser"]);
}
if ("Env" in $$parsedSource) {
$$parsedSource["Env"] = $$createField4_0($$parsedSource["Env"]);
}
if ("Dialog" in $$parsedSource) {
$$parsedSource["Dialog"] = $$createField5_0($$parsedSource["Dialog"]);
}
if ("Event" in $$parsedSource) {
$$parsedSource["Event"] = $$createField6_0($$parsedSource["Event"]);
}
if ("Menu" in $$parsedSource) {
$$parsedSource["Menu"] = $$createField7_0($$parsedSource["Menu"]);
}
if ("Screen" in $$parsedSource) {
$$parsedSource["Screen"] = $$createField8_0($$parsedSource["Screen"]);
}
if ("Clipboard" in $$parsedSource) {
$$parsedSource["Clipboard"] = $$createField9_0($$parsedSource["Clipboard"]);
}
if ("SystemTray" in $$parsedSource) {
$$parsedSource["SystemTray"] = $$createField10_0($$parsedSource["SystemTray"]);
}
if ("Logger" in $$parsedSource) {
$$parsedSource["Logger"] = $$createField1_0($$parsedSource["Logger"]);
$$parsedSource["Logger"] = $$createField11_0($$parsedSource["Logger"]);
}
return new App($$parsedSource as Partial<App>);
}
}
export class Menu {
/**
* BrowserManager manages browser-related operations
*/
export class BrowserManager {
/** Creates a new Menu instance. */
constructor($$source: Partial<Menu> = {}) {
/** Creates a new BrowserManager instance. */
constructor($$source: Partial<BrowserManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new Menu instance from a string or object.
* Creates a new BrowserManager instance from a string or object.
*/
static createFrom($$source: any = {}): Menu {
static createFrom($$source: any = {}): BrowserManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new Menu($$parsedSource as Partial<Menu>);
return new BrowserManager($$parsedSource as Partial<BrowserManager>);
}
}
/**
* ClipboardManager manages clipboard operations
*/
export class ClipboardManager {
/** Creates a new ClipboardManager instance. */
constructor($$source: Partial<ClipboardManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new ClipboardManager instance from a string or object.
*/
static createFrom($$source: any = {}): ClipboardManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new ClipboardManager($$parsedSource as Partial<ClipboardManager>);
}
}
/**
* ContextMenuManager manages all context menu operations
*/
export class ContextMenuManager {
/** Creates a new ContextMenuManager instance. */
constructor($$source: Partial<ContextMenuManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new ContextMenuManager instance from a string or object.
*/
static createFrom($$source: any = {}): ContextMenuManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new ContextMenuManager($$parsedSource as Partial<ContextMenuManager>);
}
}
/**
* DialogManager manages dialog-related operations
*/
export class DialogManager {
/** Creates a new DialogManager instance. */
constructor($$source: Partial<DialogManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new DialogManager instance from a string or object.
*/
static createFrom($$source: any = {}): DialogManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new DialogManager($$parsedSource as Partial<DialogManager>);
}
}
/**
* EnvironmentManager manages environment-related operations
*/
export class EnvironmentManager {
/** Creates a new EnvironmentManager instance. */
constructor($$source: Partial<EnvironmentManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new EnvironmentManager instance from a string or object.
*/
static createFrom($$source: any = {}): EnvironmentManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new EnvironmentManager($$parsedSource as Partial<EnvironmentManager>);
}
}
/**
* EventManager manages event-related operations
*/
export class EventManager {
/** Creates a new EventManager instance. */
constructor($$source: Partial<EventManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new EventManager instance from a string or object.
*/
static createFrom($$source: any = {}): EventManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new EventManager($$parsedSource as Partial<EventManager>);
}
}
/**
* KeyBindingManager manages all key binding operations
*/
export class KeyBindingManager {
/** Creates a new KeyBindingManager instance. */
constructor($$source: Partial<KeyBindingManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new KeyBindingManager instance from a string or object.
*/
static createFrom($$source: any = {}): KeyBindingManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new KeyBindingManager($$parsedSource as Partial<KeyBindingManager>);
}
}
/**
* MenuManager manages menu-related operations
*/
export class MenuManager {
/** Creates a new MenuManager instance. */
constructor($$source: Partial<MenuManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new MenuManager instance from a string or object.
*/
static createFrom($$source: any = {}): MenuManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new MenuManager($$parsedSource as Partial<MenuManager>);
}
}
export class ScreenManager {
/** Creates a new ScreenManager instance. */
constructor($$source: Partial<ScreenManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new ScreenManager instance from a string or object.
*/
static createFrom($$source: any = {}): ScreenManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new ScreenManager($$parsedSource as Partial<ScreenManager>);
}
}
/**
* ServiceOptions provides optional parameters for calls to [NewService].
*/
export class ServiceOptions {
/**
* Name can be set to override the name of the service
* for logging and debugging purposes.
*
* If empty, it will default
* either to the value obtained through the [ServiceName] interface,
* or to the type name.
*/
"Name": string;
/**
* If the service instance implements [http.Handler],
* it will be mounted on the internal asset server
* at the prefix specified by Route.
*/
"Route": string;
/**
* MarshalError will be called if non-nil
* to marshal to JSON the error values returned by this service's methods.
*
* MarshalError is not allowed to fail,
* but it may return a nil slice to fall back
* to the globally configured error handler.
*
* If the returned slice is not nil, it must contain valid JSON.
*/
"MarshalError": any;
/** Creates a new ServiceOptions instance. */
constructor($$source: Partial<ServiceOptions> = {}) {
if (!("Name" in $$source)) {
this["Name"] = "";
}
if (!("Route" in $$source)) {
this["Route"] = "";
}
if (!("MarshalError" in $$source)) {
this["MarshalError"] = null;
}
Object.assign(this, $$source);
}
/**
* Creates a new ServiceOptions instance from a string or object.
*/
static createFrom($$source: any = {}): ServiceOptions {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new ServiceOptions($$parsedSource as Partial<ServiceOptions>);
}
}
/**
* SystemTrayManager manages system tray-related operations
*/
export class SystemTrayManager {
/** Creates a new SystemTrayManager instance. */
constructor($$source: Partial<SystemTrayManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new SystemTrayManager instance from a string or object.
*/
static createFrom($$source: any = {}): SystemTrayManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new SystemTrayManager($$parsedSource as Partial<SystemTrayManager>);
}
}
@@ -79,8 +396,48 @@ export class WebviewWindow {
}
}
/**
* WindowManager manages all window-related operations
*/
export class WindowManager {
/** Creates a new WindowManager instance. */
constructor($$source: Partial<WindowManager> = {}) {
Object.assign(this, $$source);
}
/**
* Creates a new WindowManager instance from a string or object.
*/
static createFrom($$source: any = {}): WindowManager {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new WindowManager($$parsedSource as Partial<WindowManager>);
}
}
// Private type creation functions
const $$createType0 = Menu.createFrom;
const $$createType0 = WindowManager.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = slog$0.Logger.createFrom;
const $$createType2 = ContextMenuManager.createFrom;
const $$createType3 = $Create.Nullable($$createType2);
const $$createType4 = KeyBindingManager.createFrom;
const $$createType5 = $Create.Nullable($$createType4);
const $$createType6 = BrowserManager.createFrom;
const $$createType7 = $Create.Nullable($$createType6);
const $$createType8 = EnvironmentManager.createFrom;
const $$createType9 = $Create.Nullable($$createType8);
const $$createType10 = DialogManager.createFrom;
const $$createType11 = $Create.Nullable($$createType10);
const $$createType12 = EventManager.createFrom;
const $$createType13 = $Create.Nullable($$createType12);
const $$createType14 = MenuManager.createFrom;
const $$createType15 = $Create.Nullable($$createType14);
const $$createType16 = ScreenManager.createFrom;
const $$createType17 = $Create.Nullable($$createType16);
const $$createType18 = ClipboardManager.createFrom;
const $$createType19 = $Create.Nullable($$createType18);
const $$createType20 = SystemTrayManager.createFrom;
const $$createType21 = $Create.Nullable($$createType20);
const $$createType22 = slog$0.Logger.createFrom;
const $$createType23 = $Create.Nullable($$createType22);

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,71 @@
// 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";
/**
* LanguageInfo 语言信息结构体
*/
export class LanguageInfo {
/**
* 语言代码
*/
"Code": string;
/**
* 语言名称
*/
"Name": string;
/** Creates a new LanguageInfo instance. */
constructor($$source: Partial<LanguageInfo> = {}) {
if (!("Code" in $$source)) {
this["Code"] = "";
}
if (!("Name" in $$source)) {
this["Name"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new LanguageInfo instance from a string or object.
*/
static createFrom($$source: any = {}): LanguageInfo {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new LanguageInfo($$parsedSource as Partial<LanguageInfo>);
}
}
/**
* TranslatorType 翻译器类型
*/
export enum TranslatorType {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* GoogleTranslatorType 谷歌翻译器
*/
GoogleTranslatorType = "google",
/**
* BingTranslatorType 必应翻译器
*/
BingTranslatorType = "bing",
/**
* YoudaoTranslatorType 有道翻译器
*/
YoudaoTranslatorType = "youdao",
/**
* DeeplTranslatorType DeepL翻译器
*/
DeeplTranslatorType = "deepl",
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* BackupService 提供基于Git的备份功能
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as models$0 from "../models/models.js";
/**
* HandleConfigChange 处理备份配置变更
*/
export function HandleConfigChange(config: models$0.GitBackupConfig | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(395287784, config) as any;
return $resultPromise;
}
/**
* Initialize 初始化备份服务
*/
export function Initialize(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1052437974) as any;
return $resultPromise;
}
/**
* PushToRemote 推送本地更改到远程仓库
*/
export function PushToRemote(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(262644139) as any;
return $resultPromise;
}
/**
* Reinitialize 重新初始化备份服务,用于响应配置变更
*/
export function Reinitialize(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(301562543) as any;
return $resultPromise;
}
/**
* ServiceShutdown 服务关闭时的清理工作
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(422131801) as any;
return $resultPromise;
}
/**
* StartAutoBackup 启动自动备份定时器
*/
export function StartAutoBackup(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3035755449) as any;
return $resultPromise;
}
/**
* StopAutoBackup 停止自动备份
*/
export function StopAutoBackup(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2641894021) as any;
return $resultPromise;
}

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* ConfigService 提供基于 Viper 的配置管理功能
* ConfigService 应用配置服务
* @module
*/
@@ -42,6 +42,14 @@ export function ResetConfig(): Promise<void> & { cancel(): void } {
return $resultPromise;
}
/**
* ServiceShutdown 关闭服务
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3963562361) as any;
return $resultPromise;
}
/**
* Set 设置配置项
*/
@@ -50,6 +58,14 @@ export function Set(key: string, value: any): Promise<void> & { cancel(): void }
return $resultPromise;
}
/**
* SetBackupConfigChangeCallback 设置备份配置变更回调
*/
export function SetBackupConfigChangeCallback(callback: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3264871659, callback) as any;
return $resultPromise;
}
/**
* SetDataPathChangeCallback 设置数据路径配置变更回调
*/

View File

@@ -0,0 +1,47 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* DatabaseService provides shared database functionality
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
/**
* 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
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3907893632) as any;
return $resultPromise;
}
/**
* ServiceStartup initializes the service when the application starts
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2067840771, options) as any;
return $resultPromise;
}

View File

@@ -22,6 +22,14 @@ export function SelectDirectory(): Promise<string> & { cancel(): void } {
return $resultPromise;
}
/**
* SelectFile 打开文件选择对话框
*/
export function SelectFile(): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(37302920) as any;
return $resultPromise;
}
/**
* SetWindow 设置绑定的窗口
*/

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* DocumentService 提供文档管理功能
* DocumentService provides document management functionality
* @module
*/
@@ -10,23 +10,18 @@
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as models$0 from "../models/models.js";
/**
* ForceSave 强制保存
* CreateDocument creates a new document and returns the created document with ID
*/
export function ForceSave(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2767091023) as any;
return $resultPromise;
}
/**
* GetActiveDocument 获取活动文档
*/
export function GetActiveDocument(): Promise<models$0.Document | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(1785823398) as any;
export function CreateDocument(title: string): Promise<models$0.Document | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3360680842, title) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
@@ -35,45 +30,106 @@ export function GetActiveDocument(): Promise<models$0.Document | null> & { cance
}
/**
* Initialize 初始化服务
* DeleteDocument marks a document as deleted (default document with ID=1 cannot be deleted)
*/
export function Initialize(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3418008221) as any;
export function DeleteDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(412287269, id) as any;
return $resultPromise;
}
/**
* OnDataPathChanged 处理数据路径变更
* GetDocumentByID gets a document by ID
*/
export function OnDataPathChanged(oldPath: string, newPath: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(269349439, oldPath, newPath) as any;
export function GetDocumentByID(id: number): Promise<models$0.Document | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3468193232, id) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetFirstDocumentID gets the first active document's ID for frontend initialization
*/
export function GetFirstDocumentID(): Promise<number> & { cancel(): void } {
let $resultPromise = $Call.ByID(2970773833) as any;
return $resultPromise;
}
/**
* ReloadDocument 重新加载文档
* ListAllDocumentsMeta lists all active (non-deleted) document metadata
*/
export function ReloadDocument(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3093415283) as any;
export function ListAllDocumentsMeta(): Promise<(models$0.Document | null)[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3073950297) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* 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 锁定文档,防止删除
*/
export function LockDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1889494473, id) as any;
return $resultPromise;
}
/**
* ServiceShutdown 关闭服务
* RestoreDocument restores a deleted document
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(638578044) as any;
export function RestoreDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(784200778, id) as any;
return $resultPromise;
}
/**
* UpdateActiveDocumentContent 更新文档内容
* ServiceStartup initializes the service when the application starts
*/
export function UpdateActiveDocumentContent(content: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1486276638, content) as any;
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1474135487, options) as any;
return $resultPromise;
}
/**
* UnlockDocument 解锁文档
*/
export function UnlockDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(222307930, id) as any;
return $resultPromise;
}
/**
* UpdateDocumentContent updates the content of a document
*/
export function UpdateDocumentContent(id: number, content: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3251897116, id, content) as any;
return $resultPromise;
}
/**
* UpdateDocumentTitle updates the title of a document
*/
export function UpdateDocumentTitle(id: number, title: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2045530459, id, title) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.Document.createFrom;
const $$createType1 = $Create.Nullable($$createType0);
const $$createType2 = $Create.Array($$createType1);

View File

@@ -0,0 +1,74 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* ExtensionService 扩展管理服务
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as models$0 from "../models/models.js";
/**
* GetAllExtensions 获取所有扩展配置
*/
export function GetAllExtensions(): Promise<models$0.Extension[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(3094292124) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* ResetAllExtensionsToDefault 重置所有扩展到默认状态
*/
export function ResetAllExtensionsToDefault(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(270611949) 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 启动时调用
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(40324057, options) as any;
return $resultPromise;
}
/**
* UpdateExtensionEnabled 更新扩展启用状态
*/
export function UpdateExtensionEnabled(id: models$0.ExtensionID, enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1067300094, id, enabled) as any;
return $resultPromise;
}
/**
* UpdateExtensionState 更新扩展状态
*/
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;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.Extension.createFrom;
const $$createType1 = $Create.Array($$createType0);

View File

@@ -2,7 +2,7 @@
// This file is automatically generated. DO NOT EDIT
/**
* HotkeyService 全局热键服务
* HotkeyService Windows全局热键服务
* @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";
/**
* GetCurrentHotkey 获取当前注册的热键
* GetCurrentHotkey 获取当前热键
*/
export function GetCurrentHotkey(): Promise<models$0.HotkeyCombo | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(2572811187) as any;
@@ -38,7 +38,7 @@ export function Initialize(app: application$0.App | null): Promise<void> & { can
}
/**
* IsRegistered 检查是否已注册热键
* IsRegistered 检查是否已注册
*/
export function IsRegistered(): Promise<boolean> & { cancel(): void } {
let $resultPromise = $Call.ByID(106954156) as any;
@@ -54,21 +54,13 @@ export function RegisterHotkey(hotkey: models$0.HotkeyCombo | null): Promise<voi
}
/**
* ServiceShutdown 关闭热键服务
* ServiceShutdown 关闭服务
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(157291181) as any;
return $resultPromise;
}
/**
* ToggleWindow 切换窗口显示/隐藏 - 通过事件通知前端处理
*/
export function ToggleWindow(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1318185132) as any;
return $resultPromise;
}
/**
* UnregisterHotkey 取消注册全局热键
*/

View File

@@ -1,21 +1,37 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import * as BackupService from "./backupservice.js";
import * as ConfigService from "./configservice.js";
import * as DatabaseService from "./databaseservice.js";
import * as DialogService from "./dialogservice.js";
import * as DocumentService from "./documentservice.js";
import * as ExtensionService from "./extensionservice.js";
import * as HotkeyService from "./hotkeyservice.js";
import * as KeyBindingService from "./keybindingservice.js";
import * as MigrationService from "./migrationservice.js";
import * as SelfUpdateService from "./selfupdateservice.js";
import * as StartupService from "./startupservice.js";
import * as SystemService from "./systemservice.js";
import * as TranslationService from "./translationservice.js";
import * as TrayService from "./trayservice.js";
import * as WindowService from "./windowservice.js";
export {
BackupService,
ConfigService,
DatabaseService,
DialogService,
DocumentService,
ExtensionService,
HotkeyService,
KeyBindingService,
MigrationService,
SelfUpdateService,
StartupService,
SystemService,
TrayService
TranslationService,
TrayService,
WindowService
};
export * from "./models.js";

View File

@@ -0,0 +1,42 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* KeyBindingService 快捷键管理服务
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as models$0 from "../models/models.js";
/**
* GetAllKeyBindings 获取所有快捷键配置
*/
export function GetAllKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(1633502882) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* ServiceStartup 启动时调用
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2057121990, options) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = models$0.KeyBinding.createFrom;
const $$createType1 = $Create.Array($$createType0);

View File

@@ -50,13 +50,5 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
return $resultPromise;
}
/**
* SetProgressBroadcaster 设置进度广播函数
*/
export function SetProgressBroadcaster(broadcaster: any): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3244071921, broadcaster) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = $models.MigrationProgress.createFrom;

View File

@@ -5,6 +5,10 @@
// @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 application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
/**
* MemoryStats 内存统计信息
*/
@@ -76,19 +80,8 @@ export class MemoryStats {
* MigrationProgress 迁移进度信息
*/
export class MigrationProgress {
/**
* 迁移状态
*/
"status": MigrationStatus;
/**
* 进度百分比 (0-100)
*/
"progress": number;
/**
* 错误信息
*/
"error"?: string;
/** Creates a new MigrationProgress instance. */
@@ -121,18 +114,130 @@ export enum MigrationStatus {
*/
$zero = "",
/**
* 迁移中
*/
MigrationStatusMigrating = "migrating",
/**
* 完成
*/
MigrationStatusCompleted = "completed",
/**
* 失败
*/
MigrationStatusFailed = "failed",
};
/**
* SelfUpdateResult 自我更新结果
*/
export class SelfUpdateResult {
/**
* 是否有更新
*/
"hasUpdate": boolean;
/**
* 当前版本
*/
"currentVersion": string;
/**
* 最新版本
*/
"latestVersion": string;
/**
* 是否已应用更新
*/
"updateApplied": boolean;
/**
* 下载链接
*/
"assetURL": string;
/**
* 发布说明
*/
"releaseNotes": string;
/**
* 错误信息
*/
"error": string;
/**
* 更新源github/gitea
*/
"source": string;
/** Creates a new SelfUpdateResult instance. */
constructor($$source: Partial<SelfUpdateResult> = {}) {
if (!("hasUpdate" in $$source)) {
this["hasUpdate"] = false;
}
if (!("currentVersion" in $$source)) {
this["currentVersion"] = "";
}
if (!("latestVersion" in $$source)) {
this["latestVersion"] = "";
}
if (!("updateApplied" in $$source)) {
this["updateApplied"] = false;
}
if (!("assetURL" in $$source)) {
this["assetURL"] = "";
}
if (!("releaseNotes" in $$source)) {
this["releaseNotes"] = "";
}
if (!("error" in $$source)) {
this["error"] = "";
}
if (!("source" in $$source)) {
this["source"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new SelfUpdateResult instance from a string or object.
*/
static createFrom($$source: any = {}): SelfUpdateResult {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new SelfUpdateResult($$parsedSource as Partial<SelfUpdateResult>);
}
}
/**
* WindowInfo 窗口信息
*/
export class WindowInfo {
"Window": application$0.WebviewWindow | null;
"DocumentID": number;
"Title": string;
/** Creates a new WindowInfo instance. */
constructor($$source: Partial<WindowInfo> = {}) {
if (!("Window" in $$source)) {
this["Window"] = null;
}
if (!("DocumentID" in $$source)) {
this["DocumentID"] = 0;
}
if (!("Title" in $$source)) {
this["Title"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new WindowInfo instance from a string or object.
*/
static createFrom($$source: any = {}): WindowInfo {
const $$createField0_0 = $$createType1;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("Window" in $$parsedSource) {
$$parsedSource["Window"] = $$createField0_0($$parsedSource["Window"]);
}
return new WindowInfo($$parsedSource as Partial<WindowInfo>);
}
}
// Private type creation functions
const $$createType0 = application$0.WebviewWindow.createFrom;
const $$createType1 = $Create.Nullable($$createType0);

View File

@@ -0,0 +1,51 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* SelfUpdateService 自我更新服务
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as $models from "./models.js";
/**
* ApplyUpdate 应用更新
*/
export function ApplyUpdate(): Promise<$models.SelfUpdateResult | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(2009328394) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* CheckForUpdates 检查更新
*/
export function CheckForUpdates(): Promise<$models.SelfUpdateResult | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(438757208) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* RestartApplication 重启应用程序
*/
export function RestartApplication(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3341481538) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = $models.SelfUpdateResult.createFrom;
const $$createType1 = $Create.Nullable($$createType0);

View File

@@ -0,0 +1,19 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* StartupService 开机启动服务
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
/**
* SetEnabled 设置开机启动状态
*/
export function SetEnabled(enabled: boolean): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2911601468, enabled) as any;
return $resultPromise;
}

View File

@@ -0,0 +1,78 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* TranslationService 翻译服务
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as translator$0 from "../common/translator/models.js";
/**
* GetAvailableTranslators 获取所有可用翻译器类型
* @returns {[]string} 翻译器类型列表
*/
export function GetAvailableTranslators(): Promise<string[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(1186597995) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType0($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* GetStandardLanguageCode 获取标准化的语言代码
*/
export function GetStandardLanguageCode(translatorType: translator$0.TranslatorType, languageCode: string): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(1158131995, translatorType, languageCode) as any;
return $resultPromise;
}
/**
* GetTranslatorLanguages 获取翻译器的语言列表
* @param {string} translatorType - 翻译器类型 ("google", "bing", "youdao", "deepl")
* @returns {map[string]string} 语言代码到名称的映射
* @returns {error} 可能的错误
*/
export function GetTranslatorLanguages(translatorType: translator$0.TranslatorType): Promise<{ [_: string]: translator$0.LanguageInfo }> & { cancel(): void } {
let $resultPromise = $Call.ByID(3976114458, translatorType) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType2($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* IsLanguageSupported 检查指定的语言代码是否受支持
*/
export function IsLanguageSupported(translatorType: translator$0.TranslatorType, languageCode: string): Promise<boolean> & { cancel(): void } {
let $resultPromise = $Call.ByID(2819945417, translatorType, languageCode) as any;
return $resultPromise;
}
/**
* TranslateWith 使用指定翻译器进行翻译
* @param {string} text - 待翻译文本
* @param {string} from - 源语言代码 (如 "en", "zh", "auto")
* @param {string} to - 目标语言代码 (如 "en", "zh")
* @param {string} translatorType - 翻译器类型 ("google", "bing", "youdao", "deepl")
* @returns {string} 翻译后的文本
* @returns {error} 可能的错误
*/
export function TranslateWith(text: string, $from: string, to: string, translatorType: string): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(3577923623, text, $from, to, translatorType) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = $Create.Array($Create.Any);
const $$createType1 = translator$0.LanguageInfo.createFrom;
const $$createType2 = $Create.Map($Create.Any, $$createType1);

View File

@@ -0,0 +1,59 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* WindowService 窗口管理服务
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as $models from "./models.js";
/**
* GetOpenWindows 获取所有打开的窗口信息
*/
export function GetOpenWindows(): Promise<$models.WindowInfo[]> & { cancel(): void } {
let $resultPromise = $Call.ByID(1464997251) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* IsDocumentWindowOpen 检查指定文档的窗口是否已打开
*/
export function IsDocumentWindowOpen(documentID: number): Promise<boolean> & { cancel(): void } {
let $resultPromise = $Call.ByID(1735611839, documentID) as any;
return $resultPromise;
}
/**
* OpenDocumentWindow 为指定文档ID打开新窗口
*/
export function OpenDocumentWindow(documentID: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(494716471, documentID) as any;
return $resultPromise;
}
/**
* SetAppReferences 设置应用和主窗口引用
*/
export function SetAppReferences(app: application$0.App | null, mainWindow: application$0.WebviewWindow | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1120840759, app, mainWindow) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = $models.WindowInfo.createFrom;
const $$createType1 = $Create.Array($$createType0);

View File

@@ -8,10 +8,16 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
BlockLanguageSelector: typeof import('./src/components/toolbar/BlockLanguageSelector.vue')['default']
DocumentSelector: typeof import('./src/components/toolbar/DocumentSelector.vue')['default']
LinuxTitleBar: typeof import('./src/components/titlebar/LinuxTitleBar.vue')['default']
LoadingScreen: typeof import('./src/components/loading/LoadingScreen.vue')['default']
MacOSTitleBar: typeof import('./src/components/titlebar/MacOSTitleBar.vue')['default']
MemoryMonitor: typeof import('./src/components/monitor/MemoryMonitor.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Toolbar: typeof import('./src/components/toolbar/Toolbar.vue')['default']
WindowsTitleBar: typeof import('./src/components/titlebar/WindowsTitleBar.vue')['default']
WindowTitleBar: typeof import('./src/components/titlebar/WindowTitleBar.vue')['default']
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,57 +15,67 @@
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-angular": "^0.1.4",
"@codemirror/lang-cpp": "^6.0.2",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-java": "^6.0.1",
"@codemirror/lang-java": "^6.0.2",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-less": "^6.0.2",
"@codemirror/lang-lezer": "^6.0.2",
"@codemirror/lang-liquid": "^6.2.3",
"@codemirror/lang-markdown": "^6.3.2",
"@codemirror/lang-php": "^6.0.1",
"@codemirror/lang-markdown": "^6.3.3",
"@codemirror/lang-php": "^6.0.2",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-rust": "^6.0.1",
"@codemirror/lang-rust": "^6.0.2",
"@codemirror/lang-sass": "^6.0.2",
"@codemirror/lang-sql": "^6.9.0",
"@codemirror/lang-vue": "^0.1.3",
"@codemirror/lang-wast": "^6.0.2",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.11.1",
"@codemirror/language": "^6.11.2",
"@codemirror/language-data": "^6.5.1",
"@codemirror/legacy-modes": "^6.5.1",
"@codemirror/lint": "^6.8.5",
"@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.37.1",
"@codemirror/view": "^6.38.1",
"@lezer/highlight": "^1.2.1",
"@types/uuid": "^10.0.0",
"@vueuse/core": "^13.3.0",
"codemirror": "^6.0.1",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.3.0",
"sass": "^1.89.1",
"uuid": "^11.1.0",
"vue": "^3.5.16",
"vue-i18n": "^11.1.5",
"@lezer/lr": "^1.4.2",
"codemirror": "^6.0.2",
"codemirror-lang-elixir": "^4.0.0",
"colors-named": "^1.0.2",
"colors-named-hex": "^1.0.2",
"franc-min": "^6.2.0",
"hsl-matcher": "^1.2.4",
"lezer": "^0.13.5",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.4.1",
"prettier": "^3.6.2",
"remarkable": "^2.0.1",
"sass": "^1.89.2",
"vue": "^3.5.17",
"vue-i18n": "^11.1.10",
"vue-pick-colors": "^1.8.0",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@eslint/js": "^9.28.0",
"@types/node": "^22.15.29",
"@vitejs/plugin-vue": "^5.2.4",
"@eslint/js": "^9.31.0",
"@lezer/generator": "^1.8.0",
"@types/node": "^24.0.14",
"@types/remarkable": "^2.0.8",
"@vitejs/plugin-vue": "^6.0.0",
"@wailsio/runtime": "latest",
"eslint": "^9.28.0",
"eslint-plugin-vue": "^10.1.0",
"globals": "^16.2.0",
"eslint": "^9.31.0",
"eslint-plugin-vue": "^10.3.0",
"globals": "^16.3.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.33.1",
"unplugin-vue-components": "^28.7.0",
"vite": "^6.3.5",
"vue-eslint-parser": "^10.1.3",
"vue-tsc": "^2.2.10"
"typescript-eslint": "^8.37.0",
"unplugin-vue-components": "^28.8.0",
"vite": "^7.0.4",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.0.1"
}
}

28
frontend/public/guesslang.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,48 @@
importScripts("guesslang.min.js")
const LANGUAGES = ["json", "py", "html", "sql", "md", "java", "php", "css", "xml", "cpp", "rs", "cs", "rb", "sh", "yaml", "toml", "go", "clj", "ex", "erl", "js", "ts", "swift", "kt", "groovy", "ps1", "dart", "scala"]
const guessLang = new self.GuessLang()
function sendResult(language, confidence, idx) {
postMessage({language, confidence, idx})
}
onmessage = (event) => {
const {content, idx} = event.data
// JSON 快速检测
const trimmed = content.trim()
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
try {
if (typeof JSON.parse(trimmed) === "object") {
sendResult("json", 1.0, idx)
return
}
} catch (e) {
}
}
guessLang.runModel(content).then((result) => {
if (result.length > 0) {
const lang = result[0]
if (LANGUAGES.includes(lang.languageId) && lang.confidence > 0.15) {
sendResult(lang.languageId, lang.confidence, idx)
return
}
}
for (let lang of result) {
if (LANGUAGES.includes(lang.languageId) && lang.confidence > 0.5) {
sendResult(lang.languageId, lang.confidence, idx)
return
}
}
sendResult("text", 0.0, idx)
}).catch(() => {
sendResult("text", 0.0, idx)
})
}

View File

@@ -1,23 +1,44 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { useConfigStore } from '@/stores/configStore';
import { useSystemTheme } from '@/composables/useSystemTheme';
import {onMounted} from 'vue';
import {useConfigStore} from '@/stores/configStore';
import {useSystemStore} from '@/stores/systemStore';
import {useKeybindingStore} from '@/stores/keybindingStore';
import {useThemeStore} from '@/stores/themeStore';
import {useUpdateStore} from '@/stores/updateStore';
import {useBackupStore} from '@/stores/backupStore';
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
const configStore = useConfigStore();
const { setTheme } = useSystemTheme();
const systemStore = useSystemStore();
const keybindingStore = useKeybindingStore();
const themeStore = useThemeStore();
const updateStore = useUpdateStore();
const backupStore = useBackupStore();
// 应用启动时加载配置
// 应用启动时加载配置和初始化系统信息
onMounted(async () => {
await configStore.initConfig();
// 并行初始化配置、系统信息和快捷键配置
await Promise.all([
configStore.initConfig(),
systemStore.initializeSystemInfo(),
keybindingStore.loadKeyBindings(),
]);
// 初始化语言和主题
await configStore.initializeLanguage();
setTheme(configStore.config.appearance.systemTheme);
themeStore.initializeTheme();
// 初始化备份服务
await backupStore.initialize();
// 启动时检查更新
await updateStore.checkOnStartup();
});
</script>
<template>
<div class="app-container">
<WindowTitleBar />
<WindowTitleBar/>
<div class="app-content">
<router-view/>
</div>
@@ -36,7 +57,6 @@ onMounted(async () => {
.app-content {
flex: 1;
margin-top: 32px;
overflow: hidden;
position: relative;
}

View File

@@ -23,6 +23,16 @@
--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;
@@ -45,6 +55,16 @@
--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);
@@ -68,6 +88,17 @@
--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;
}
@@ -96,6 +127,16 @@
--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);
}
}
@@ -123,6 +164,16 @@
--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);
}
}
@@ -149,6 +200,16 @@
--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);
}
/* 手动选择深色主题 */
@@ -174,4 +235,14 @@
--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

@@ -0,0 +1,177 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
const props = defineProps({
text: {
type: String,
default: 'LOADING'
}
});
const characters = ref<HTMLSpanElement[]>([]);
const isDone = ref(false);
const cycleCount = 5;
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()-_=+{}|[]\\;\':"<>?,./`~'.split('');
let animationFrameId: number | null = null;
let resetTimeoutId: number | null = null;
// 将字符串拆分为单个字符的span
function letterize() {
const container = document.querySelector('.loading-word');
if (!container) return;
// 清除现有内容
container.innerHTML = '';
// 为每个字符创建span
for (let i = 0; i < props.text.length; i++) {
const span = document.createElement('span');
span.setAttribute('data-orig', props.text[i]);
span.textContent = '-';
span.className = `char${i+1}`;
container.appendChild(span);
}
// 获取所有span元素
characters.value = Array.from(container.querySelectorAll('span'));
}
// 获取随机字符
function getRandomChar() {
return chars[Math.floor(Math.random() * chars.length)];
}
// 动画循环
function animationLoop() {
let currentCycle = 0;
let currentLetterIndex = 0;
let isAnimationDone = false;
function loop() {
// 为未完成的字符设置随机字符和不透明度
for (let i = currentLetterIndex; i < characters.value.length; i++) {
const char = characters.value[i];
if (!char.classList.contains('done')) {
char.textContent = getRandomChar();
char.style.opacity = Math.random().toString();
}
}
if (currentCycle < cycleCount) {
// 继续当前周期
currentCycle++;
} else if (currentLetterIndex < characters.value.length) {
// 当前周期结束,显示下一个字符的原始值
const currentChar = characters.value[currentLetterIndex];
currentChar.textContent = currentChar.getAttribute('data-orig') || '';
currentChar.style.opacity = '1';
currentChar.classList.add('done');
currentLetterIndex++;
currentCycle = 0;
} else {
// 所有字符都已显示
isAnimationDone = true;
isDone.value = true;
}
if (!isAnimationDone) {
animationFrameId = requestAnimationFrame(loop);
} else {
// 等待一段时间后重置动画
resetTimeoutId = window.setTimeout(() => {
reset();
}, 750);
}
}
loop();
}
// 重置动画
function reset() {
isDone.value = false;
for (const char of characters.value) {
char.textContent = char.getAttribute('data-orig') || '';
char.classList.remove('done');
}
animationLoop();
}
// 清理所有定时器
function cleanup() {
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
if (resetTimeoutId !== null) {
clearTimeout(resetTimeoutId);
resetTimeoutId = null;
}
}
onMounted(() => {
letterize();
animationLoop();
});
onBeforeUnmount(() => {
cleanup();
});
</script>
<template>
<div class="loading-screen">
<div class="loading-word"></div>
<div class="overlay"></div>
</div>
</template>
<style scoped lang="scss">
.loading-screen {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--voidraft-bg-gradient, radial-gradient(#222922, #000500));
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--voidraft-mono-font, monospace),serif;
}
.loading-word {
color: var(--voidraft-loading-color, #fff);
font-size: 2.5em;
height: 2.5em;
line-height: 2.5em;
text-align: center;
text-shadow: var(--voidraft-loading-glow, 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5));
}
.loading-word span {
display: inline-block;
transform: translateX(100%) scale(0.9);
transition: transform 500ms;
}
.loading-word .done {
color: var(--voidraft-loading-done-color, #6f6);
transform: translateX(0) scale(1);
}
.overlay {
background-image: var(--voidraft-loading-overlay, linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%));
background-size: 1000px 2px;
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
pointer-events: none;
}
</style>

View File

@@ -1,18 +1,40 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import { ref, onMounted, onUnmounted, nextTick, computed, watch } from 'vue';
import { SystemService } from '@/../bindings/voidraft/internal/services';
import type { MemoryStats } from '@/../bindings/voidraft/internal/services';
import { useI18n } from 'vue-i18n';
import { useThemeStore } from '@/stores/themeStore';
import { SystemThemeType } from '@/../bindings/voidraft/internal/models/models';
const { t } = useI18n();
const themeStore = useThemeStore();
const memoryStats = ref<MemoryStats | null>(null);
const formattedMemory = ref('');
const isLoading = ref(true);
const canvasRef = ref<HTMLCanvasElement | null>(null);
let intervalId: ReturnType<typeof setInterval> | null = null;
// 存储历史数据点 (最近60个数据点每3秒一个点总共3分钟历史)
// 存储历史数据点 (最近60个数据点)
const historyData = ref<number[]>([]);
const maxDataPoints = 60;
// 动态最大内存值MB初始为200MB会根据实际使用动态调整
const maxMemoryMB = ref(200);
// 使用themeStore获取当前主题
const isDarkTheme = computed(() => {
const theme = themeStore.currentTheme;
if (theme === SystemThemeType.SystemThemeAuto) {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
return theme === SystemThemeType.SystemThemeDark;
});
// 监听主题变化,重新绘制图表
watch(() => themeStore.currentTheme, () => {
nextTick(() => drawChart());
});
// 静默错误处理包装器
const withSilentErrorHandling = async <T>(
operation: () => Promise<T>,
@@ -47,8 +69,14 @@ const fetchMemoryStats = async () => {
formattedMemory.value = `${heapMB.toFixed(0)}M`;
}
// 添加新数据点到历史记录
const memoryUsagePercent = Math.min((stats.heapInUse / (100 * 1024 * 1024)) * 100, 100);
// 自动调整最大内存值,确保图表能够显示更大范围
if (heapMB > maxMemoryMB.value * 0.8) {
// 如果内存使用超过当前最大值的80%则将最大值调整为当前使用值的2倍
maxMemoryMB.value = Math.ceil(heapMB * 2);
}
// 添加新数据点到历史记录 - 使用动态最大值计算百分比
const memoryUsagePercent = Math.min((heapMB / maxMemoryMB.value) * 100, 100);
historyData.value.push(memoryUsagePercent);
// 保持最大数据点数量
@@ -62,7 +90,7 @@ const fetchMemoryStats = async () => {
isLoading.value = false;
};
// 绘制实时曲线图
// 绘制实时曲线图 - 简化版
const drawChart = () => {
if (!canvasRef.value || historyData.value.length === 0) return;
@@ -82,11 +110,16 @@ const drawChart = () => {
// 清除画布
ctx.clearRect(0, 0, width, height);
// 绘制背景网格 - 朦胧的网格,从上到下逐渐清晰
for (let i = 0; i <= 6; i++) {
const y = (height / 6) * i;
const opacity = 0.01 + (i / 6) * 0.03; // 从上到下逐渐清晰
ctx.strokeStyle = `rgba(255, 255, 255, ${opacity})`;
// 根据主题选择合适的颜色 - 更柔和的颜色
const gridColor = isDarkTheme.value ? 'rgba(255, 255, 255, 0.03)' : 'rgba(0, 0, 0, 0.07)';
const lineColor = isDarkTheme.value ? 'rgba(74, 158, 255, 0.6)' : 'rgba(37, 99, 235, 0.6)';
const fillColor = isDarkTheme.value ? 'rgba(74, 158, 255, 0.05)' : 'rgba(37, 99, 235, 0.05)';
const pointColor = isDarkTheme.value ? 'rgba(74, 158, 255, 0.8)' : 'rgba(37, 99, 235, 0.8)';
// 绘制背景网格 - 更加柔和
for (let i = 0; i <= 4; i++) {
const y = (height / 4) * i;
ctx.strokeStyle = gridColor;
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(0, y);
@@ -95,9 +128,9 @@ const drawChart = () => {
}
// 垂直网格线
for (let i = 0; i <= 8; i++) {
const x = (width / 8) * i;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.02)';
for (let i = 0; i <= 6; i++) {
const x = (width / 6) * i;
ctx.strokeStyle = gridColor;
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(x, 0);
@@ -112,13 +145,7 @@ const drawChart = () => {
const stepX = width / (maxDataPoints - 1);
const startX = width - (dataLength - 1) * stepX;
// 绘制填充区域 - 从上朦胧到下清晰的渐变
const gradient = ctx.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, 'rgba(74, 158, 255, 0.1)'); // 顶部很淡
gradient.addColorStop(0.3, 'rgba(74, 158, 255, 0.15)');
gradient.addColorStop(0.7, 'rgba(74, 158, 255, 0.25)');
gradient.addColorStop(1, 'rgba(74, 158, 255, 0.4)'); // 底部较浓
// 绘制填充区域 - 更柔和的填充
ctx.beginPath();
ctx.moveTo(startX, height);
@@ -126,17 +153,23 @@ const drawChart = () => {
const firstY = height - (historyData.value[0] / 100) * height;
ctx.lineTo(startX, firstY);
// 使用二次贝塞尔曲线平滑曲线
// 绘制数据点路径 - 使用曲线连接点,确保连续性
for (let i = 1; i < dataLength; i++) {
const x = startX + i * stepX;
const y = height - (historyData.value[i] / 100) * height;
// 使用贝塞尔曲线平滑连接
if (i < dataLength - 1) {
const nextX = startX + (i + 1) * stepX;
const nextY = height - (historyData.value[i + 1] / 100) * height;
const controlX = x + stepX / 2;
const controlY = y;
ctx.quadraticCurveTo(controlX, controlY, (x + nextX) / 2, (y + nextY) / 2);
const cpX1 = x - stepX / 4;
const cpY1 = y;
const cpX2 = x + stepX / 4;
const cpY2 = nextY;
// 使用三次贝塞尔曲线平滑连接点
ctx.bezierCurveTo(cpX1, cpY1, cpX2, cpY2, nextX, nextY);
i++; // 跳过下一个点,因为已经在曲线中处理了
} else {
ctx.lineTo(x, y);
}
@@ -146,68 +179,55 @@ const drawChart = () => {
const lastX = startX + (dataLength - 1) * stepX;
ctx.lineTo(lastX, height);
ctx.closePath();
ctx.fillStyle = gradient;
ctx.fillStyle = fillColor;
ctx.fill();
// 绘制主曲线 - 从上到下逐渐清晰
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// 绘制主曲线 - 平滑连续的曲线
ctx.beginPath();
ctx.moveTo(startX, firstY);
// 分段绘制曲线,每段有不同的透明度
const segments = 10;
for (let seg = 0; seg < segments; seg++) {
const segmentStart = seg / segments;
const segmentEnd = (seg + 1) / segments;
const opacity = 0.3 + (seg / segments) * 0.7; // 从上0.3到下1.0
// 重新绘制曲线路径,但这次只绘制线条
for (let i = 1; i < dataLength; i++) {
const x = startX + i * stepX;
const y = height - (historyData.value[i] / 100) * height;
ctx.strokeStyle = `rgba(74, 158, 255, ${opacity})`;
ctx.lineWidth = 1.5 + (seg / segments) * 0.8; // 线条也从细到粗
ctx.beginPath();
let segmentStarted = false;
for (let i = 0; i < dataLength; i++) {
const x = startX + i * stepX;
const y = height - (historyData.value[i] / 100) * height;
const yPercent = 1 - (y / height);
// 使用贝塞尔曲线平滑连接
if (i < dataLength - 1) {
const nextX = startX + (i + 1) * stepX;
const nextY = height - (historyData.value[i + 1] / 100) * height;
const cpX1 = x - stepX / 4;
const cpY1 = y;
const cpX2 = x + stepX / 4;
const cpY2 = nextY;
if (yPercent >= segmentStart && yPercent <= segmentEnd) {
if (!segmentStarted) {
ctx.moveTo(x, y);
segmentStarted = true;
} else {
if (i < dataLength - 1) {
const nextX = startX + (i + 1) * stepX;
const nextY = height - (historyData.value[i + 1] / 100) * height;
const controlX = x + stepX / 2;
const controlY = y;
ctx.quadraticCurveTo(controlX, controlY, (x + nextX) / 2, (y + nextY) / 2);
} else {
ctx.lineTo(x, y);
}
}
}
}
if (segmentStarted) {
ctx.stroke();
// 使用三次贝塞尔曲线平滑连接点
ctx.bezierCurveTo(cpX1, cpY1, cpX2, cpY2, nextX, nextY);
i++; // 跳过下一个点,因为已经在曲线中处理了
} else {
ctx.lineTo(x, y);
}
}
// 绘制当前值的高亮点 - 根据位置调整透明度
ctx.strokeStyle = lineColor;
ctx.lineWidth = 1.5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.stroke();
// 绘制当前值的高亮点
const lastY = height - (historyData.value[dataLength - 1] / 100) * height;
const pointOpacity = 0.4 + (1 - lastY / height) * 0.6;
// 外圈
ctx.fillStyle = `rgba(74, 158, 255, ${pointOpacity * 0.3})`;
ctx.fillStyle = pointColor;
ctx.globalAlpha = 0.4;
ctx.beginPath();
ctx.arc(lastX, lastY, 4, 0, Math.PI * 2);
ctx.arc(lastX, lastY, 3, 0, Math.PI * 2);
ctx.fill();
// 内圈
ctx.fillStyle = `rgba(74, 158, 255, ${pointOpacity})`;
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.arc(lastX, lastY, 2, 0, Math.PI * 2);
ctx.arc(lastX, lastY, 1.5, 0, Math.PI * 2);
ctx.fill();
};
@@ -228,31 +248,59 @@ const handleResize = () => {
}
};
// 仅监听系统主题变化
const setupSystemThemeListener = () => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemThemeChange = () => {
// 仅当设置为auto时才响应系统主题变化
if (themeStore.currentTheme === SystemThemeType.SystemThemeAuto) {
nextTick(() => drawChart());
}
};
// 添加监听器
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleSystemThemeChange);
}
// 返回清理函数
return () => {
if (mediaQuery.removeEventListener) {
mediaQuery.removeEventListener('change', handleSystemThemeChange);
}
};
};
onMounted(() => {
fetchMemoryStats();
// 每3秒更新一次内存信息
// 每1秒更新一次内存信息
intervalId = setInterval(fetchMemoryStats, 3000);
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
if (intervalId) {
clearInterval(intervalId);
}
window.removeEventListener('resize', handleResize);
// 设置系统主题监听器仅用于auto模式
const cleanupThemeListener = setupSystemThemeListener();
// 在卸载时清理
onUnmounted(() => {
if (intervalId) {
clearInterval(intervalId);
}
window.removeEventListener('resize', handleResize);
cleanupThemeListener();
});
});
</script>
<template>
<div class="memory-monitor" @click="triggerGC" :title="`内存: ${formattedMemory} | 点击清理内存`">
<div class="memory-monitor" @click="triggerGC" :title="`${t('monitor.memory')}: ${formattedMemory} | ${t('monitor.clickToClean')}`">
<div class="monitor-info">
<div class="memory-label">
<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="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
</svg>
<span>内存</span>
<span>{{ t('monitor.memory') }}</span>
</div>
<div class="memory-value" v-if="!isLoading">{{ formattedMemory }}</div>
<div class="memory-loading" v-else>--</div>
@@ -279,11 +327,11 @@ onUnmounted(() => {
&:hover {
.monitor-info {
.memory-label {
color: #4a9eff;
color: var(--selection-text);
}
.memory-value {
color: #ffffff;
color: var(--toolbar-text);
}
}
@@ -301,7 +349,7 @@ onUnmounted(() => {
display: flex;
align-items: center;
gap: 4px;
color: #a0a0a0;
color: var(--text-secondary);
font-size: 10px;
font-weight: 500;
transition: color 0.2s ease;
@@ -318,7 +366,7 @@ onUnmounted(() => {
}
.memory-value, .memory-loading {
color: #e0e0e0;
color: var(--toolbar-text-secondary);
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-size: 9px;
font-weight: 600;

View File

@@ -0,0 +1,297 @@
<template>
<div class="linux-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
<div class="titlebar-icon">
<img src="/appicon.png" alt="voidraft" />
</div>
<div class="titlebar-title">{{ titleText }}</div>
</div>
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
<button
class="titlebar-button minimize-button"
@click="minimizeWindow"
:title="t('titlebar.minimize')"
>
<svg width="16" height="16" viewBox="0 0 16 16">
<path d="M4 8h8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<button
class="titlebar-button maximize-button"
@click="toggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
>
<svg width="16" height="16" viewBox="0 0 16 16" v-if="!isMaximized">
<rect x="4" y="4" width="8" height="8" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>
<svg width="16" height="16" viewBox="0 0 16 16" v-else>
<rect x="3" y="5" width="6" height="6" fill="none" stroke="currentColor" stroke-width="1.5"/>
<rect x="7" y="3" width="6" height="6" fill="none" stroke="currentColor" stroke-width="1.5"/>
</svg>
</button>
<button
class="titlebar-button close-button"
@click="closeWindow"
:title="t('titlebar.close')"
>
<svg width="16" height="16" viewBox="0 0 16 16">
<path d="M4 4l8 8m0-8L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import * as runtime from '@wailsio/runtime';
import { useWindowStore } from '@/stores/windowStore';
import { useDocumentStore } from '@/stores/documentStore';
const { t } = useI18n();
const isMaximized = ref(false);
const documentStore = useDocumentStore();
const minimizeWindow = async () => {
try {
await runtime.Window.Minimise();
} catch (error) {
// Error handling
}
};
const toggleMaximize = async () => {
try {
const newState = !isMaximized.value;
isMaximized.value = newState;
if (newState) {
await runtime.Window.Maximise();
} else {
await runtime.Window.UnMaximise();
}
setTimeout(async () => {
await checkMaximizedState();
}, 100);
} catch (error) {
isMaximized.value = !isMaximized.value;
}
};
const closeWindow = async () => {
try {
await runtime.Window.Close();
} catch (error) {
// Error handling
}
};
const checkMaximizedState = async () => {
try {
isMaximized.value = await runtime.Window.IsMaximised();
} catch (error) {
// Error handling
}
};
// 计算标题文本
const titleText = computed(() => {
const currentDoc = documentStore.currentDocument;
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
});
onMounted(async () => {
await checkMaximizedState();
runtime.Events.On('window:maximised', () => {
isMaximized.value = true;
});
runtime.Events.On('window:unmaximised', () => {
isMaximized.value = false;
});
runtime.Events.On('window:focus', async () => {
await checkMaximizedState();
});
});
onUnmounted(() => {
runtime.Events.Off('window:maximised');
runtime.Events.Off('window:unmaximised');
runtime.Events.Off('window:focus');
});
</script>
<style scoped lang="scss">
.linux-titlebar {
display: flex;
height: 34px;
background: var(--toolbar-bg, linear-gradient(to bottom, #f6f6f6, #e8e8e8));
border-bottom: 1px solid var(--toolbar-border, #d0d0d0);
user-select: none;
-webkit-user-select: none;
width: 100%;
font-family: 'Ubuntu', 'Cantarell', 'DejaVu Sans', system-ui, sans-serif;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
}
.titlebar-content {
display: flex;
align-items: center;
flex: 1;
padding-left: 12px;
gap: 8px;
color: var(--toolbar-text, #333);
font-size: 13px;
font-weight: 500;
cursor: default;
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
}
.titlebar-content .titlebar-icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.titlebar-title {
font-size: 13px;
color: var(--toolbar-text, #333);
font-weight: 500;
}
.titlebar-controls {
display: flex;
height: 100%;
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
}
.titlebar-button {
width: 36px;
height: 34px;
border: none;
background: transparent;
color: var(--toolbar-text, #555);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
padding: 0;
margin: 0;
border-radius: 0;
svg {
width: 16px;
height: 16px;
opacity: 0.8;
transition: opacity 0.15s ease;
}
&:hover {
background: var(--toolbar-button-hover, rgba(0, 0, 0, 0.1));
svg {
opacity: 1;
}
}
&:active {
background: var(--toolbar-button-active, rgba(0, 0, 0, 0.15));
}
}
.close-button {
&:hover {
background: #e74c3c;
color: #ffffff;
svg {
opacity: 1;
}
}
&:active {
background: #c0392b;
}
}
// Dark theme support
@media (prefers-color-scheme: dark) {
.linux-titlebar {
background: var(--toolbar-bg, linear-gradient(to bottom, #3c3c3c, #2e2e2e));
border-bottom-color: var(--toolbar-border, #1e1e1e);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.titlebar-content,
.titlebar-title {
color: var(--toolbar-text, #f0f0f0);
}
.titlebar-button {
color: var(--toolbar-text, #ccc);
&:hover {
background: var(--toolbar-button-hover, rgba(255, 255, 255, 0.1));
}
&:active {
background: var(--toolbar-button-active, rgba(255, 255, 255, 0.15));
}
}
}
// GNOME-like styling variant
.linux-titlebar.gnome-style {
height: 38px;
border-radius: 12px 12px 0 0;
.titlebar-button {
height: 38px;
width: 32px;
border-radius: 6px;
margin: 3px 2px;
&:hover {
background: rgba(255, 255, 255, 0.1);
}
}
}
// KDE-like styling variant
.linux-titlebar.kde-style {
background: var(--toolbar-bg, #eff0f1);
border-bottom: 1px solid var(--toolbar-border, #bdc3c7);
.titlebar-button {
border-radius: 4px;
margin: 2px 1px;
&:hover {
background: rgba(61, 174, 233, 0.2);
}
}
}
</style>

View File

@@ -0,0 +1,271 @@
<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')"
>
<div class="button-icon">
<svg width="6" height="6" viewBox="0 0 6 6" v-show="showControlIcons">
<path d="M1 1l4 4m0-4L1 5" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
</svg>
</div>
</button>
<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">
<path d="M0 0h8" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
</svg>
</div>
</button>
<button
class="titlebar-button maximize-button"
@click="toggleMaximize"
: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">
<path d="M1 1l4 0 0 4-4 0z" fill="none" stroke="currentColor" stroke-width="1"/>
<path d="M2 2l2 0 0 2" fill="none" stroke="currentColor" stroke-width="1"/>
</svg>
<svg width="6" height="6" viewBox="0 0 6 6" v-show="showControlIcons && isMaximized">
<path d="M1 2l4 0 0 3-4 0z" fill="none" stroke="currentColor" stroke-width="1"/>
<path d="M2 1l3 0 0 3" fill="none" stroke="currentColor" stroke-width="1"/>
</svg>
</div>
</button>
</div>
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
<div class="titlebar-title">{{ titleText }}</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import * as runtime from '@wailsio/runtime';
import { useWindowStore } from '@/stores/windowStore';
import { useDocumentStore } from '@/stores/documentStore';
const { t } = useI18n();
const isMaximized = ref(false);
const showControlIcons = ref(false);
const documentStore = useDocumentStore();
const minimizeWindow = async () => {
try {
await runtime.Window.Minimise();
} catch (error) {
// Error handling
}
};
const toggleMaximize = async () => {
try {
const newState = !isMaximized.value;
isMaximized.value = newState;
if (newState) {
await runtime.Window.Maximise();
} else {
await runtime.Window.UnMaximise();
}
setTimeout(async () => {
await checkMaximizedState();
}, 100);
} catch (error) {
isMaximized.value = !isMaximized.value;
}
};
const closeWindow = async () => {
try {
await runtime.Window.Close();
} catch (error) {
// Error handling
}
};
const checkMaximizedState = async () => {
try {
isMaximized.value = await runtime.Window.IsMaximised();
} catch (error) {
// Error handling
}
};
// 计算标题文本
const titleText = computed(() => {
const currentDoc = documentStore.currentDocument;
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
});
onMounted(async () => {
await checkMaximizedState();
runtime.Events.On('window:maximised', () => {
isMaximized.value = true;
});
runtime.Events.On('window:unmaximised', () => {
isMaximized.value = false;
});
runtime.Events.On('window:focus', async () => {
await checkMaximizedState();
});
});
onUnmounted(() => {
runtime.Events.Off('window:maximised');
runtime.Events.Off('window:unmaximised');
runtime.Events.Off('window:focus');
});
</script>
<style scoped lang="scss">
.macos-titlebar {
display: flex;
height: 28px;
background: var(--toolbar-bg, #ececec);
border-bottom: 1px solid var(--toolbar-border, rgba(0, 0, 0, 0.1));
user-select: none;
-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 {
opacity: 1;
}
}
}
}
.titlebar-controls {
display: flex;
height: 100%;
align-items: center;
padding-left: 8px;
gap: 8px;
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
}
.titlebar-button {
width: 12px;
height: 12px;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
padding: 0;
margin: 0;
position: relative;
.button-icon {
opacity: 0;
transition: opacity 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: rgba(0, 0, 0, 0.7);
}
&:hover .button-icon {
opacity: 1;
}
}
.close-button {
background: #ff5f57;
&:hover {
background: #ff453a;
}
&:active {
background: #d7463f;
}
}
.minimize-button {
background: #ffbd2e;
&:hover {
background: #ffb524;
}
&:active {
background: #e6a220;
}
}
.maximize-button {
background: #28ca42;
&:hover {
background: #1ebe36;
}
&:active {
background: #1ba932;
}
}
.titlebar-content {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
cursor: default;
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
}
.titlebar-title {
font-size: 13px;
font-weight: 500;
color: var(--toolbar-text, #333);
text-align: center;
}
@media (prefers-color-scheme: dark) {
.macos-titlebar {
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>

View File

@@ -1,250 +1,17 @@
<template>
<div class="windows-titlebar" style="--wails-draggable:drag" @contextmenu.prevent @mouseenter="checkMaximizedState" @mouseup="checkMaximizedState">
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
<div class="titlebar-icon">
<img src="/appicon.png" alt="voidraft" />
</div>
<div class="titlebar-title">voidraft</div>
</div>
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
<button
class="titlebar-button minimize-button"
@click="minimizeWindow"
:title="t('titlebar.minimize')"
>
<span class="titlebar-icon">&#xE921;</span>
</button>
<button
class="titlebar-button maximize-button"
@click="toggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
>
<span class="titlebar-icon" v-html="maximizeIcon"></span>
</button>
<button
class="titlebar-button close-button"
@click="closeWindow"
:title="t('titlebar.close')"
>
<span class="titlebar-icon">&#xE8BB;</span>
</button>
</div>
</div>
<WindowsTitleBar v-if="systemStore.isWindows" />
<MacOSTitleBar v-else-if="systemStore.isMacOS" />
<LinuxTitleBar v-else />
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import * as runtime from '@wailsio/runtime';
import { useSystemStore } from '@/stores/systemStore';
import WindowsTitleBar from './WindowsTitleBar.vue';
import MacOSTitleBar from './MacOSTitleBar.vue';
import LinuxTitleBar from './LinuxTitleBar.vue';
const { t } = useI18n();
const isMaximized = ref(false);
// 计算属性用于图标,减少重复渲染
const maximizeIcon = computed(() => isMaximized.value ? '&#xE923;' : '&#xE922;');
const minimizeWindow = async () => {
try {
await runtime.Window.Minimise();
} catch (error) {
}
};
const toggleMaximize = async () => {
try {
// 立即更新UI状态提供即时反馈
const newState = !isMaximized.value;
isMaximized.value = newState;
// 然后执行实际操作
if (newState) {
await runtime.Window.Maximise();
} else {
await runtime.Window.UnMaximise();
}
// 操作完成后再次确认状态(防止操作失败时状态不一致)
setTimeout(async () => {
await checkMaximizedState();
}, 100);
} catch (error) {
// 如果操作失败,恢复原状态
isMaximized.value = !isMaximized.value;
}
};
const closeWindow = async () => {
try {
// 使用Window的Close方法会触发窗口关闭事件
await runtime.Window.Close();
} catch (error) {
}
};
const checkMaximizedState = async () => {
try {
isMaximized.value = await runtime.Window.IsMaximised();
} catch (error) {
}
};
onMounted(async () => {
// 检查初始最大化状态
await checkMaximizedState();
// 监听窗口状态变化事件
runtime.Events.On('window:maximised', () => {
isMaximized.value = true;
});
runtime.Events.On('window:unmaximised', () => {
isMaximized.value = false;
});
// 监听窗口焦点事件,确保状态同步
runtime.Events.On('window:focus', async () => {
await checkMaximizedState();
});
onUnmounted(() => {
// 清理事件监听器
runtime.Events.Off('window:maximised');
runtime.Events.Off('window:unmaximised');
runtime.Events.Off('window:focus');
});
});
const systemStore = useSystemStore();
</script>
<style scoped lang="scss">
.windows-titlebar {
display: flex;
height: 32px;
background: var(--toolbar-bg);
border-bottom: 1px solid var(--toolbar-border);
user-select: none;
-webkit-user-select: none;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
/* 禁用右键菜单 */
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
}
.titlebar-content {
display: flex;
align-items: center;
flex: 1;
padding-left: 8px;
gap: 8px;
color: var(--toolbar-text);
font-size: 12px;
font-weight: 400;
cursor: default;
/* 禁用右键菜单 */
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
}
.titlebar-content .titlebar-icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.titlebar-title {
font-size: 12px;
color: var(--toolbar-text);
}
.titlebar-controls {
display: flex;
height: 100%;
/* 禁用右键菜单 */
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
}
.titlebar-button {
width: 46px;
height: 32px;
border: none;
background: transparent;
color: var(--toolbar-text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.1s ease;
padding: 0;
margin: 0;
&:hover {
background: var(--toolbar-button-hover);
}
&:active {
background: var(--toolbar-button-hover);
opacity: 0.8;
}
}
.titlebar-button .titlebar-icon {
font-family: 'Segoe MDL2 Assets', 'Segoe UI Symbol', 'Segoe UI', system-ui;
font-size: 9px;
line-height: 1;
display: inline-block;
opacity: 0.9;
transition: opacity 0.1s ease;
.titlebar-button:hover & {
opacity: 1;
}
}
.minimize-button:hover,
.maximize-button:hover {
background: var(--toolbar-button-hover);
}
.minimize-button:active,
.maximize-button:active {
background: var(--toolbar-button-hover);
opacity: 0.8;
}
.close-button:hover {
background: #c42b1c;
color: #ffffff;
.titlebar-icon {
opacity: 1;
}
}
.close-button:active {
background: #a93226;
.titlebar-icon {
opacity: 1;
}
}
</style>

View File

@@ -0,0 +1,250 @@
<template>
<div class="windows-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
<div class="titlebar-icon">
<img src="/appicon.png" alt="voidraft"/>
</div>
<div class="titlebar-title">{{ titleText }}</div>
</div>
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
<button
class="titlebar-button minimize-button"
@click="minimizeWindow"
:title="t('titlebar.minimize')"
>
<span class="titlebar-icon">&#xE921;</span>
</button>
<button
class="titlebar-button maximize-button"
@click="toggleMaximize"
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
>
<span class="titlebar-icon" v-html="maximizeIcon"></span>
</button>
<button
class="titlebar-button close-button"
@click="closeWindow"
:title="t('titlebar.close')"
>
<span class="titlebar-icon">&#xE8BB;</span>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import {computed, onMounted, onUnmounted, ref} from 'vue';
import {useI18n} from 'vue-i18n';
import * as runtime from '@wailsio/runtime';
import { useWindowStore } from '@/stores/windowStore';
import { useDocumentStore } from '@/stores/documentStore';
const {t} = useI18n();
const isMaximized = ref(false);
const documentStore = useDocumentStore();
// 计算属性用于图标,减少重复渲染
const maximizeIcon = computed(() => isMaximized.value ? '&#xE923;' : '&#xE922;');
// 计算标题文本
const titleText = computed(() => {
const currentDoc = documentStore.currentDocument;
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
});
const minimizeWindow = async () => {
try {
await runtime.Window.Minimise();
} catch (error) {
// Error handling
}
};
const toggleMaximize = async () => {
try {
// 立即更新UI状态提供即时反馈
const newState = !isMaximized.value;
isMaximized.value = newState;
// 然后执行实际操作
if (newState) {
await runtime.Window.Maximise();
} else {
await runtime.Window.UnMaximise();
}
// 操作完成后再次确认状态(防止操作失败时状态不一致)
setTimeout(async () => {
await checkMaximizedState();
}, 100);
} catch (error) {
// 如果操作失败,恢复原状态
isMaximized.value = !isMaximized.value;
}
};
const closeWindow = async () => {
try {
await runtime.Window.Close();
} catch (error) {
// Error handling
}
};
const checkMaximizedState = async () => {
try {
isMaximized.value = await runtime.Window.IsMaximised();
} catch (error) {
// Error handling
}
};
onMounted(async () => {
await checkMaximizedState();
runtime.Events.On('window:maximised', () => {
isMaximized.value = true;
});
runtime.Events.On('window:unmaximised', () => {
isMaximized.value = false;
});
runtime.Events.On('window:focus', async () => {
await checkMaximizedState();
});
});
onUnmounted(() => {
runtime.Events.Off('window:maximised');
runtime.Events.Off('window:unmaximised');
runtime.Events.Off('window:focus');
});
</script>
<style scoped lang="scss">
.windows-titlebar {
display: flex;
height: 32px;
background: var(--toolbar-bg);
border-bottom: 1px solid var(--toolbar-border);
user-select: none;
-webkit-user-select: none;
width: 100%;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
}
.titlebar-content {
display: flex;
align-items: center;
flex: 1;
padding-left: 8px;
gap: 8px;
color: var(--toolbar-text);
font-size: 12px;
font-weight: 400;
cursor: default;
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
}
.titlebar-content .titlebar-icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.titlebar-title {
font-size: 12px;
color: var(--toolbar-text);
}
.titlebar-controls {
display: flex;
height: 100%;
-webkit-context-menu: none;
-moz-context-menu: none;
context-menu: none;
}
.titlebar-button {
width: 46px;
height: 32px;
border: none;
background: transparent;
color: var(--toolbar-text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.1s ease;
padding: 0;
margin: 0;
&:hover {
background: var(--toolbar-button-hover);
}
&:active {
background: var(--toolbar-button-hover);
opacity: 0.8;
}
}
.titlebar-button .titlebar-icon {
font-family: 'Segoe MDL2 Assets', 'Segoe UI Symbol', 'Segoe UI', system-ui;
font-size: 9px;
line-height: 1;
display: inline-block;
opacity: 0.9;
transition: opacity 0.1s ease;
.titlebar-button:hover & {
opacity: 1;
}
}
.minimize-button:hover,
.maximize-button:hover {
background: var(--toolbar-button-hover);
}
.minimize-button:active,
.maximize-button:active {
background: var(--toolbar-button-hover);
opacity: 0.8;
}
.close-button:hover {
background: #c42b1c;
color: #ffffff;
.titlebar-icon {
opacity: 1;
}
}
.close-button:active {
background: #a93226;
.titlebar-icon {
opacity: 1;
}
}
</style>

View File

@@ -0,0 +1,555 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useEditorStore } from '@/stores/editorStore';
import { SUPPORTED_LANGUAGES, type SupportedLanguage } from '@/views/editor/extensions/codeblock/types';
import { getActiveNoteBlock } from '@/views/editor/extensions/codeblock/state';
import { changeCurrentBlockLanguage } from '@/views/editor/extensions/codeblock/commands';
const { t } = useI18n();
const editorStore = useEditorStore();
// 组件状态
const showLanguageMenu = ref(false);
const searchQuery = ref('');
const searchInputRef = ref<HTMLInputElement>();
// 语言别名映射
const LANGUAGE_ALIASES: Record<SupportedLanguage, string> = {
auto: 'auto',
text: 'txt',
json: 'JSON',
py: 'python',
html: 'HTML',
sql: 'SQL',
md: 'markdown',
java: 'Java',
php: 'PHP',
css: 'CSS',
xml: 'XML',
cpp: 'c++',
rs: 'rust',
cs: 'c#',
rb: 'ruby',
sh: 'shell',
yaml: 'yml',
toml: 'TOML',
go: 'Go',
clj: 'clojure',
ex: 'elixir',
erl: 'erlang',
js: 'javascript',
ts: 'typescript',
swift: 'Swift',
kt: 'kotlin',
groovy: 'Groovy',
ps1: 'powershell',
dart: 'Dart',
scala: 'Scala'
};
// 语言显示名称映射
const LANGUAGE_NAMES: Record<SupportedLanguage, string> = {
auto: 'Auto',
text: 'Plain Text',
json: 'JSON',
py: 'Python',
html: 'HTML',
sql: 'SQL',
md: 'Markdown',
java: 'Java',
php: 'PHP',
css: 'CSS',
xml: 'XML',
cpp: 'C++',
rs: 'Rust',
cs: 'C#',
rb: 'Ruby',
sh: 'Shell',
yaml: 'YAML',
toml: 'TOML',
go: 'Go',
clj: 'Clojure',
ex: 'Elixir',
erl: 'Erlang',
js: 'JavaScript',
ts: 'TypeScript',
swift: 'Swift',
kt: 'Kotlin',
groovy: 'Groovy',
ps1: 'PowerShell',
dart: 'Dart',
scala: 'Scala'
};
// 当前活动块的语言信息
const currentBlockLanguage = ref<{ name: SupportedLanguage; auto: boolean }>({
name: 'text',
auto: false
});
// 事件监听器引用
const eventListeners = ref<{
updateListener?: () => void;
selectionUpdateListener?: () => void;
}>({});
// 更新当前块语言信息
const updateCurrentBlockLanguage = () => {
if (!editorStore.editorView) {
currentBlockLanguage.value = { name: 'text', auto: false };
return;
}
try {
const state = editorStore.editorView.state;
const activeBlock = getActiveNoteBlock(state as any);
if (activeBlock) {
const newLanguage = {
name: activeBlock.language.name as SupportedLanguage,
auto: activeBlock.language.auto
};
// 只有当语言信息实际发生变化时才更新
if (currentBlockLanguage.value.name !== newLanguage.name ||
currentBlockLanguage.value.auto !== newLanguage.auto) {
currentBlockLanguage.value = newLanguage;
}
} else {
if (currentBlockLanguage.value.name !== 'text' || currentBlockLanguage.value.auto !== false) {
currentBlockLanguage.value = { name: 'text', auto: false };
}
}
} catch (error) {
console.warn('Failed to get active block language:', error);
currentBlockLanguage.value = { name: 'text', auto: false };
}
};
// 清理事件监听器
const cleanupEventListeners = () => {
if (editorStore.editorView?.dom && eventListeners.value.updateListener) {
const dom = editorStore.editorView.dom;
dom.removeEventListener('click', eventListeners.value.updateListener);
dom.removeEventListener('keyup', eventListeners.value.updateListener);
dom.removeEventListener('keydown', eventListeners.value.updateListener);
dom.removeEventListener('focus', eventListeners.value.updateListener);
dom.removeEventListener('mouseup', eventListeners.value.updateListener);
if (eventListeners.value.selectionUpdateListener) {
dom.removeEventListener('selectionchange', eventListeners.value.selectionUpdateListener);
}
}
eventListeners.value = {};
};
// 设置事件监听器
const setupEventListeners = (view: any) => {
cleanupEventListeners();
// 监听编辑器状态更新
const updateListener = () => {
// 使用 requestAnimationFrame 确保在下一帧更新,性能更好
requestAnimationFrame(() => {
updateCurrentBlockLanguage();
});
};
// 监听选择变化
const selectionUpdateListener = () => {
requestAnimationFrame(() => {
updateCurrentBlockLanguage();
});
};
// 保存监听器引用
eventListeners.value = { updateListener, selectionUpdateListener };
// 监听关键事件:光标位置变化、文档变化、焦点变化
view.dom.addEventListener('click', updateListener);
view.dom.addEventListener('keyup', updateListener);
view.dom.addEventListener('keydown', updateListener);
view.dom.addEventListener('focus', updateListener);
view.dom.addEventListener('mouseup', updateListener); // 鼠标选择结束
// 监听编辑器的选择变化事件
if (view.dom.addEventListener) {
view.dom.addEventListener('selectionchange', selectionUpdateListener);
}
// 立即更新一次当前状态
updateCurrentBlockLanguage();
};
// 监听编辑器状态变化
watch(
() => editorStore.editorView,
(newView) => {
if (newView) {
setupEventListeners(newView);
} else {
cleanupEventListeners();
}
},
{ immediate: true }
);
// 过滤后的语言列表
const filteredLanguages = computed(() => {
if (!searchQuery.value) {
return SUPPORTED_LANGUAGES;
}
const query = searchQuery.value.toLowerCase();
return SUPPORTED_LANGUAGES.filter(langId => {
const name = LANGUAGE_NAMES[langId];
const alias = LANGUAGE_ALIASES[langId];
return langId.toLowerCase().includes(query) ||
(name && name.toLowerCase().includes(query)) ||
(alias && alias.toLowerCase().includes(query));
});
});
// 切换语言选择器显示状态
const toggleLanguageMenu = () => {
showLanguageMenu.value = !showLanguageMenu.value;
// 如果菜单打开,滚动到当前语言
if (showLanguageMenu.value) {
scrollToCurrentLanguage();
}
};
// 关闭语言选择器
const closeLanguageMenu = () => {
showLanguageMenu.value = false;
searchQuery.value = '';
};
// 选择语言
const selectLanguage = (languageId: SupportedLanguage) => {
if (!editorStore.editorView) {
closeLanguageMenu();
return;
}
try {
const view = editorStore.editorView;
const state = view.state;
const dispatch = view.dispatch;
let targetLanguage: string;
let autoDetect: boolean;
if (languageId === 'auto') {
// 设置为自动检测
targetLanguage = 'text';
autoDetect = true;
} else {
// 设置为指定语言,关闭自动检测
targetLanguage = languageId;
autoDetect = false;
}
// 使用修复后的函数来更改语言
const success = changeCurrentBlockLanguage(state as any, dispatch, targetLanguage, autoDetect);
if (success) {
// 立即更新当前语言状态
updateCurrentBlockLanguage();
}
} catch (error) {
console.warn('Failed to change block language:', error);
}
closeLanguageMenu();
};
// 点击外部关闭
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!target.closest('.block-language-selector')) {
closeLanguageMenu();
}
};
// 键盘事件处理
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closeLanguageMenu();
}
};
onMounted(() => {
document.addEventListener('click', handleClickOutside);
document.addEventListener('keydown', handleKeydown);
// 立即更新一次当前语言状态
updateCurrentBlockLanguage();
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
document.removeEventListener('keydown', handleKeydown);
cleanupEventListeners();
});
// 获取当前语言的显示名称
const getCurrentLanguageName = computed(() => {
const lang = currentBlockLanguage.value;
if (lang.auto) {
return `${lang.name} (auto)`;
}
return lang.name;
});
// 获取当前显示的语言选项
const getCurrentDisplayLanguage = computed(() => {
const lang = currentBlockLanguage.value;
if (lang.auto) {
return 'auto';
}
return lang.name;
});
// 滚动到当前选择的语言
const scrollToCurrentLanguage = () => {
nextTick(() => {
const currentLang = getCurrentDisplayLanguage.value;
const selectorElement = document.querySelector('.block-language-selector');
if (!selectorElement) return;
const languageList = selectorElement.querySelector('.language-list') as HTMLElement;
const activeOption = selectorElement.querySelector(`.language-option[data-language="${currentLang}"]`) as HTMLElement;
if (languageList && activeOption) {
// 使用 scrollIntoView 进行平滑滚动
activeOption.scrollIntoView({
behavior: 'auto',
block: 'nearest',
inline: 'nearest'
});
}
});
};
</script>
<template>
<div class="block-language-selector">
<button
class="language-btn"
:title="t('toolbar.blockLanguage')"
@click="toggleLanguageMenu"
>
<span class="language-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">
<polyline points="16 18 22 12 16 6"></polyline>
<polyline points="8 6 2 12 8 18"></polyline>
</svg>
</span>
<span class="language-name">{{ getCurrentLanguageName }}</span>
<span class="arrow" :class="{ 'open': showLanguageMenu }"></span>
</button>
<div class="language-menu" v-if="showLanguageMenu">
<!-- 搜索框 -->
<div class="search-container">
<input
ref="searchInputRef"
v-model="searchQuery"
type="text"
class="search-input"
:placeholder="t('toolbar.searchLanguage')"
@keydown.stop
/>
<svg class="search-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="language-list">
<div
v-for="language in filteredLanguages"
:key="language"
class="language-option"
:class="{ 'active': getCurrentDisplayLanguage === language }"
:data-language="language"
@click="selectLanguage(language)"
>
<span class="language-name">{{ LANGUAGE_NAMES[language] || language }}</span>
<span class="language-alias">{{ language }}</span>
</div>
<!-- 无结果提示 -->
<div v-if="filteredLanguages.length === 0" class="no-results">
{{ t('toolbar.noLanguageFound') }}
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.block-language-selector {
position: relative;
.language-btn {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 11px;
display: flex;
align-items: center;
gap: 3px;
padding: 2px 4px;
border-radius: 3px;
&:hover {
background-color: var(--border-color);
opacity: 0.8;
}
.language-icon {
display: flex;
align-items: center;
}
.language-name {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arrow {
font-size: 8px;
margin-left: 2px;
transition: transform 0.2s ease;
&.open {
transform: rotate(180deg);
}
}
}
.language-menu {
position: absolute;
bottom: 100%;
right: 0;
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 3px;
margin-bottom: 4px;
width: 220px;
max-height: 280px;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
overflow: hidden;
.search-container {
position: relative;
padding: 8px;
border-bottom: 1px solid var(--border-color);
.search-input {
width: 100%;
box-sizing: border-box;
background-color: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 2px;
padding: 5px 8px 5px 26px;
font-size: 11px;
color: var(--text-primary);
outline: none;
line-height: 1.2;
&:focus {
border-color: var(--text-muted);
}
&::placeholder {
color: var(--text-muted);
}
}
.search-icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
pointer-events: none;
}
}
.language-list {
max-height: 200px;
overflow-y: auto;
.language-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
cursor: pointer;
font-size: 11px;
&:hover {
background-color: var(--border-color);
opacity: 0.8;
}
&.active {
background-color: var(--selection-bg);
color: var(--selection-text);
.language-alias {
color: var(--selection-text);
opacity: 0.7;
}
}
.language-name {
font-weight: normal;
}
.language-alias {
font-size: 10px;
color: var(--text-muted);
opacity: 0.6;
}
}
.no-results {
padding: 12px 8px;
text-align: center;
color: var(--text-muted);
font-size: 11px;
}
}
}
}
/* 自定义滚动条 */
.language-list::-webkit-scrollbar {
width: 4px;
}
.language-list::-webkit-scrollbar-track {
background: transparent;
}
.language-list::-webkit-scrollbar-thumb {
background-color: var(--border-color);
border-radius: 2px;
&:hover {
background-color: var(--text-muted);
}
}
</style>

View File

@@ -0,0 +1,774 @@
<script setup lang="ts">
import {computed, nextTick, onMounted, onUnmounted, ref} from 'vue';
import {useDocumentStore} from '@/stores/documentStore';
import {useI18n} from 'vue-i18n';
import type {Document} from '@/../bindings/voidraft/internal/models/models';
import {useWindowStore} from "@/stores/windowStore";
const documentStore = useDocumentStore();
const windowStore = useWindowStore();
const {t} = useI18n();
// 组件状态
const showMenu = ref(false);
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 alreadyOpenDocId = ref<number | null>(null);
const errorMessageTimer = ref<number | null>(null);
// 过滤后的文档列表 + 创建选项
const filteredItems = computed(() => {
const docs = documentStore.documentList;
const query = inputValue.value.trim();
if (!query) {
return docs;
}
// 过滤匹配的文档
const filtered = docs.filter(doc =>
doc.title.toLowerCase().includes(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,
...filtered
];
}
return filtered;
});
// 当前文档显示名称
const currentDocName = computed(() => {
if (!documentStore.currentDocument) return t('toolbar.selectDocument');
const title = documentStore.currentDocument.title;
return title.length > 12 ? title.substring(0, 12) + '...' : title;
});
// 打开菜单
const openMenu = async () => {
showMenu.value = true;
await documentStore.updateDocuments();
nextTick(() => {
inputRef.value?.focus();
});
};
// 关闭菜单
const closeMenu = () => {
showMenu.value = false;
inputValue.value = '';
editingId.value = null;
editingTitle.value = '';
deleteConfirmId.value = null;
// 清除错误状态和定时器
clearErrorMessage();
};
// 清除错误提示和定时器
const clearErrorMessage = () => {
if (errorMessageTimer.value) {
clearTimeout(errorMessageTimer.value);
errorMessageTimer.value = null;
}
alreadyOpenDocId.value = null;
};
// 切换菜单
const toggleMenu = () => {
if (showMenu.value) {
closeMenu();
} else {
openMenu();
}
};
// 选择文档或创建文档
const selectItem = async (item: any) => {
if (item.isCreateOption) {
// 创建新文档
await createDoc(inputValue.value.trim());
} else {
// 选择现有文档
await selectDoc(item);
}
};
// 选择文档
const selectDoc = async (doc: Document) => {
try {
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
if (hasOpen) {
// 设置错误状态并启动定时器
alreadyOpenDocId.value = doc.id;
// 清除之前的定时器(如果存在)
if (errorMessageTimer.value) {
clearTimeout(errorMessageTimer.value);
}
// 设置新的定时器3秒后清除错误信息
errorMessageTimer.value = window.setTimeout(() => {
alreadyOpenDocId.value = null;
errorMessageTimer.value = null;
}, 3000);
return;
}
const success = await documentStore.openDocument(doc.id);
if (success) {
closeMenu();
}
} catch (error) {
console.error('Failed to switch documents:', error);
}
};
// 文档名称长度限制
const MAX_TITLE_LENGTH = 50;
// 验证文档名称
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 createDoc = async (title: string) => {
const trimmedTitle = title.trim();
const error = validateTitle(trimmedTitle);
if (error) {
return;
}
try {
const newDoc = await documentStore.createNewDocument(trimmedTitle);
if (newDoc) {
await selectDoc(newDoc);
}
} catch (error) {
console.error('Failed to create document:', error);
}
};
// 开始重命名
const startRename = (doc: Document, event: Event) => {
event.stopPropagation();
editingId.value = doc.id;
editingTitle.value = doc.title;
deleteConfirmId.value = null; // 清除删除确认状态
nextTick(() => {
editInputRef.value?.focus();
editInputRef.value?.select();
});
};
// 保存编辑
const saveEdit = async () => {
if (editingId.value && editingTitle.value.trim()) {
const trimmedTitle = editingTitle.value.trim();
const error = validateTitle(trimmedTitle);
if (error) {
return;
}
try {
await documentStore.updateDocumentMetadata(editingId.value, trimmedTitle);
await documentStore.updateDocuments();
} catch (error) {
return;
}
}
editingId.value = null;
editingTitle.value = '';
};
// 在新窗口打开文档
const openInNewWindow = async (doc: Document, event: Event) => {
event.stopPropagation();
try {
await documentStore.openDocumentInNewWindow(doc.id);
} catch (error) {
console.error('Failed to open document in new window:', error);
}
};
// 处理删除 - 简化确认机制
const handleDelete = async (doc: Document, event: Event) => {
event.stopPropagation();
if (deleteConfirmId.value === doc.id) {
// 确认删除前检查文档是否在其他窗口打开
try {
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
if (hasOpen) {
// 设置错误状态并启动定时器
alreadyOpenDocId.value = doc.id;
// 清除之前的定时器(如果存在)
if (errorMessageTimer.value) {
clearTimeout(errorMessageTimer.value);
}
// 设置新的定时器3秒后清除错误信息
errorMessageTimer.value = window.setTimeout(() => {
alreadyOpenDocId.value = null;
errorMessageTimer.value = null;
}, 3000);
// 取消删除确认状态
deleteConfirmId.value = null;
return;
}
await documentStore.deleteDocument(doc.id);
await documentStore.updateDocuments();
// 如果删除的是当前文档,切换到第一个文档
if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) {
const firstDoc = documentStore.documentList[0];
if (firstDoc) {
await selectDoc(firstDoc);
}
}
} catch (error) {
console.error('deleted failed:', error);
}
deleteConfirmId.value = null;
} else {
// 进入确认状态
deleteConfirmId.value = doc.id;
editingId.value = null; // 清除编辑状态
// 3秒后自动取消确认状态
setTimeout(() => {
if (deleteConfirmId.value === doc.id) {
deleteConfirmId.value = null;
}
}, 3000);
}
};
// 格式化时间
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 (error) {
return t('toolbar.timeError');
}
};
// 键盘事件
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
if (editingId.value) {
editingId.value = null;
editingTitle.value = '';
} else if (deleteConfirmId.value) {
deleteConfirmId.value = null;
} else {
closeMenu();
}
}
};
// 输入框键盘事件
const handleInputKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault();
const query = inputValue.value.trim();
if (query) {
// 如果有匹配的项目,选择第一个
if (filteredItems.value.length > 0) {
selectItem(filteredItems.value[0]);
}
}
} else if (event.key === 'Escape') {
event.preventDefault();
closeMenu();
}
event.stopPropagation();
};
// 编辑键盘事件
const handleEditKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault();
saveEdit();
} else if (event.key === 'Escape') {
event.preventDefault();
editingId.value = null;
editingTitle.value = '';
}
event.stopPropagation();
};
// 点击外部关闭
const handleClickOutside = (event: Event) => {
const target = event.target as HTMLElement;
if (!target.closest('.document-selector')) {
closeMenu();
}
};
// 生命周期
onMounted(() => {
document.addEventListener('click', handleClickOutside);
document.addEventListener('keydown', handleKeydown);
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
document.removeEventListener('keydown', handleKeydown);
// 清理定时器
if (errorMessageTimer.value) {
clearTimeout(errorMessageTimer.value);
}
});
</script>
<template>
<div class="document-selector">
<!-- 选择器按钮 -->
<button class="doc-btn" @click="toggleMenu">
<span class="doc-name">{{ currentDocName }}</span>
<span class="arrow" :class="{ open: showMenu }"></span>
</button>
<!-- 菜单 -->
<div v-if="showMenu" 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>
<!-- 项目列表 -->
<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="alreadyOpenDocId === item.id" class="doc-error">
{{ t('toolbar.alreadyOpenInNewWindow') }}
</div>
<div v-else class="doc-date">{{ formatTime(item.updatedAt) }}</div>
</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-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>
</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>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.document-selector {
position: relative;
.doc-btn {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 11px;
display: flex;
align-items: center;
gap: 3px;
padding: 2px 4px;
border-radius: 3px;
&:hover {
background-color: var(--border-color);
opacity: 0.8;
}
.doc-name {
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arrow {
font-size: 8px;
margin-left: 2px;
transition: transform 0.2s ease;
&.open {
transform: rotate(180deg);
}
}
}
.doc-menu {
position: absolute;
bottom: 100%;
right: 0;
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 3px;
margin-bottom: 4px;
width: 260px;
max-height: 320px;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
overflow: hidden;
.input-box {
position: relative;
padding: 8px;
border-bottom: 1px solid var(--border-color);
.main-input {
width: 100%;
box-sizing: border-box;
background-color: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 2px;
padding: 5px 8px 5px 26px;
font-size: 11px;
color: var(--text-primary);
outline: none;
&:focus {
border-color: var(--text-muted);
}
&::placeholder {
color: var(--text-muted);
}
}
.input-icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
pointer-events: none;
}
}
.item-list {
max-height: 240px;
overflow-y: auto;
.list-item {
cursor: pointer;
border-bottom: 1px solid var(--border-color);
&:hover {
background-color: var(--bg-hover);
}
&.active {
background-color: var(--selection-bg);
.doc-item-content .doc-info {
.doc-title {
color: var(--selection-text);
}
.doc-date, .doc-error {
color: var(--selection-text);
opacity: 0.7;
}
}
}
&.create-item {
.create-option {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 8px;
font-size: 11px;
font-weight: normal;
svg {
flex-shrink: 0;
color: var(--text-muted);
}
}
}
.doc-item-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 8px;
.doc-info {
flex: 1;
min-width: 0;
.doc-title {
font-size: 12px;
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: normal;
}
.doc-date {
font-size: 10px;
color: var(--text-muted);
opacity: 0.6;
}
.doc-error {
font-size: 10px;
color: var(--text-danger);
font-weight: 500;
animation: fadeInOut 3s forwards;
}
}
.doc-edit {
flex: 1;
.edit-input {
width: 100%;
box-sizing: border-box;
background-color: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 2px;
padding: 4px 6px;
font-size: 11px;
color: var(--text-primary);
outline: none;
&:focus {
border-color: var(--text-muted);
}
}
}
.doc-actions {
display: flex;
gap: 6px;
opacity: 0;
transition: opacity 0.2s ease;
.action-btn {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 4px;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
min-width: 20px;
min-height: 20px;
svg {
width: 12px;
height: 12px;
}
&:hover {
background-color: var(--border-color);
color: var(--text-primary);
}
&.delete-btn:hover {
color: var(--text-danger);
}
&.delete-confirm {
background-color: var(--text-danger);
color: white;
.confirm-text {
font-size: 10px;
padding: 0 4px;
font-weight: normal;
}
&:hover {
background-color: var(--text-danger);
color: white !important; // 确保确认状态下文字始终为白色
opacity: 0.8;
}
}
}
}
}
&:hover .doc-actions {
opacity: 1;
}
}
.empty, .loading {
padding: 12px 8px;
text-align: center;
font-size: 11px;
color: var(--text-muted);
}
}
}
// 自定义滚动条
.item-list {
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: var(--border-color);
border-radius: 2px;
&:hover {
background-color: var(--text-muted);
}
}
}
}
@keyframes fadeInOut {
0% {
opacity: 1;
}
70% {
opacity: 1;
}
100% {
opacity: 0;
}
}
</style>

View File

@@ -1,53 +1,38 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { ref, onMounted, watch } from 'vue';
import { useConfigStore } from '@/stores/configStore';
import { useEditorStore } from '@/stores/editorStore';
import { useErrorHandler } from '@/utils/errorHandler';
import {useI18n} from 'vue-i18n';
import {onMounted, onUnmounted, ref, watch, computed} from 'vue';
import {useConfigStore} from '@/stores/configStore';
import {useEditorStore} from '@/stores/editorStore';
import {useUpdateStore} from '@/stores/updateStore';
import {useWindowStore} from '@/stores/windowStore';
import * as runtime from '@wailsio/runtime';
import { useRouter } from 'vue-router';
import {useRouter} from 'vue-router';
import BlockLanguageSelector from './BlockLanguageSelector.vue';
import DocumentSelector from './DocumentSelector.vue';
import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state';
import {getLanguage} from '@/views/editor/extensions/codeblock/lang-parser/languages';
import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode';
const editorStore = useEditorStore();
const configStore = useConfigStore();
const { safeCall } = useErrorHandler();
const { t, locale } = useI18n();
const updateStore = useUpdateStore();
const windowStore = useWindowStore();
const {t} = useI18n();
const router = useRouter();
// 语言下拉菜单
const showLanguageMenu = ref(false);
// 当前块是否支持格式化的响应式状态
const canFormatCurrentBlock = ref(false);
const supportedLanguages = [
{ code: 'zh-CN', name: '简体中文' },
{ code: 'en-US', name: 'English' }
];
// 窗口置顶状态管理(仅当前窗口,不同步到配置文件)
const isCurrentWindowOnTop = ref(false);
const toggleLanguageMenu = () => {
showLanguageMenu.value = !showLanguageMenu.value;
};
const closeLanguageMenu = () => {
showLanguageMenu.value = false;
};
const changeLanguage = async (langCode: string) => {
await safeCall(() => configStore.setLanguage(langCode as any), 'language.changeFailed');
closeLanguageMenu();
};
// 设置窗口置顶
const setWindowAlwaysOnTop = async (isTop: boolean) => {
await safeCall(async () => {
await runtime.Window.SetAlwaysOnTop(isTop);
}, 'window.setTopFailed');
await runtime.Window.SetAlwaysOnTop(isTop);
};
// 切换窗口置顶
const toggleAlwaysOnTop = async () => {
await safeCall(async () => {
await configStore.toggleAlwaysOnTop();
// 使用Window.SetAlwaysOnTop方法设置窗口置顶状态
await runtime.Window.SetAlwaysOnTop(configStore.config.general.alwaysOnTop);
}, 'config.alwaysOnTopFailed');
isCurrentWindowOnTop.value = !isCurrentWindowOnTop.value;
await runtime.Window.SetAlwaysOnTop(isCurrentWindowOnTop.value);
};
// 跳转到设置页面
@@ -55,93 +40,239 @@ const goToSettings = () => {
router.push('/settings');
};
// 执行格式化
const formatCurrentBlock = () => {
if (!canFormatCurrentBlock.value || !editorStore.editorView) return;
formatBlockContent(editorStore.editorView);
};
// 格式化按钮状态更新
const updateFormatButtonState = () => {
// 安全检查
const view = editorStore.editorView;
if (!view) {
canFormatCurrentBlock.value = false;
return;
}
try {
// 获取活动块和语言信息
const state = view.state;
const activeBlock = getActiveNoteBlock(state as any);
// 检查块和语言格式化支持
canFormatCurrentBlock.value = !!(
activeBlock &&
getLanguage(activeBlock.language.name as any)?.prettier
);
} catch (error) {
console.warn('Error checking format capability:', error);
canFormatCurrentBlock.value = false;
}
};
// 创建带300ms防抖的更新函数
const debouncedUpdateFormatButton = (() => {
let timeout: number | null = null;
return () => {
if (timeout) clearTimeout(timeout);
timeout = window.setTimeout(() => {
updateFormatButtonState();
timeout = null;
}, 300);
};
})();
// 编辑器事件管理
const setupEditorListeners = (view: any) => {
if (!view?.dom) return [];
const events = [
{ type: 'click', handler: updateFormatButtonState },
{ type: 'keyup', handler: debouncedUpdateFormatButton },
{ type: 'focus', handler: updateFormatButtonState }
];
// 注册所有事件
events.forEach(event => view.dom.addEventListener(event.type, event.handler));
// 返回清理函数数组
return events.map(event =>
() => view.dom.removeEventListener(event.type, event.handler)
);
};
// 监听编辑器视图变化
let cleanupListeners: (() => void)[] = [];
watch(
() => editorStore.editorView,
(newView) => {
// 清理旧监听器
cleanupListeners.forEach(cleanup => cleanup());
cleanupListeners = [];
if (newView) {
// 初始更新状态
updateFormatButtonState();
// 设置新监听器
cleanupListeners = setupEditorListeners(newView);
} else {
canFormatCurrentBlock.value = false;
}
},
{ immediate: true }
);
// 组件生命周期
const isLoaded = ref(false);
onMounted(() => {
isLoaded.value = true;
// 首次更新格式化状态
updateFormatButtonState();
});
// 监听置顶设置变化
watch(
() => configStore.config.general.alwaysOnTop,
async (newValue) => {
if (!isLoaded.value) return;
await runtime.Window.SetAlwaysOnTop(newValue);
}
);
onUnmounted(() => {
// 清理所有事件监听器
cleanupListeners.forEach(cleanup => cleanup());
cleanupListeners = [];
});
// 组件加载完成后应用置顶设置
watch(isLoaded, async (newLoaded) => {
if (newLoaded && configStore.config.general.alwaysOnTop) {
await setWindowAlwaysOnTop(true);
// 组件加载后初始化置顶状态
watch(isLoaded, async (loaded) => {
if (loaded) {
// 初始化时从配置文件读取置顶状态
isCurrentWindowOnTop.value = configStore.config.general.alwaysOnTop;
await setWindowAlwaysOnTop(isCurrentWindowOnTop.value);
}
});
const handleUpdateButtonClick = async () => {
if (updateStore.hasUpdate && !updateStore.isUpdating && !updateStore.updateSuccess) {
// 开始下载更新
await updateStore.applyUpdate();
} else if (updateStore.updateSuccess) {
// 更新成功后,点击重启
await updateStore.restartApplication();
}
};
// 更新按钮标题计算属性
const updateButtonTitle = computed(() => {
if (updateStore.isChecking) return t('settings.checking');
if (updateStore.isUpdating) return t('settings.updating');
if (updateStore.updateSuccess) return t('settings.updateSuccessRestartRequired');
if (updateStore.hasUpdate) return `${t('settings.newVersionAvailable')}: ${updateStore.updateResult?.latestVersion || ''}`;
return '';
});
</script>
<template>
<div class="toolbar-container">
<div class="statistics">
<span class="stat-item" :title="t('toolbar.editor.lines')">{{ t('toolbar.editor.lines') }}: <span class="stat-value">{{
<span class="stat-item" :title="t('toolbar.editor.lines')">{{ t('toolbar.editor.lines') }}: <span
class="stat-value">{{
editorStore.documentStats.lines
}}</span></span>
<span class="stat-item" :title="t('toolbar.editor.characters')">{{ t('toolbar.editor.characters') }}: <span class="stat-value">{{
<span class="stat-item" :title="t('toolbar.editor.characters')">{{ t('toolbar.editor.characters') }}: <span
class="stat-value">{{
editorStore.documentStats.characters
}}</span></span>
<span class="stat-item" :title="t('toolbar.editor.selected')" v-if="editorStore.documentStats.selectedCharacters > 0">
{{ t('toolbar.editor.selected') }}: <span class="stat-value">{{ editorStore.documentStats.selectedCharacters }}</span>
<span class="stat-item" :title="t('toolbar.editor.selected')"
v-if="editorStore.documentStats.selectedCharacters > 0">
{{ t('toolbar.editor.selected') }}: <span class="stat-value">{{
editorStore.documentStats.selectedCharacters
}}</span>
</span>
</div>
<div class="actions">
<span class="font-size" :title="t('toolbar.fontSizeTooltip')" @click="() => configStore.resetFontSize()">
{{ configStore.config.editing.fontSize }}px
</span>
<span class="tab-settings">
<label :title="t('toolbar.tabLabel')" class="tab-toggle">
<input type="checkbox" :checked="configStore.config.editing.enableTabIndent" @change="() => configStore.toggleTabIndent()"/>
<span>{{ t('toolbar.tabLabel') }}</span>
</label>
<span class="tab-type" :title="t('toolbar.tabType.' + (configStore.config.editing.tabType === 'spaces' ? 'spaces' : 'tab'))" @click="() => configStore.toggleTabType()">
{{ t('toolbar.tabType.' + (configStore.config.editing.tabType === 'spaces' ? 'spaces' : 'tab')) }}
</span>
<span class="tab-size" title="Tab大小" v-if="configStore.config.editing.tabType === 'spaces'">
<button class="tab-btn" @click="() => configStore.decreaseTabSize()" :disabled="configStore.config.editing.tabSize <= configStore.tabSize.min">-</button>
<span>{{ configStore.config.editing.tabSize }}</span>
<button class="tab-btn" @click="() => configStore.increaseTabSize()" :disabled="configStore.config.editing.tabSize >= configStore.tabSize.max">+</button>
</span>
</span>
<!-- 窗口置顶图标按钮 -->
<div
class="pin-button"
:class="{ 'active': configStore.config.general.alwaysOnTop }"
:title="t('toolbar.alwaysOnTop')"
@click="toggleAlwaysOnTop"
<!-- 文档选择器 -->
<DocumentSelector v-if="windowStore.isMainWindow"/>
<!-- 块语言选择器 -->
<BlockLanguageSelector/>
<!-- 格式化按钮 - 支持点击操作 -->
<div
v-if="canFormatCurrentBlock"
class="format-button"
:title="t('toolbar.formatHint')"
@click="formatCurrentBlock"
>
<svg class="pin-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="M557.44 104.96l361.6 361.6-60.16 64-26.88-33.92-181.12 181.12L617.6 832l-60.16 60.16-181.12-184.32-211.2 211.2-60.16-60.16 211.2-211.2-181.12-181.12 60.16-60.16 151.04-30.08 181.12-181.12-30.72-30.08 64-60.16zM587.52 256L387.84 455.04l-120.32 23.68 277.76 277.76 23.68-120.32L768 436.48z" />
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path
d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/>
<path d="M5 3v4"/>
<path d="M19 17v4"/>
<path d="M3 5h4"/>
<path d="M17 19h4"/>
</svg>
</div>
<!-- 语言切换按钮 -->
<div class="selector-dropdown">
<button class="selector-btn" @click="toggleLanguageMenu">
{{ locale }}
<span class="arrow"></span>
</button>
<div class="selector-menu" v-if="showLanguageMenu">
<div
v-for="lang in supportedLanguages"
:key="lang.code"
class="selector-option"
:class="{ active: locale === lang.code }"
@click="changeLanguage(lang.code)"
>
{{ t(`languages.${lang.code}`) }}
</div>
</div>
<!-- 更新按钮 - 根据状态显示不同图标 -->
<div
v-if="updateStore.hasUpdate || updateStore.isChecking || updateStore.isUpdating || updateStore.updateSuccess"
class="update-button"
:class="{
'checking': updateStore.isChecking,
'updating': updateStore.isUpdating,
'success': updateStore.updateSuccess,
'available': updateStore.hasUpdate && !updateStore.isUpdating && !updateStore.updateSuccess
}"
:title="updateButtonTitle"
@click="handleUpdateButtonClick"
>
<!-- 检查更新中 -->
<svg v-if="updateStore.isChecking" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotating">
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>
<!-- 下载更新中 -->
<svg v-else-if="updateStore.isUpdating" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="rotating">
<path d="M21 12a9 9 0 1 1-6.219-8.56"></path>
<path d="M12 2a10 10 0 1 0 10 10"></path>
</svg>
<!-- 更新成功等待重启 -->
<svg v-else-if="updateStore.updateSuccess" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pulsing">
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path>
<line x1="12" y1="2" x2="12" y2="12"></line>
</svg>
<!-- 有更新可用 -->
<svg v-else xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="7.5,10.5 12,15 16.5,10.5"/>
<polyline points="12,15 12,3"/>
</svg>
</div>
<button class="settings-btn" :title="t('toolbar.settings')" @click="goToSettings">
<!-- 窗口置顶图标按钮 -->
<div
class="pin-button"
:class="{ 'active': isCurrentWindowOnTop }"
:title="t('toolbar.alwaysOnTop')"
@click="toggleAlwaysOnTop"
>
<svg class="pin-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path
d="M557.44 104.96l361.6 361.6-60.16 64-26.88-33.92-181.12 181.12L617.6 832l-60.16 60.16-181.12-184.32-211.2 211.2-60.16-60.16 211.2-211.2-181.12-181.12 60.16-60.16 151.04-30.08 181.12-181.12-30.72-30.08 64-60.16zM587.52 256L387.84 455.04l-120.32 23.68 277.76 277.76 23.68-120.32L768 436.48z"/>
</svg>
</div>
<button v-if="windowStore.isMainWindow" class="settings-btn" :title="t('toolbar.settings')" @click="goToSettings">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"></circle>
@@ -191,60 +322,100 @@ watch(isLoaded, async (newLoaded) => {
cursor: help;
}
.tab-settings {
/* 更新按钮样式 */
.update-button {
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
color: var(--text-muted);
font-size: 11px;
justify-content: center;
width: 20px;
height: 20px;
padding: 2px;
border-radius: 3px;
transition: all 0.2s ease;
.tab-toggle {
display: flex;
align-items: center;
gap: 3px;
cursor: pointer;
input {
cursor: pointer;
/* 有更新可用状态 */
&.available {
background-color: rgba(76, 175, 80, 0.1);
animation: pulse 2s infinite;
svg {
stroke: #4caf50;
}
&:hover {
background-color: rgba(76, 175, 80, 0.2);
transform: scale(1.05);
}
}
.tab-type {
cursor: pointer;
padding: 0 3px;
border-radius: 3px;
background-color: var(--border-color);
opacity: 0.6;
/* 检查更新中状态 */
&.checking {
background-color: rgba(255, 193, 7, 0.1);
&:hover {
opacity: 1;
background-color: var(--border-color);
svg {
stroke: #ffc107;
}
}
/* 更新下载中状态 */
&.updating {
background-color: rgba(33, 150, 243, 0.1);
svg {
stroke: #2196f3;
}
}
/* 更新成功状态 */
&.success {
background-color: rgba(156, 39, 176, 0.1);
svg {
stroke: #9c27b0;
}
}
.tab-size {
display: flex;
align-items: center;
gap: 2px;
/* 旋转动画 */
.rotating {
animation: rotate 1.5s linear infinite;
}
/* 脉冲动画 */
.pulsing {
animation: pulse-strong 1.2s ease-in-out infinite;
}
.tab-btn {
background: none;
border: none;
color: var(--text-primary);
cursor: pointer;
padding: 0 3px;
font-size: 12px;
line-height: 1;
&:disabled {
color: var(--text-muted);
opacity: 0.5;
cursor: not-allowed;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
@keyframes pulse-strong {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}
/* 窗口置顶图标按钮样式 */
.pin-button {
cursor: pointer;
@@ -256,20 +427,20 @@ watch(isLoaded, async (newLoaded) => {
padding: 2px;
border-radius: 3px;
transition: all 0.2s ease;
&:hover {
background-color: var(--border-color);
opacity: 0.8;
}
&.active {
background-color: rgba(181, 206, 168, 0.2);
.pin-icon {
fill: #b5cea8;
}
}
.pin-icon {
width: 14px;
height: 14px;
@@ -277,63 +448,33 @@ watch(isLoaded, async (newLoaded) => {
transition: fill 0.2s ease;
}
}
/* 通用下拉选择器样式 */
.selector-dropdown {
position: relative;
.selector-btn {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 11px;
display: flex;
align-items: center;
gap: 2px;
padding: 2px 4px;
border-radius: 3px;
&:hover {
background-color: var(--border-color);
opacity: 0.8;
}
.arrow {
font-size: 8px;
margin-left: 2px;
}
.format-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;
}
.selector-menu {
position: absolute;
bottom: 100%;
right: 0;
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 3px;
margin-bottom: 4px;
min-width: 120px;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
.selector-option {
padding: 4px 8px;
cursor: pointer;
font-size: 11px;
white-space: nowrap;
&:hover {
background-color: var(--border-color);
opacity: 0.8;
}
&.active {
color: #b5cea8;
}
}
svg {
width: 14px;
height: 14px;
stroke: var(--text-muted);
transition: stroke 0.2s ease;
}
&:hover svg {
stroke: var(--text-secondary);
}
}

View File

@@ -1,189 +0,0 @@
import { ref, computed, shallowRef } from 'vue';
import { Extension, Compartment } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import type { ThemeType } from '@/types';
// 主题加载器类型
type ThemeLoader = () => Promise<Extension>;
// 默认主题常量
const DEFAULT_THEME = 'default-dark' as ThemeType;
// 主题加载映射
const themeLoaderMap = new Map<string, ThemeLoader>();
// 初始化主题加载器
const initThemeLoaders = () => {
themeLoaderMap.set('default-dark', () => import('@/views/editor/theme/default-dark').then(m => m.defaultDark));
themeLoaderMap.set('dracula', () => import('@/views/editor/theme/dracula').then(m => m.dracula));
themeLoaderMap.set('aura', () => import('@/views/editor/theme/aura').then(m => m.aura));
themeLoaderMap.set('github-dark', () => import('@/views/editor/theme/github-dark').then(m => m.githubDark));
themeLoaderMap.set('github-light', () => import('@/views/editor/theme/github-light').then(m => m.githubLight));
themeLoaderMap.set('material-dark', () => import('@/views/editor/theme/material-dark').then(m => m.materialDark));
themeLoaderMap.set('material-light', () => import('@/views/editor/theme/material-light').then(m => m.materialLight));
themeLoaderMap.set('solarized-dark', () => import('@/views/editor/theme/solarized-dark').then(m => m.solarizedDark));
themeLoaderMap.set('solarized-light', () => import('@/views/editor/theme/solarized-light').then(m => m.solarizedLight));
themeLoaderMap.set('tokyo-night', () => import('@/views/editor/theme/tokyo-night').then(m => m.tokyoNight));
themeLoaderMap.set('tokyo-night-storm', () => import('@/views/editor/theme/tokyo-night-storm').then(m => m.tokyoNightStorm));
themeLoaderMap.set('tokyo-night-day', () => import('@/views/editor/theme/tokyo-night-day').then(m => m.tokyoNightDay));
};
// 延迟初始化
initThemeLoaders();
// 全局状态
const currentTheme = ref<ThemeType>(DEFAULT_THEME);
const themeCompartment = new Compartment();
const themeCache = new Map<ThemeType, Extension>();
const failedThemes = new Set<ThemeType>(); // 记录加载失败的主题
/**
* 编辑器主题管理
*/
export function useEditorTheme() {
/**
* 安全加载主题扩展
*/
const loadTheme = async (targetTheme: ThemeType): Promise<Extension> => {
// 1. 从缓存快速返回
const cached = themeCache.get(targetTheme);
if (cached) return cached;
// 2. 检查是否已知失败的主题,避免重复尝试
if (failedThemes.has(targetTheme) && targetTheme !== DEFAULT_THEME) {
console.info(`Theme ${targetTheme} is known to fail, attempting default theme directly`);
return attemptLoadTheme(DEFAULT_THEME).catch(() => [] as Extension);
}
// 3. 使用 try-catch 链和 nullish coalescing替代递归
const result = await attemptLoadTheme(targetTheme)
.catch(async (error) => {
// 仅当目标主题不是默认主题时,才尝试默认主题
if (targetTheme !== DEFAULT_THEME) {
console.warn(`Theme ${targetTheme} failed, fallback to ${DEFAULT_THEME}:`, error);
return attemptLoadTheme(DEFAULT_THEME).catch((fallbackError) => {
console.error(`Fallback theme ${DEFAULT_THEME} also failed:`, fallbackError);
return [] as Extension; // 最终回退到空扩展
});
}
// 如果默认主题也失败了,返回空扩展
console.error(`Default theme ${DEFAULT_THEME} failed:`, error);
return [] as Extension;
});
return result;
};
/**
* 单纯的主题加载尝试 - 不处理回退逻辑
*/
const attemptLoadTheme = async (themeType: ThemeType): Promise<Extension> => {
// 获取加载器,使用 optional chaining 和 nullish coalescing
const loader = themeLoaderMap.get(themeType);
if (!loader) {
const error = new Error(`Theme loader not found: ${themeType}`);
failedThemes.add(themeType);
throw error;
}
try {
const extension = await loader();
// 缓存成功加载的主题
themeCache.set(themeType, extension);
// 从失败列表中移除(如果存在)
failedThemes.delete(themeType);
return extension;
} catch (error) {
// 记录失败的主题
failedThemes.add(themeType);
console.error(`Failed to load theme: ${themeType}`, error);
throw error;
}
};
/**
* 创建可配置的主题扩展
*/
const createThemeExtension = async (themeType: ThemeType): Promise<Extension> => {
const extension = await loadTheme(themeType);
currentTheme.value = themeType;
return themeCompartment.of(extension);
};
/**
* 更新编辑器主题 - 使用防抖和错误处理
*/
const updateTheme = async (view: EditorView, themeType: ThemeType): Promise<void> => {
// 使用可选链操作符检查 view
if (!view?.dispatch || themeType === currentTheme.value) {
return;
}
const extension = await loadTheme(themeType);
// 使用 try-catch 包装 dispatch避免编辑器状态异常
try {
view.dispatch({
effects: themeCompartment.reconfigure(extension)
});
currentTheme.value = themeType;
} catch (error) {
console.error('Failed to dispatch theme update:', error);
throw error; // 重新抛出,让调用者处理
}
};
/**
* 批量预加载主题 - 使用 Promise.allSettled 确保部分失败不影响其他
*/
const preloadThemes = async (themes: ThemeType[]): Promise<PromiseSettledResult<Extension>[]> => {
const uniqueThemes = [...new Set(themes)]; // 去重
return Promise.allSettled(
uniqueThemes.map(theme => loadTheme(theme))
);
};
/**
* 重置主题系统状态
*/
const resetThemeSystem = (): void => {
themeCache.clear();
failedThemes.clear();
currentTheme.value = DEFAULT_THEME;
};
/**
* 获取主题系统状态信息
*/
const getThemeSystemInfo = () => ({
currentTheme: currentTheme.value,
cachedThemes: Array.from(themeCache.keys()),
failedThemes: Array.from(failedThemes),
availableThemes: Array.from(themeLoaderMap.keys()),
});
return {
// 状态
currentTheme: computed(() => currentTheme.value),
// 核心方法
createThemeExtension,
updateTheme,
loadTheme,
// 批量操作
preloadThemes,
// 工具方法
resetThemeSystem,
getThemeSystemInfo,
// 缓存管理
clearCache: () => themeCache.clear(),
clearFailedThemes: () => failedThemes.clear(),
};
}

View File

@@ -1,35 +0,0 @@
import { ref, watch, onMounted, onUnmounted } from 'vue';
import { useConfigStore } from '@/stores/configStore';
export function useSystemTheme() {
const configStore = useConfigStore();
const currentSystemTheme = ref<'dark' | 'light'>('dark');
// 设置主题 - 简化版本
const setTheme = (theme: string) => {
const root = document.documentElement;
root.setAttribute('data-theme', theme);
};
// 监听配置变化
watch(
() => configStore.config.appearance.systemTheme,
(newTheme) => {
// 直接根据配置设置主题,不需要检测系统主题
const root = document.documentElement;
root.setAttribute('data-theme', newTheme);
},
{ immediate: true }
);
onMounted(() => {
const root = document.documentElement;
const systemTheme = configStore.config.appearance.systemTheme;
root.setAttribute('data-theme', systemTheme);
});
return {
currentSystemTheme,
setTheme
};
}

View File

@@ -1,59 +0,0 @@
import { useConfigStore } from '@/stores/configStore';
import { useEditorTheme } from './useEditorTheme';
import type { ThemeType } from '@/types';
/**
* 主题管理 - 用于设置页面
*/
export function useTheme() {
const configStore = useConfigStore();
const { preloadThemes, getThemeSystemInfo } = useEditorTheme();
/**
* 设置主题 - 同时更新配置和预览
*/
const setTheme = async (themeType: ThemeType): Promise<void> => {
try {
// 更新配置存储(这会自动触发编辑器主题更新)
await configStore.setTheme(themeType);
console.info(`Theme switched to: ${themeType}`);
} catch (error) {
console.error('Failed to set theme:', error);
throw error;
}
};
/**
* 预加载常用主题
*/
const preloadCommonThemes = async (): Promise<void> => {
const commonThemes: ThemeType[] = [
'default-dark' as ThemeType,
'dracula' as ThemeType,
'github-dark' as ThemeType,
'material-dark' as ThemeType
];
try {
await preloadThemes(commonThemes);
console.info('Common themes preloaded successfully');
} catch (error) {
console.warn('Some themes failed to preload:', error);
}
};
/**
* 获取主题状态
*/
const getThemeStatus = () => ({
current: configStore.config.appearance.theme,
...getThemeSystemInfo()
});
return {
setTheme,
preloadCommonThemes,
getThemeStatus,
};
}

View File

@@ -1,423 +0,0 @@
import { ref, onUnmounted, reactive, computed, watch, nextTick } from 'vue';
// 基础WebSocket消息接口
interface WebSocketMessage<T = any> {
type: string;
data: T;
}
// 迁移进度接口(与后端保持一致)
interface MigrationProgress {
status: 'migrating' | 'completed' | 'failed';
progress: number; // 0-100
error?: string;
}
// 连接状态枚举
enum ConnectionState {
DISCONNECTED = 'disconnected',
CONNECTING = 'connecting',
CONNECTED = 'connected',
RECONNECTING = 'reconnecting',
ERROR = 'error'
}
// WebSocket配置选项
interface WebSocketOptions {
url?: string;
reconnectInterval?: number;
maxReconnectAttempts?: number;
debug?: boolean;
autoConnect?: boolean;
protocols?: string | string[];
heartbeat?: {
enabled: boolean;
interval: number;
message: string;
};
}
// 消息处理器类型
type MessageHandler<T = any> = (data: T) => void;
// 事件处理器映射
interface EventHandlers {
[messageType: string]: MessageHandler[];
}
// 连接事件类型
type ConnectionEventType = 'connect' | 'disconnect' | 'error' | 'reconnect';
type ConnectionEventHandler = (event?: Event | CloseEvent | ErrorEvent) => void;
export function useWebSocket(options: WebSocketOptions = {}) {
const {
url = 'ws://localhost:8899/ws/migration',
reconnectInterval = 3000,
maxReconnectAttempts = 10,
debug = false,
autoConnect = true,
protocols,
heartbeat = { enabled: false, interval: 30000, message: 'ping' }
} = options;
// === 状态管理 ===
const connectionState = ref<ConnectionState>(ConnectionState.DISCONNECTED);
const connectionError = ref<string | null>(null);
const reconnectAttempts = ref(0);
const lastMessage = ref<WebSocketMessage | null>(null);
const messageHistory = ref<WebSocketMessage[]>([]);
// 迁移进度状态(保持向后兼容)
const migrationProgress = reactive<MigrationProgress>({
status: 'completed',
progress: 0
});
// === 计算属性 ===
const isConnected = computed(() => connectionState.value === ConnectionState.CONNECTED);
const isConnecting = computed(() =>
connectionState.value === ConnectionState.CONNECTING ||
connectionState.value === ConnectionState.RECONNECTING
);
const canReconnect = computed(() => reconnectAttempts.value < maxReconnectAttempts);
// === 内部状态 ===
let ws: WebSocket | null = null;
let reconnectTimer: number | null = null;
let heartbeatTimer: number | null = null;
let isManualDisconnect = false;
// 事件处理器
const eventHandlers: EventHandlers = {};
const connectionEventHandlers: Map<ConnectionEventType, ConnectionEventHandler[]> = new Map();
// === 工具函数 ===
const log = (level: 'info' | 'warn' | 'error', message: string, ...args: any[]) => {
if (debug) {
console[level](`[WebSocket] ${message}`, ...args);
}
};
const clearTimers = () => {
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
heartbeatTimer = null;
}
};
const updateConnectionState = (newState: ConnectionState, error?: string) => {
connectionState.value = newState;
connectionError.value = error || null;
log('info', `Connection state changed to: ${newState}`, error);
};
// === 事件系统 ===
const on = <T = any>(messageType: string, handler: MessageHandler<T>) => {
if (!eventHandlers[messageType]) {
eventHandlers[messageType] = [];
}
eventHandlers[messageType].push(handler as MessageHandler);
// 返回取消订阅函数
return () => off(messageType, handler);
};
const off = <T = any>(messageType: string, handler: MessageHandler<T>) => {
if (eventHandlers[messageType]) {
const index = eventHandlers[messageType].indexOf(handler as MessageHandler);
if (index > -1) {
eventHandlers[messageType].splice(index, 1);
}
}
};
const onConnection = (eventType: ConnectionEventType, handler: ConnectionEventHandler) => {
if (!connectionEventHandlers.has(eventType)) {
connectionEventHandlers.set(eventType, []);
}
connectionEventHandlers.get(eventType)!.push(handler);
return () => offConnection(eventType, handler);
};
const offConnection = (eventType: ConnectionEventType, handler: ConnectionEventHandler) => {
const handlers = connectionEventHandlers.get(eventType);
if (handlers) {
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
};
const emit = (eventType: ConnectionEventType, event?: Event | CloseEvent | ErrorEvent) => {
const handlers = connectionEventHandlers.get(eventType);
if (handlers) {
handlers.forEach(handler => {
try {
handler(event);
} catch (error) {
log('error', `Error in ${eventType} event handler:`, error);
}
});
}
};
// === 消息处理 ===
const handleMessage = (event: MessageEvent) => {
try {
const message: WebSocketMessage = JSON.parse(event.data);
log('info', 'Received message:', message);
// 更新消息历史
lastMessage.value = message;
messageHistory.value.push(message);
// 限制历史记录长度
if (messageHistory.value.length > 100) {
messageHistory.value.shift();
}
// 特殊处理迁移进度消息(保持向后兼容)
if (message.type === 'migration_progress') {
Object.assign(migrationProgress, message.data);
}
// 触发注册的处理器
const handlers = eventHandlers[message.type];
if (handlers) {
handlers.forEach(handler => {
try {
handler(message.data);
} catch (error) {
log('error', `Error in message handler for ${message.type}:`, error);
}
});
}
} catch (error) {
log('error', 'Failed to parse message:', error, event.data);
}
};
// === 心跳机制 ===
const startHeartbeat = () => {
if (!heartbeat.enabled || heartbeatTimer) return;
heartbeatTimer = window.setInterval(() => {
if (isConnected.value) {
send(heartbeat.message);
}
}, heartbeat.interval);
};
const stopHeartbeat = () => {
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
heartbeatTimer = null;
}
};
// === 连接管理 ===
const connect = async (): Promise<void> => {
if (isConnecting.value || isConnected.value) {
log('warn', 'Already connecting or connected');
return;
}
updateConnectionState(ConnectionState.CONNECTING);
isManualDisconnect = false;
try {
log('info', 'Connecting to:', url);
ws = new WebSocket(url, protocols);
// 连接超时处理
const connectTimeout = setTimeout(() => {
if (ws && ws.readyState === WebSocket.CONNECTING) {
ws.close();
updateConnectionState(ConnectionState.ERROR, 'Connection timeout');
}
}, 10000);
ws.onopen = () => {
clearTimeout(connectTimeout);
log('info', 'Connected successfully');
updateConnectionState(ConnectionState.CONNECTED);
reconnectAttempts.value = 0;
clearTimers();
startHeartbeat();
emit('connect');
};
ws.onmessage = handleMessage;
ws.onclose = (event) => {
clearTimeout(connectTimeout);
stopHeartbeat();
log('info', 'Connection closed:', event.code, event.reason);
const wasConnected = connectionState.value === ConnectionState.CONNECTED;
updateConnectionState(ConnectionState.DISCONNECTED);
ws = null;
emit('disconnect', event);
// 自动重连逻辑
if (!isManualDisconnect && event.code !== 1000 && canReconnect.value) {
scheduleReconnect();
} else if (reconnectAttempts.value >= maxReconnectAttempts) {
updateConnectionState(ConnectionState.ERROR, 'Max reconnection attempts reached');
}
};
ws.onerror = (event) => {
clearTimeout(connectTimeout);
log('error', 'Connection error:', event);
updateConnectionState(ConnectionState.ERROR, 'WebSocket connection error');
emit('error', event);
};
} catch (error) {
log('error', 'Failed to create WebSocket:', error);
updateConnectionState(ConnectionState.ERROR, 'Failed to create WebSocket connection');
}
};
const disconnect = (code: number = 1000, reason: string = 'Manual disconnect') => {
isManualDisconnect = true;
clearTimers();
stopHeartbeat();
if (ws) {
log('info', 'Disconnecting manually');
ws.close(code, reason);
}
updateConnectionState(ConnectionState.DISCONNECTED);
};
const scheduleReconnect = () => {
if (!canReconnect.value || isManualDisconnect) return;
clearTimers();
reconnectAttempts.value++;
updateConnectionState(ConnectionState.RECONNECTING,
`Reconnecting... (${reconnectAttempts.value}/${maxReconnectAttempts})`);
log('info', `Scheduling reconnect attempt ${reconnectAttempts.value}/${maxReconnectAttempts} in ${reconnectInterval}ms`);
reconnectTimer = window.setTimeout(() => {
connect();
}, reconnectInterval);
emit('reconnect');
};
const reconnect = () => {
disconnect();
reconnectAttempts.value = 0;
nextTick(() => {
connect();
});
};
// === 消息发送 ===
const send = (message: any): boolean => {
if (!isConnected.value || !ws) {
log('warn', 'Cannot send message: not connected');
return false;
}
try {
const data = typeof message === 'string' ? message : JSON.stringify(message);
ws.send(data);
log('info', 'Sent message:', data);
return true;
} catch (error) {
log('error', 'Failed to send message:', error);
return false;
}
};
const sendMessage = <T = any>(type: string, data?: T): boolean => {
return send({ type, data });
};
// === 状态查询 ===
const getConnectionInfo = () => ({
state: connectionState.value,
error: connectionError.value,
reconnectAttempts: reconnectAttempts.value,
maxReconnectAttempts,
canReconnect: canReconnect.value,
url,
readyState: ws?.readyState,
protocol: ws?.protocol,
extensions: ws?.extensions
});
// === 初始化 ===
if (autoConnect) {
nextTick(() => {
connect();
});
}
// === 清理 ===
onUnmounted(() => {
disconnect();
});
// 监听连接状态变化,用于调试
if (debug) {
watch(connectionState, (newState, oldState) => {
log('info', `State transition: ${oldState} -> ${newState}`);
});
}
return {
// === 状态(只读) ===
connectionState: computed(() => connectionState.value),
isConnected,
isConnecting,
connectionError: computed(() => connectionError.value),
reconnectAttempts: computed(() => reconnectAttempts.value),
canReconnect,
lastMessage: computed(() => lastMessage.value),
messageHistory: computed(() => messageHistory.value),
// === 向后兼容的状态 ===
migrationProgress,
// === 连接控制 ===
connect,
disconnect,
reconnect,
// === 消息发送 ===
send,
sendMessage,
// === 事件系统 ===
on,
off,
onConnection,
offConnection,
// === 工具方法 ===
getConnectionInfo,
clearHistory: () => {
messageHistory.value = [];
lastMessage.value = null;
}
};
}
// 导出类型
export type { WebSocketMessage, MigrationProgress, WebSocketOptions, MessageHandler, ConnectionEventType };
export { ConnectionState };

View File

@@ -3,7 +3,8 @@ import messages from './locales';
// 创建i18n实例
const i18n = createI18n({
compositionOnly: false,
legacy: false,
compositionOnly: true,
globalInjection: true,
silentTranslationWarn: true,
locale: 'zh-CN',

View File

@@ -1,4 +1,5 @@
export default {
locale: 'en-US',
titlebar: {
minimize: 'Minimize',
maximize: 'Maximize',
@@ -11,38 +12,35 @@ export default {
characters: 'Ch',
selected: 'Sel'
},
fontSize: 'Font Size',
fontSizeTooltip: 'Font Size (Ctrl+wheel to adjust)',
tabLabel: 'Tab',
tabType: {
spaces: 'Spaces',
tab: 'Tab'
},
encoding: 'UTF-8',
settings: 'Settings',
alwaysOnTop: 'Always on Top'
},
config: {
loadSuccess: 'Configuration loaded successfully',
loadFailed: 'Failed to load configuration',
saveSuccess: 'Configuration saved',
saveFailed: 'Failed to save configuration',
resetSuccess: 'Configuration reset to defaults',
resetFailed: 'Failed to reset configuration',
fontSizeFixed: 'Font size ({value}) has been corrected to {fixed}',
tabSizeFixed: 'Tab size ({value}) has been corrected to {fixed}',
tabTypeFixed: 'Tab type ({value}) is invalid, corrected to spaces',
alwaysOnTopFailed: 'Failed to set window always on top',
alwaysOnTopSuccess: 'Window always on top status updated',
languageChanged: 'Language setting updated',
languageChangeFailed: 'Failed to update language setting',
themeChanged: 'Theme setting updated',
themeChangeFailed: 'Failed to update theme setting',
systemThemeChanged: 'System theme setting updated',
systemThemeChangeFailed: 'Failed to update system theme setting'
alwaysOnTop: 'Always on Top',
blockLanguage: 'Block Language',
searchLanguage: 'Search language...',
noLanguageFound: 'No language found',
formatHint: 'Click Format Block (Ctrl+Shift+F)',
// Document selector
selectDocument: 'Select Document',
searchOrCreateDocument: 'Search or enter new document name...',
createDocument: 'Create',
noDocumentFound: 'No document found',
loading: 'Loading...',
rename: 'Rename',
delete: 'Delete',
confirm: 'Confirm',
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': '简体中文',
'zh-CN': 'Chinese',
'en-US': 'English'
},
systemTheme: {
@@ -50,23 +48,69 @@ export default {
light: 'Light',
auto: 'Follow System'
},
document: {
loadSuccess: 'Document loaded successfully',
loadFailed: 'Failed to load document',
saveSuccess: 'Document saved successfully',
saveFailed: 'Failed to save document',
manualSaveSuccess: 'Manually saved successfully',
settings: {
loadFailed: 'Failed to load save settings',
saveSuccess: 'Save settings updated',
saveFailed: 'Failed to update save settings'
keybindings: {
headers: {
shortcut: 'Shortcut',
category: 'Category',
description: 'Description'
},
},
migration: {
started: 'Starting data migration',
migrating: 'Migrating',
completed: 'Migration Completed',
failed: 'Migration Failed'
commands: {
showSearch: 'Show search panel',
hideSearch: 'Hide search panel',
searchToggleCase: 'Toggle case-sensitive matching',
searchToggleWord: 'Toggle whole word matching',
searchToggleRegex: 'Toggle regular expression matching',
searchShowReplace: 'Show replace functionality',
searchReplaceAll: 'Replace all matches',
blockSelectAll: 'Select all in block',
blockAddAfterCurrent: 'Add new block after current',
blockAddAfterLast: 'Add new block at end',
blockAddBeforeCurrent: 'Add new block before current',
blockGotoPrevious: 'Go to previous block',
blockGotoNext: 'Go to next block',
blockSelectPrevious: 'Select previous block',
blockSelectNext: 'Select next block',
blockDelete: 'Delete current block',
blockMoveUp: 'Move current block up',
blockMoveDown: 'Move current block down',
blockDeleteLine: 'Delete line',
blockMoveLineUp: 'Move line up',
blockMoveLineDown: 'Move line down',
blockTransposeChars: 'Transpose characters',
blockFormat: 'Format code block',
blockCopy: 'Copy',
blockCut: 'Cut',
blockPaste: 'Paste',
historyUndo: 'Undo',
historyRedo: 'Redo',
historyUndoSelection: 'Undo selection',
historyRedoSelection: 'Redo selection',
foldCode: 'Fold code',
unfoldCode: 'Unfold code',
foldAll: 'Fold all',
unfoldAll: 'Unfold all',
cursorSyntaxLeft: 'Cursor syntax left',
cursorSyntaxRight: 'Cursor syntax right',
selectSyntaxLeft: 'Select syntax left',
selectSyntaxRight: 'Select syntax right',
copyLineUp: 'Copy line up',
copyLineDown: 'Copy line down',
insertBlankLine: 'Insert blank line',
selectLine: 'Select line',
selectParentSyntax: 'Select parent syntax',
indentLess: 'Indent less',
indentMore: 'Indent more',
indentSelection: 'Indent selection',
cursorMatchingBracket: 'Cursor matching bracket',
toggleComment: 'Toggle comment',
toggleBlockComment: 'Toggle block comment',
insertNewlineAndIndent: 'Insert newline and indent',
deleteCharBackward: 'Delete character backward',
deleteCharForward: 'Delete character forward',
deleteGroupBackward: 'Delete group backward',
deleteGroupForward: 'Delete group forward',
textHighlightToggle: 'Toggle text highlight',
}
},
settings: {
title: 'Settings',
@@ -75,27 +119,28 @@ export default {
general: 'General',
editing: 'Editor',
appearance: 'Appearance',
backupPage: 'Backup',
keyBindings: 'Key Bindings',
updates: 'Updates',
comingSoon: 'Coming Soon...',
save: 'Save',
reset: 'Reset',
apply: 'Apply',
cancel: 'Cancel',
dangerZone: 'Danger Zone',
resetAllSettings: 'Reset All Settings',
confirmReset: 'Click again to confirm reset',
confirmReset: 'Confirm the reset?',
globalHotkey: 'Global Keyboard Shortcuts',
enableGlobalHotkey: 'Enable Global Hotkeys',
window: 'Window/Application',
showInSystemTray: 'Show in System Tray',
enableSystemTray: 'Enable System Tray',
alwaysOnTop: 'Always on Top',
startup: 'Startup Settings',
startAtLogin: 'Start at Login',
dataStorage: 'Data Storage',
dataPath: 'Data Storage Path',
clickToSelectPath: 'Click to select path',
resetDefault: 'Reset Default',
resetToDefaultPath: 'Reset to default path',
restartRequiredForDataPath: 'Restart required',
fontSize: 'Font Size',
fontSizeDescription: 'Editor font size',
fontSettings: 'Font Settings',
@@ -103,6 +148,60 @@ export default {
fontFamilyDescription: 'Choose editor font family',
fontWeight: 'Font Weight',
fontWeightDescription: 'Set the thickness of the font',
fontWeights: {
'100': 'Thin (100)',
'200': 'Extra Light (200)',
'300': 'Light (300)',
'normal': 'Regular (400)',
'500': 'Medium (500)',
'600': 'Semi Bold (600)',
'bold': 'Bold (700)',
'800': 'Extra Bold (800)',
'900': 'Black (900)'
},
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',
background: 'Main Background',
backgroundSecondary: 'Secondary Background',
surface: 'Panel Background',
foreground: 'Primary Text',
foregroundSecondary: 'Secondary Text',
comment: 'Comments',
keyword: 'Keywords',
string: 'Strings',
function: 'Functions',
number: 'Numbers',
operator: 'Operators',
variable: 'Variables',
type: 'Types',
cursor: 'Cursor',
selection: 'Selection Background',
selectionBlur: 'Unfocused Selection',
activeLine: 'Active Line Highlight',
lineNumber: 'Line Numbers',
activeLineNumber: 'Active Line Number',
borderColor: 'Border Color',
borderLight: 'Light Border',
searchMatch: 'Search Match',
matchingBracket: 'Matching Bracket'
},
fontFamilies: {
harmonyOS: 'HarmonyOS Sans',
microsoftYahei: 'Microsoft YaHei',
pingfang: 'PingFang SC',
jetbrainsMono: 'JetBrains Mono',
firaCode: 'Fira Code',
sourceCodePro: 'Source Code Pro',
cascadiaCode: 'Cascadia Code'
},
lineHeight: 'Line Height',
lineHeightDescription: 'Set the spacing between text lines',
tabSettings: 'Tab Settings',
@@ -113,15 +212,127 @@ export default {
enableTabIndent: 'Enable Tab Indent',
language: 'Interface Language',
systemTheme: 'System Theme',
theme: 'Editor Theme',
themeDescription: 'Choose editor theme',
restartRequired: '(Restart required)',
saveOptions: 'Save Options',
autoSaveDelay: 'Auto Save Delay (ms)',
selectDirectoryFailed: 'Failed to select directory',
validation: {
customPathRequired: 'A valid directory must be selected when enabling custom path',
customPathAutoDisabled: 'Custom data path has been automatically disabled due to no valid directory selected'
updateSettings: 'Update Settings',
autoCheckUpdates: 'Automatically Check Updates',
autoCheckUpdatesDescription: 'Check for updates when application starts',
manualCheck: 'Manual Update',
currentVersion: 'Current Version',
checkForUpdates: 'Check for Updates',
checking: 'Checking...',
checkFailed: 'Check Failed',
newVersionAvailable: 'New Version Available',
upToDate: 'Up to Date',
viewUpdate: 'View Update',
releaseNotes: 'Release Notes',
networkError: 'Network connection error, please check your network settings',
extensions: 'Extensions',
extensionsPage: {
loading: 'Loading',
categoryEditing: 'Editing Enhancement',
categoryUI: 'UI Enhancement',
categoryTools: 'Tools',
configuration: 'Configuration',
resetToDefault: 'Reset to Default Configuration',
},
updateNow: 'Update Now',
updating: 'Updating...',
updateSuccess: 'Update Success',
updateSuccessRestartRequired: 'Update has been successfully applied. Please restart the application.',
restartNow: 'Restart Now',
hotkeyPreview: 'Preview:',
none: 'None',
backup: {
basicSettings: 'Basic Settings',
enableBackup: 'Enable Git Backup',
autoBackup: 'Auto Backup',
backupInterval: 'Backup Interval',
intervals: {
'5min': '5 minutes',
'10min': '10 minutes',
'15min': '15 minutes',
'30min': '30 minutes',
'1hour': '1 hour'
},
repositoryConfig: 'Repository Configuration',
repoUrl: 'Repository URL',
repoUrlPlaceholder: 'Enter Git repository URL',
authConfig: 'Authentication Configuration',
authMethod: 'Authentication Method',
authMethods: {
token: 'Access Token',
sshKey: 'SSH Key',
userPass: 'Username/Password'
},
username: 'Username',
usernamePlaceholder: 'Enter username',
password: 'Password',
passwordPlaceholder: 'Enter password',
token: 'Access Token',
tokenPlaceholder: 'Enter access token',
sshKeyPath: 'SSH Key Path',
sshKeyPathPlaceholder: 'Select SSH key file',
sshKeyPassphrase: 'SSH Key Passphrase',
sshKeyPassphrasePlaceholder: 'Enter SSH key passphrase',
backupOperations: 'Backup Operations',
pushToRemote: 'Push to Remote',
pushing: 'Pushing...',
actions: {
push: 'Push',
},
status: {
success: 'Success',
failed: 'Failed'
}
},
},
extensions: {
rainbowBrackets: {
name: 'Rainbow Brackets',
description: 'Display nested brackets in different colors'
},
hyperlink: {
name: 'Hyperlink',
description: 'Recognize and make hyperlinks clickable'
},
colorSelector: {
name: 'Color Selector',
description: 'Visual color picker and color value display'
},
translator: {
name: 'Text Translator',
description: 'Translate selected text with multiple translation services'
},
minimap: {
name: 'Minimap',
description: 'Display minimap overview of the document'
},
search: {
name: 'Search',
description: 'Text search and replace functionality'
},
fold: {
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'
},
codeblock: {
name: 'Code Block',
description: 'Code block related functionality'
}
},
monitor: {
memory: 'Memory',
clickToClean: 'Click to clean memory'
}
};
};

View File

@@ -1,4 +1,5 @@
export default {
locale: 'zh-CN',
titlebar: {
minimize: '最小化',
maximize: '最大化',
@@ -11,35 +12,32 @@ export default {
characters: 'Ch',
selected: 'Sel'
},
fontSize: '字体大小',
fontSizeTooltip: '字体大小 (Ctrl+滚轮调整)',
tabLabel: 'Tab',
tabType: {
spaces: '空格',
tab: '制表符'
},
encoding: 'UTF-8',
settings: '设置',
alwaysOnTop: '窗口置顶'
},
config: {
loadSuccess: '配置加载成功',
loadFailed: '配置加载失败',
saveSuccess: '配置已保存',
saveFailed: '配置保存失败',
resetSuccess: '配置已重置为默认值',
resetFailed: '重置配置失败',
fontSizeFixed: '字体大小值({value})已被修正为{fixed}',
tabSizeFixed: 'Tab大小值({value})已被修正为{fixed}',
tabTypeFixed: 'Tab类型({value})不合法,已修正为空格',
alwaysOnTopFailed: '无法设置窗口置顶状态',
alwaysOnTopSuccess: '窗口置顶状态已更新',
languageChanged: '语言设置已更新',
languageChangeFailed: '语言设置更新失败',
themeChanged: '主题设置已更新',
themeChangeFailed: '主题设置更新失败',
systemThemeChanged: '系统主题设置已更新',
systemThemeChangeFailed: '系统主题设置更新失败'
alwaysOnTop: '窗口置顶',
blockLanguage: '块语言',
searchLanguage: '搜索语言...',
noLanguageFound: '未找到匹配的语言',
formatHint: '点击格式化区块Ctrl+Shift+F',
// 文档选择器
selectDocument: '选择文档',
searchOrCreateDocument: '搜索或输入新文档名...',
createDocument: '创建',
noDocumentFound: '没有找到文档',
loading: '加载中...',
rename: '重命名',
delete: '删除',
confirm: '确认',
confirmDelete: '再次点击确认删除',
openInNewWindow: '在新窗口中打开',
alreadyOpenInNewWindow: '已在新窗口中打开',
documentNameTooLong: '文档名称不能超过{max}个字符',
documentNameRequired: '文档名称不能为空',
cannotDeleteLastDocument: '无法删除最后一个文档',
cannotDeleteDefaultDocument: '无法删除默认文档',
unknownTime: '未知时间',
invalidDate: '无效日期',
timeError: '时间错误',
},
languages: {
'zh-CN': '简体中文',
@@ -50,23 +48,69 @@ export default {
light: '浅色',
auto: '跟随系统'
},
document: {
loadSuccess: '文档加载成功',
loadFailed: '文档加载失败',
saveSuccess: '文档保存成功',
saveFailed: '文档保存失败',
manualSaveSuccess: '手动保存成功',
settings: {
loadFailed: '加载保存设置失败',
saveSuccess: '保存设置已更新',
saveFailed: '保存设置更新失败'
keybindings: {
headers: {
shortcut: '快捷键',
category: '分类',
description: '描述'
},
},
migration: {
started: '开始迁移数据',
migrating: '迁移中',
completed: '迁移已完成',
failed: '迁移失败'
commands: {
showSearch: '显示搜索面板',
hideSearch: '隐藏搜索面板',
searchToggleCase: '切换大小写敏感匹配',
searchToggleWord: '切换整词匹配',
searchToggleRegex: '切换正则表达式匹配',
searchShowReplace: '显示替换功能',
searchReplaceAll: '替换全部匹配项',
blockSelectAll: '块内选择全部',
blockAddAfterCurrent: '在当前块后添加新块',
blockAddAfterLast: '在最后添加新块',
blockAddBeforeCurrent: '在当前块前添加新块',
blockGotoPrevious: '跳转到上一个块',
blockGotoNext: '跳转到下一个块',
blockSelectPrevious: '选择上一个块',
blockSelectNext: '选择下一个块',
blockDelete: '删除当前块',
blockMoveUp: '向上移动当前块',
blockMoveDown: '向下移动当前块',
blockDeleteLine: '删除行',
blockMoveLineUp: '向上移动行',
blockMoveLineDown: '向下移动行',
blockTransposeChars: '字符转置',
blockFormat: '格式化代码块',
blockCopy: '复制',
blockCut: '剪切',
blockPaste: '粘贴',
historyUndo: '撤销',
historyRedo: '重做',
historyUndoSelection: '撤销选择',
historyRedoSelection: '重做选择',
foldCode: '折叠代码',
unfoldCode: '展开代码',
foldAll: '折叠全部',
unfoldAll: '展开全部',
cursorSyntaxLeft: '光标按语法左移',
cursorSyntaxRight: '光标按语法右移',
selectSyntaxLeft: '按语法选择左侧',
selectSyntaxRight: '按语法选择右侧',
copyLineUp: '向上复制行',
copyLineDown: '向下复制行',
insertBlankLine: '插入空行',
selectLine: '选择行',
selectParentSyntax: '选择父级语法',
indentLess: '减少缩进',
indentMore: '增加缩进',
indentSelection: '缩进选择',
cursorMatchingBracket: '光标到匹配括号',
toggleComment: '切换注释',
toggleBlockComment: '切换块注释',
insertNewlineAndIndent: '插入新行并缩进',
deleteCharBackward: '向后删除字符',
deleteCharForward: '向前删除字符',
deleteGroupBackward: '向后删除组',
deleteGroupForward: '向前删除组',
textHighlightToggle: '切换文本高亮',
}
},
settings: {
title: '设置',
@@ -75,27 +119,29 @@ export default {
general: '常规',
editing: '编辑器',
appearance: '外观',
backupPage: '备份',
extensions: '扩展',
keyBindings: '快捷键',
updates: '更新',
comingSoon: '即将推出...',
save: '保存',
reset: '重置',
apply: '应用',
cancel: '取消',
dangerZone: '危险操作',
resetAllSettings: '重置所有设置',
confirmReset: '再次点击确认重置',
confirmReset: '确认重置?',
globalHotkey: '全局键盘快捷键',
enableGlobalHotkey: '启用全局热键',
window: '窗口/应用程序',
showInSystemTray: '在系统托盘中显示',
enableSystemTray: '启用系统托盘',
alwaysOnTop: '窗口始终置顶',
startup: '启动设置',
startAtLogin: '开机自启动',
dataStorage: '数据存储',
dataPath: '数据存储路径',
clickToSelectPath: '点击选择路径',
resetDefault: '恢复默认',
resetToDefaultPath: '恢复为默认路径',
restartRequiredForDataPath: '需要重启应用程序',
fontSize: '字体大小',
fontSizeDescription: '编辑器字体大小',
fontSettings: '字体设置',
@@ -113,15 +159,181 @@ export default {
enableTabIndent: '启用 Tab 缩进',
language: '界面语言',
systemTheme: '系统主题',
theme: '编辑器主题',
themeDescription: '选择编辑器主题',
restartRequired: '(需要重启)',
saveOptions: '保存选项',
autoSaveDelay: '自动保存延迟(毫秒)',
selectDirectoryFailed: '选择目录失败',
validation: {
customPathRequired: '启用自定义路径时必须选择一个有效的目录',
customPathAutoDisabled: '由于未选择有效目录,自定义数据路径已自动关闭'
updateSettings: '更新设置',
autoCheckUpdates: '自动检查更新',
autoCheckUpdatesDescription: '应用启动时自动检查更新',
manualCheck: '手动更新',
currentVersion: '当前版本',
checkForUpdates: '检查更新',
checking: '正在检查...',
checkFailed: '检查失败',
newVersionAvailable: '发现新版本',
upToDate: '已是最新版本',
viewUpdate: '查看更新',
releaseNotes: '更新日志',
networkError: '网络连接错误,请检查网络设置',
updateNow: '立即更新',
updating: '正在更新...',
updateSuccess: '更新成功',
updateSuccessRestartRequired: '更新已成功应用,请重启应用以生效',
updateSuccessNoRestart: '更新已完成,无需重启',
restartNow: '立即重启',
extensionsPage: {
loading: '加载中',
categoryEditing: '编辑增强',
categoryUI: '界面增强',
categoryTools: '工具扩展',
configuration: '配置',
resetToDefault: '重置为默认配置',
},
fontWeights: {
'100': '极细 (100)',
'200': '超细 (200)',
'300': '细 (300)',
'normal': '正常 (400)',
'500': '中等 (500)',
'600': '半粗 (600)',
'bold': '粗体 (700)',
'800': '超粗 (800)',
'900': '极粗 (900)'
},
customThemeColors: '自定义主题颜色',
resetToDefault: '重置为默认',
colorValue: '颜色值',
themeColors: {
basic: '基础色调',
text: '文本颜色',
syntax: '语法高亮',
interface: '界面元素',
border: '边框分割线',
search: '搜索匹配',
background: '主背景色',
backgroundSecondary: '次要背景色',
surface: '面板背景',
foreground: '主文本色',
foregroundSecondary: '次要文本色',
comment: '注释色',
keyword: '关键字',
string: '字符串',
function: '函数名',
number: '数字',
operator: '操作符',
variable: '变量',
type: '类型',
cursor: '光标',
selection: '选中背景',
selectionBlur: '失焦选中背景',
activeLine: '当前行高亮',
lineNumber: '行号',
activeLineNumber: '活动行号',
borderColor: '边框色',
borderLight: '浅色边框',
searchMatch: '搜索匹配',
matchingBracket: '匹配括号'
},
fontFamilies: {
harmonyOS: '鸿蒙字体',
microsoftYahei: '微软雅黑',
pingfang: '苹方字体',
jetbrainsMono: 'JetBrains Mono',
firaCode: 'Fira Code',
sourceCodePro: 'Source Code Pro',
cascadiaCode: 'Cascadia Code'
},
hotkeyPreview: '预览:',
none: '无',
backup: {
basicSettings: '基本设置',
enableBackup: '启用备份',
autoBackup: '自动备份',
backupInterval: '备份间隔',
intervals: {
'5min': '5分钟',
'10min': '10分钟',
'15min': '15分钟',
'30min': '30分钟',
'1hour': '1小时'
},
repositoryConfig: '仓库配置',
repoUrl: '仓库地址',
repoUrlPlaceholder: '请输入Git仓库地址',
authConfig: '认证配置',
authMethod: '认证方式',
authMethods: {
token: '访问令牌',
sshKey: 'SSH密钥',
userPass: '用户名密码'
},
username: '用户名',
usernamePlaceholder: '请输入用户名',
password: '密码',
passwordPlaceholder: '请输入密码',
token: '访问令牌',
tokenPlaceholder: '请输入访问令牌',
sshKeyPath: 'SSH密钥路径',
sshKeyPathPlaceholder: '请选择SSH密钥文件',
sshKeyPassphrase: 'SSH密钥密码',
sshKeyPassphrasePlaceholder: '请输入SSH密钥密码',
backupOperations: '备份操作',
pushToRemote: '推送到远程',
pushing: '推送中...',
actions: {
push: '推送',
},
status: {
success: '成功',
failed: '失败'
}
},
},
extensions: {
rainbowBrackets: {
name: '彩虹括号',
description: '用不同颜色显示嵌套括号'
},
hyperlink: {
name: '超链接',
description: '识别并可点击超链接'
},
colorSelector: {
name: '颜色选择器',
description: '颜色值的可视化和选择'
},
translator: {
name: '划词翻译',
description: '选择文本后显示翻译按钮,支持多种翻译服务'
},
minimap: {
name: '小地图',
description: '显示小地图视图'
},
search: {
name: '搜索功能',
description: '文本搜索和替换功能'
},
fold: {
name: '代码折叠',
description: '折叠和展开代码段以提高代码可读性'
},
textHighlight: {
name: '文本高亮',
description: '高亮选中的文本内容 (Ctrl+Shift+H 切换高亮)',
backgroundColor: '背景颜色',
opacity: '透明度'
},
checkbox: {
name: '选择框',
description: '将 [x] 和 [ ] 渲染为可交互的选择框'
},
codeblock: {
name: '代码块',
description: '代码块相关功能'
}
},
monitor: {
memory: '内存',
clickToClean: '点击清理内存'
}
};
};

View File

@@ -6,6 +6,8 @@ import EditingPage from '@/views/settings/pages/EditingPage.vue';
import AppearancePage from '@/views/settings/pages/AppearancePage.vue';
import KeyBindingsPage from '@/views/settings/pages/KeyBindingsPage.vue';
import UpdatesPage from '@/views/settings/pages/UpdatesPage.vue';
import ExtensionsPage from '@/views/settings/pages/ExtensionsPage.vue';
import BackupPage from '@/views/settings/pages/BackupPage.vue';
const routes: RouteRecordRaw[] = [
{
@@ -34,6 +36,11 @@ const routes: RouteRecordRaw[] = [
name: 'SettingsAppearance',
component: AppearancePage
},
{
path: 'extensions',
name: 'SettingsExtensions',
component: ExtensionsPage
},
{
path: 'key-bindings',
name: 'SettingsKeyBindings',
@@ -43,6 +50,11 @@ const routes: RouteRecordRaw[] = [
path: 'updates',
name: 'SettingsUpdates',
component: UpdatesPage
},
{
path: 'backup',
name: 'SettingsBackup',
component: BackupPage
}
]
}
@@ -53,4 +65,4 @@ const router = createRouter({
routes: routes
});
export default router;
export default router;

View File

@@ -0,0 +1,123 @@
import {defineStore} from 'pinia'
import {computed, readonly, ref} from 'vue'
import type {GitBackupConfig} from '@/../bindings/voidraft/internal/models'
import {BackupService} from '@/../bindings/voidraft/internal/services'
import {useConfigStore} from '@/stores/configStore'
/**
* Minimalist Backup Store
*/
export const useBackupStore = defineStore('backup', () => {
// Core state
const config = ref<GitBackupConfig | null>(null)
const isPushing = ref(false)
const error = ref<string | null>(null)
const isInitialized = ref(false)
// Backup result states
const pushSuccess = ref(false)
const pushError = ref(false)
// Timers for auto-hiding status icons and error messages
let pushStatusTimer: number | null = null
let errorTimer: number | null = null
// 获取configStore
const configStore = useConfigStore()
// Computed properties
const isEnabled = computed(() => configStore.config.backup.enabled)
const isConfigured = computed(() => configStore.config.backup.repo_url)
// 清除状态显示
const clearPushStatus = () => {
if (pushStatusTimer !== null) {
window.clearTimeout(pushStatusTimer)
pushStatusTimer = null
}
pushSuccess.value = false
pushError.value = false
}
// 清除错误信息和错误图标
const clearError = () => {
if (errorTimer !== null) {
window.clearTimeout(errorTimer)
errorTimer = null
}
error.value = null
pushError.value = false
}
// 设置错误信息和错误图标并自动清除
const setErrorWithAutoHide = (errorMessage: string, hideAfter: number = 3000) => {
clearError()
clearPushStatus()
error.value = errorMessage
pushError.value = true
errorTimer = window.setTimeout(() => {
error.value = null
pushError.value = false
errorTimer = null
}, hideAfter)
}
// Push to remote repository
const pushToRemote = async () => {
if (isPushing.value || !isConfigured.value) return
isPushing.value = true
clearError() // 清除之前的错误信息
clearPushStatus()
try {
await BackupService.PushToRemote()
// 显示成功状态并设置3秒后自动消失
pushSuccess.value = true
pushStatusTimer = window.setTimeout(() => {
pushSuccess.value = false
pushStatusTimer = null
}, 3000)
} catch (err: any) {
setErrorWithAutoHide(err?.message || 'Backup operation failed')
} finally {
isPushing.value = false
}
}
// 初始化备份服务
const initialize = async () => {
if (!isEnabled.value) return
// 避免重复初始化
if (isInitialized.value) return
clearError() // 清除之前的错误信息
try {
await BackupService.Initialize()
isInitialized.value = true
} catch (err: any) {
setErrorWithAutoHide(err?.message || 'Failed to initialize backup service')
}
}
return {
// State
config: readonly(config),
isPushing: readonly(isPushing),
error: readonly(error),
isInitialized: readonly(isInitialized),
pushSuccess: readonly(pushSuccess),
pushError: readonly(pushError),
// Computed
isEnabled,
isConfigured,
// Methods
pushToRemote,
initialize,
clearError
}
})

View File

@@ -1,6 +1,6 @@
import {defineStore} from 'pinia';
import {computed, reactive} from 'vue';
import {ConfigService} from '@/../bindings/voidraft/internal/services';
import {ConfigService, StartupService} from '@/../bindings/voidraft/internal/services';
import {
AppConfig,
AppearanceConfig,
@@ -9,13 +9,16 @@ import {
LanguageType,
SystemThemeType,
TabType,
ThemeType
} from '@/../bindings/voidraft/internal/models';
UpdatesConfig,
UpdateSourceType,
GitBackupConfig,
AuthMethod
} from '@/../bindings/voidraft/internal/models/models';
import {useI18n} from 'vue-i18n';
import {useErrorHandler} from '@/utils/errorHandler';
import {ConfigUtils} from '@/utils/configUtils';
import {WindowController} from '@/utils/windowController';
import * as runtime from '@wailsio/runtime';
import {useBackupStore} from '@/stores/backupStore';
// 国际化相关导入
export type SupportedLocaleType = 'zh-CN' | 'en-US';
@@ -44,32 +47,66 @@ type AppearanceConfigKeyMap = {
readonly [K in keyof AppearanceConfig]: string;
};
type UpdatesConfigKeyMap = {
readonly [K in keyof UpdatesConfig]: string;
};
type BackupConfigKeyMap = {
readonly [K in keyof GitBackupConfig]: string;
};
type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight';
// 配置键映射
const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
alwaysOnTop: 'general.always_on_top',
dataPath: 'general.data_path',
enableSystemTray: 'general.enable_system_tray',
enableGlobalHotkey: 'general.enable_global_hotkey',
globalHotkey: 'general.global_hotkey'
alwaysOnTop: 'general.alwaysOnTop',
dataPath: 'general.dataPath',
enableSystemTray: 'general.enableSystemTray',
startAtLogin: 'general.startAtLogin',
enableGlobalHotkey: 'general.enableGlobalHotkey',
globalHotkey: 'general.globalHotkey'
} as const;
const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
fontSize: 'editing.font_size',
fontFamily: 'editing.font_family',
fontWeight: 'editing.font_weight',
lineHeight: 'editing.line_height',
enableTabIndent: 'editing.enable_tab_indent',
tabSize: 'editing.tab_size',
tabType: 'editing.tab_type',
autoSaveDelay: 'editing.auto_save_delay'
fontSize: 'editing.fontSize',
fontFamily: 'editing.fontFamily',
fontWeight: 'editing.fontWeight',
lineHeight: 'editing.lineHeight',
enableTabIndent: 'editing.enableTabIndent',
tabSize: 'editing.tabSize',
tabType: 'editing.tabType',
autoSaveDelay: 'editing.autoSaveDelay'
} as const;
const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
language: 'appearance.language',
theme: 'appearance.theme',
systemTheme: 'appearance.system_theme'
systemTheme: 'appearance.systemTheme',
customTheme: 'appearance.customTheme'
} as const;
const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
version: 'updates.version',
autoUpdate: 'updates.autoUpdate',
primarySource: 'updates.primarySource',
backupSource: 'updates.backupSource',
backupBeforeUpdate: 'updates.backupBeforeUpdate',
updateTimeout: 'updates.updateTimeout',
github: 'updates.github',
gitea: 'updates.gitea'
} as const;
const BACKUP_CONFIG_KEY_MAP: BackupConfigKeyMap = {
enabled: 'backup.enabled',
repo_url: 'backup.repo_url',
auth_method: 'backup.auth_method',
username: 'backup.username',
password: 'backup.password',
token: 'backup.token',
ssh_key_path: 'backup.ssh_key_path',
ssh_key_passphrase: 'backup.ssh_key_passphrase',
backup_interval: 'backup.backup_interval',
auto_backup: 'backup.auto_backup',
} as const;
// 配置限制
@@ -80,26 +117,40 @@ const CONFIG_LIMITS = {
tabType: {values: [TabType.TabTypeSpaces, TabType.TabTypeTab], default: TabType.TabTypeSpaces}
} as const;
// 常用字体选项
export const FONT_OPTIONS = [
// 创建获取翻译的函数
export const createFontOptions = (t: (key: string) => string) => [
{
label: '鸿蒙字体',
label: t('settings.fontFamilies.harmonyOS'),
value: '"HarmonyOS Sans SC", "HarmonyOS Sans", "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial, sans-serif'
},
{label: '微软雅黑', value: '"Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial, sans-serif'},
{label: '苹方字体', value: '"PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif'},
{
label: 'JetBrains Mono',
label: t('settings.fontFamilies.microsoftYahei'),
value: '"Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial, sans-serif'
},
{
label: t('settings.fontFamilies.pingfang'),
value: '"PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif'
},
{
label: t('settings.fontFamilies.jetbrainsMono'),
value: '"JetBrains Mono", "Fira Code", "SF Mono", Monaco, Consolas, "Ubuntu Mono", monospace'
},
{label: 'Fira Code', value: '"Fira Code", "JetBrains Mono", "SF Mono", Monaco, Consolas, "Ubuntu Mono", monospace'},
{label: 'Source Code Pro', value: '"Source Code Pro", "SF Mono", Monaco, Consolas, "Ubuntu Mono", monospace'},
{label: 'Cascadia Code', value: '"Cascadia Code", "SF Mono", Monaco, Consolas, "Ubuntu Mono", monospace'},
{
label: '系统等宽字体',
value: '"SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace'
label: t('settings.fontFamilies.firaCode'),
value: '"Fira Code", "JetBrains Mono", "SF Mono", Monaco, Consolas, "Ubuntu Mono", monospace'
},
{
label: t('settings.fontFamilies.sourceCodePro'),
value: '"Source Code Pro", "SF Mono", Monaco, Consolas, "Ubuntu Mono", monospace'
},
{
label: t('settings.fontFamilies.cascadiaCode'),
value: '"Cascadia Code", "SF Mono", Monaco, Consolas, "Ubuntu Mono", monospace'
}
] as const;
];
// 常用字体选项
export const FONT_OPTIONS = createFontOptions((key) => key);
// 获取浏览器的默认语言
const getBrowserLanguage = (): SupportedLocaleType => {
@@ -120,6 +171,7 @@ const DEFAULT_CONFIG: AppConfig = {
alwaysOnTop: false,
dataPath: '',
enableSystemTray: true,
startAtLogin: false,
enableGlobalHotkey: false,
globalHotkey: {
ctrl: false,
@@ -141,21 +193,116 @@ const DEFAULT_CONFIG: AppConfig = {
},
appearance: {
language: LanguageType.LangZhCN,
theme: 'default-dark' as ThemeType,
systemTheme: 'dark' as SystemThemeType
systemTheme: SystemThemeType.SystemThemeAuto,
customTheme: {
darkTheme: {
// 基础色调
background: '#252B37',
backgroundSecondary: '#213644',
surface: '#474747',
foreground: '#9BB586',
foregroundSecondary: '#9c9c9c',
// 语法高亮
comment: '#6272a4',
keyword: '#ff79c6',
string: '#f1fa8c',
function: '#50fa7b',
number: '#bd93f9',
operator: '#ff79c6',
variable: '#8fbcbb',
type: '#8be9fd',
// 界面元素
cursor: '#fff',
selection: '#0865a9aa',
selectionBlur: '#225377aa',
activeLine: 'rgba(255,255,255,0.04)',
lineNumber: 'rgba(255,255,255, 0.15)',
activeLineNumber: 'rgba(255,255,255, 0.6)',
// 边框分割线
borderColor: '#1e222a',
borderLight: 'rgba(255,255,255, 0.1)',
// 搜索匹配
searchMatch: '#8fbcbb',
matchingBracket: 'rgba(255,255,255,0.1)'
},
lightTheme: {
// 基础色调
background: '#ffffff',
backgroundSecondary: '#f1faf1',
surface: '#f5f5f5',
foreground: '#444d56',
foregroundSecondary: '#6a737d',
// 语法高亮
comment: '#6a737d',
keyword: '#d73a49',
string: '#032f62',
function: '#005cc5',
number: '#005cc5',
operator: '#d73a49',
variable: '#24292e',
type: '#6f42c1',
// 界面元素
cursor: '#000',
selection: '#77baff8c',
selectionBlur: '#b2c2ca85',
activeLine: '#000000',
lineNumber: '#000000',
activeLineNumber: '#000000',
// 边框分割线
borderColor: '#dfdfdf',
borderLight: '#0000000C',
// 搜索匹配
searchMatch: '#005cc5',
matchingBracket: 'rgba(0,0,0,0.1)'
}
}
},
updates: {
version: "1.0.0",
autoUpdate: true,
primarySource: UpdateSourceType.UpdateSourceGithub,
backupSource: UpdateSourceType.UpdateSourceGitea,
backupBeforeUpdate: true,
updateTimeout: 30,
github: {
owner: "landaiqing",
repo: "voidraft",
},
gitea: {
baseURL: "https://git.landaiqing.cn",
owner: "landaiqing",
repo: "voidraft",
}
},
backup: {
enabled: false,
repo_url: "",
auth_method: AuthMethod.UserPass,
username: "",
password: "",
token: "",
ssh_key_path: "",
ssh_key_passphrase: "",
backup_interval: 60,
auto_backup: true,
},
keyBindings: {},
updates: {},
metadata: {
version: '1.0.0',
lastUpdated: null
lastUpdated: new Date().toString(),
}
};
export const useConfigStore = defineStore('config', () => {
const {locale} = useI18n();
const {safeCall} = useErrorHandler();
const {locale, t} = useI18n();
// 响应式状态
const state = reactive({
@@ -163,6 +310,9 @@ export const useConfigStore = defineStore('config', () => {
isLoading: false,
configLoaded: false
});
// 初始化FONT_OPTIONS国际化版本
const localizedFontOptions = computed(() => createFontOptions(t));
// 计算属性 - 使用工厂函数简化
const createLimitComputed = (key: NumberConfigKey) => computed(() => CONFIG_LIMITS[key]);
@@ -216,6 +366,36 @@ export const useConfigStore = defineStore('config', () => {
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 initConfig = async (): Promise<void> => {
if (state.isLoading) return;
@@ -229,8 +409,8 @@ export const useConfigStore = defineStore('config', () => {
if (appConfig.general) Object.assign(state.config.general, appConfig.general);
if (appConfig.editing) Object.assign(state.config.editing, appConfig.editing);
if (appConfig.appearance) Object.assign(state.config.appearance, appConfig.appearance);
if (appConfig.keyBindings) Object.assign(state.config.keyBindings, appConfig.keyBindings);
if (appConfig.updates) Object.assign(state.config.updates, appConfig.updates);
if (appConfig.backup) Object.assign(state.config.backup, appConfig.backup);
if (appConfig.metadata) Object.assign(state.config.metadata, appConfig.metadata);
}
@@ -250,26 +430,26 @@ export const useConfigStore = defineStore('config', () => {
const clamp = (value: number) => ConfigUtils.clamp(value, limit.min, limit.max);
return {
increase: () => safeCall(() => updateEditingConfig(key, clamp(state.config.editing[key] + 1)), 'config.saveFailed', 'config.saveSuccess'),
decrease: () => safeCall(() => updateEditingConfig(key, clamp(state.config.editing[key] - 1)), 'config.saveFailed', 'config.saveSuccess'),
set: (value: number) => safeCall(() => updateEditingConfig(key, clamp(value)), 'config.saveFailed', 'config.saveSuccess'),
reset: () => safeCall(() => updateEditingConfig(key, limit.default), 'config.saveFailed', 'config.saveSuccess')
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)
};
};
// 通用布尔值切换器
const createGeneralToggler = <T extends keyof GeneralConfig>(key: T) =>
() => safeCall(() => updateGeneralConfig(key, !state.config.general[key] as GeneralConfig[T]), 'config.saveFailed', 'config.saveSuccess');
async () => await updateGeneralConfig(key, !state.config.general[key] as GeneralConfig[T]);
const createEditingToggler = <T extends keyof EditingConfig>(key: T) =>
() => safeCall(() => updateEditingConfig(key, !state.config.editing[key] as EditingConfig[T]), 'config.saveFailed', 'config.saveSuccess');
async () => await updateEditingConfig(key, !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 safeCall(() => updateEditingConfig(key, values[nextIndex]), 'config.saveFailed', 'config.saveSuccess');
return await updateEditingConfig(key, values[nextIndex]);
};
// 重置配置
@@ -278,16 +458,12 @@ export const useConfigStore = defineStore('config', () => {
state.isLoading = true;
try {
// 调用后端重置配置
await safeCall(() => ConfigService.ResetConfig(), 'config.resetFailed', 'config.resetSuccess');
// 立即重新加载后端配置以确保前端状态同步
await safeCall(async () => {
const appConfig = await ConfigService.GetConfig();
if (appConfig) {
state.config = JSON.parse(JSON.stringify(appConfig)) as AppConfig;
}
}, 'config.loadFailed', 'config.loadSuccess');
await ConfigService.ResetConfig()
const appConfig = await ConfigService.GetConfig();
if (appConfig) {
state.config = JSON.parse(JSON.stringify(appConfig)) as AppConfig;
}
} finally {
state.isLoading = false;
}
@@ -295,27 +471,61 @@ export const useConfigStore = defineStore('config', () => {
// 语言设置方法
const setLanguage = async (language: LanguageType): Promise<void> => {
await safeCall(async () => {
await updateAppearanceConfig('language', language);
await updateAppearanceConfig('language', language);
// 同步更新前端语言
const frontendLocale = ConfigUtils.backendLanguageToFrontend(language);
locale.value = frontendLocale as any;
}, 'config.languageChangeFailed', 'config.languageChanged');
};
// 主题设置方法
const setTheme = async (theme: ThemeType): Promise<void> => {
await safeCall(async () => {
await updateAppearanceConfig('theme', theme);
}, 'config.themeChangeFailed', 'config.themeChanged');
// 同步更新前端语言
const frontendLocale = ConfigUtils.backendLanguageToFrontend(language);
locale.value = frontendLocale as any;
};
// 系统主题设置方法
const setSystemTheme = async (systemTheme: SystemThemeType): Promise<void> => {
await safeCall(async () => {
await updateAppearanceConfig('systemTheme', systemTheme);
}, 'config.systemThemeChangeFailed', 'config.systemThemeChanged');
await updateAppearanceConfig('systemTheme', systemTheme);
};
// 更新自定义主题方法
const updateCustomTheme = async (themeType: 'darkTheme' | 'lightTheme', colorKey: string, colorValue: string): Promise<void> => {
// 确保配置已加载
if (!state.configLoaded && !state.isLoading) {
await initConfig();
}
try {
// 深拷贝当前配置
const customTheme = JSON.parse(JSON.stringify(state.config.appearance.customTheme));
// 更新对应主题的颜色值
customTheme[themeType][colorKey] = colorValue;
// 更新整个自定义主题配置到后端
await ConfigService.Set(APPEARANCE_CONFIG_KEY_MAP.customTheme, customTheme);
// 更新前端状态
state.config.appearance.customTheme = customTheme;
} catch (error) {
throw error;
}
};
// 设置整个自定义主题配置
const setCustomTheme = async (customTheme: any): Promise<void> => {
// 确保配置已加载
if (!state.configLoaded && !state.isLoading) {
await initConfig();
}
try {
// 更新整个自定义主题配置到后端
await ConfigService.Set(APPEARANCE_CONFIG_KEY_MAP.customTheme, customTheme);
// 更新前端状态
state.config.appearance.customTheme = customTheme;
// 确保Vue能检测到变化
state.config.appearance = { ...state.config.appearance };
} catch (error) {
throw error;
}
};
// 初始化语言设置
@@ -346,21 +556,19 @@ export const useConfigStore = defineStore('config', () => {
const togglers = {
tabIndent: createEditingToggler('enableTabIndent'),
alwaysOnTop: async () => {
await safeCall(async () => {
await updateGeneralConfig('alwaysOnTop', !state.config.general.alwaysOnTop);
// 立即应用窗口置顶状态
await runtime.Window.SetAlwaysOnTop(state.config.general.alwaysOnTop);
}, 'config.alwaysOnTopFailed', 'config.alwaysOnTopSuccess');
await updateGeneralConfig('alwaysOnTop', !state.config.general.alwaysOnTop);
// 立即应用窗口置顶状态
await runtime.Window.SetAlwaysOnTop(state.config.general.alwaysOnTop);
},
tabType: createEnumToggler('tabType', CONFIG_LIMITS.tabType.values)
};
// 字符串配置设置器
const setters = {
fontFamily: (value: string) => safeCall(() => updateEditingConfig('fontFamily', value), 'config.saveFailed', 'config.saveSuccess'),
fontWeight: (value: string) => safeCall(() => updateEditingConfig('fontWeight', value), 'config.saveFailed', 'config.saveSuccess'),
dataPath: (value: string) => safeCall(() => updateGeneralConfig('dataPath', value), 'config.saveFailed', 'config.saveSuccess'),
autoSaveDelay: (value: number) => safeCall(() => updateEditingConfig('autoSaveDelay', value), 'config.saveFailed', 'config.saveSuccess')
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 {
@@ -368,12 +576,13 @@ export const useConfigStore = defineStore('config', () => {
config: computed(() => state.config),
configLoaded: computed(() => state.configLoaded),
isLoading: computed(() => state.isLoading),
localizedFontOptions,
// 限制常量
...limits,
// 核心方法
initConfig: () => safeCall(() => initConfig(), 'config.loadFailed', 'config.loadSuccess'),
initConfig,
resetConfig,
// 语言相关方法
@@ -381,8 +590,9 @@ export const useConfigStore = defineStore('config', () => {
initializeLanguage,
// 主题相关方法
setTheme,
setSystemTheme,
updateCustomTheme,
setCustomTheme,
// 字体大小操作
...adjusters.fontSize,
@@ -393,7 +603,7 @@ export const useConfigStore = defineStore('config', () => {
// Tab操作
toggleTabIndent: togglers.tabIndent,
setEnableTabIndent: (value: boolean) => safeCall(() => updateEditingConfig('enableTabIndent', value), 'config.saveFailed', 'config.saveSuccess'),
setEnableTabIndent: (value: boolean) => updateEditingConfig('enableTabIndent', value),
...adjusters.tabSize,
increaseTabSize: adjusters.tabSize.increase,
decreaseTabSize: adjusters.tabSize.decrease,
@@ -405,7 +615,7 @@ export const useConfigStore = defineStore('config', () => {
// 窗口操作
toggleAlwaysOnTop: togglers.alwaysOnTop,
setAlwaysOnTop: (value: boolean) => safeCall(() => updateGeneralConfig('alwaysOnTop', value), 'config.saveFailed', 'config.saveSuccess'),
setAlwaysOnTop: (value: boolean) => updateGeneralConfig('alwaysOnTop', value),
// 字体操作
setFontFamily: setters.fontFamily,
@@ -418,10 +628,33 @@ export const useConfigStore = defineStore('config', () => {
setAutoSaveDelay: setters.autoSaveDelay,
// 热键配置相关方法
setEnableGlobalHotkey: (value: boolean) => safeCall(() => updateGeneralConfig('enableGlobalHotkey', value), 'config.saveFailed', 'config.saveSuccess'),
setGlobalHotkey: (hotkey: any) => safeCall(() => updateGeneralConfig('globalHotkey', hotkey), 'config.saveFailed', 'config.saveSuccess'),
setEnableGlobalHotkey: (value: boolean) => updateGeneralConfig('enableGlobalHotkey', value),
setGlobalHotkey: (hotkey: any) => updateGeneralConfig('globalHotkey', hotkey),
// 系统托盘配置相关方法
setEnableSystemTray: (value: boolean) => safeCall(() => updateGeneralConfig('enableSystemTray', value), 'config.saveFailed', 'config.saveSuccess')
setEnableSystemTray: (value: boolean) => updateGeneralConfig('enableSystemTray', value),
// 开机启动配置相关方法
setStartAtLogin: async (value: boolean) => {
// 先更新配置文件
await updateGeneralConfig('startAtLogin', value);
// 再调用系统设置API
await StartupService.SetEnabled(value);
},
// 更新配置相关方法
setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value),
// 备份配置相关方法
setEnableBackup: async (value: boolean) => {await updateBackupConfig('enabled', value);},
setAutoBackup: async (value: boolean) => {await updateBackupConfig('auto_backup', value);},
setRepoUrl: async (value: string) => await updateBackupConfig('repo_url', value),
setAuthMethod: async (value: AuthMethod) => await updateBackupConfig('auth_method', value),
setUsername: async (value: string) => await updateBackupConfig('username', value),
setPassword: async (value: string) => await updateBackupConfig('password', value),
setToken: async (value: string) => await updateBackupConfig('token', value),
setSshKeyPath: async (value: string) => await updateBackupConfig('ssh_key_path', value),
setSshKeyPassphrase: async (value: string) => await updateBackupConfig('ssh_key_passphrase', value),
setBackupInterval: async (value: number) => await updateBackupConfig('backup_interval', value),
};
});

View File

@@ -1,124 +1,251 @@
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 {useErrorHandler} from '@/utils/errorHandler';
const SCRATCH_DOCUMENT_ID = 1; // 默认草稿文档ID
export const useDocumentStore = defineStore('document', () => {
const {safeCall} = useErrorHandler();
// === 核心状态 ===
const documents = ref<Record<number, Document>>({});
const recentDocumentIds = ref<number[]>([SCRATCH_DOCUMENT_ID]);
const currentDocumentId = ref<number | null>(null);
const currentDocument = ref<Document | null>(null);
// 状态
const activeDocument = ref<Document | null>(null);
const isLoading = ref(false);
const isSaving = ref(false);
const lastSaved = ref<Date | null>(null);
// === UI状态 ===
const showDocumentSelector = ref(false);
const isLoading = ref(false);
// 计算属性
const documentContent = computed(() => activeDocument.value?.content ?? '');
const documentTitle = computed(() => activeDocument.value?.meta?.title ?? '');
const hasActiveDocument = computed(() => !!activeDocument.value);
const isSaveInProgress = computed(() => isSaving.value);
const lastSavedTime = computed(() => lastSaved.value);
// === 计算属性 ===
const documentList = computed(() =>
Object.values(documents.value).sort((a, b) => {
const aIndex = recentDocumentIds.value.indexOf(a.id);
const bIndex = recentDocumentIds.value.indexOf(b.id);
// 状态管理包装器
const withStateGuard = async <T>(
operation: () => Promise<T>,
stateRef: typeof isLoading | typeof isSaving,
errorMessageKey: string,
successMessageKey?: string
): Promise<T | null> => {
if (stateRef.value) return null;
stateRef.value = true;
try {
return await safeCall(operation, errorMessageKey, successMessageKey);
} finally {
stateRef.value = false;
// 按最近使用排序
if (aIndex !== -1 && bIndex !== -1) {
return aIndex - bIndex;
}
if (aIndex !== -1) return -1;
if (bIndex !== -1) return 1;
// 然后按更新时间排序
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
})
);
// === 私有方法 ===
const addRecentDocument = (docId: number) => {
const recent = recentDocumentIds.value.filter(id => id !== docId);
recent.unshift(docId);
recentDocumentIds.value = recent.slice(0, 100); // 保留最近100个
};
const setDocuments = (docs: Document[]) => {
documents.value = {};
docs.forEach(doc => {
documents.value[doc.id] = doc;
});
};
// === 公共API ===
// 在新窗口中打开文档
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
try {
await OpenDocumentWindow(docId);
return true;
} catch (error) {
console.error('Failed to open document in new window:', error);
return false;
}
};
// 更新文档列表
const updateDocuments = async () => {
try {
const docs = await DocumentService.ListAllDocumentsMeta();
if (docs) {
setDocuments(docs.filter((doc): doc is Document => doc !== null));
}
} catch (error) {
console.error('Failed to update documents:', error);
}
};
// 打开文档
const openDocument = async (docId: number): Promise<boolean> => {
try {
closeDialog();
// 获取完整文档数据
const doc = await DocumentService.GetDocumentByID(docId);
if (!doc) {
throw new Error(`Document ${docId} not found`);
}
currentDocumentId.value = docId;
currentDocument.value = doc;
addRecentDocument(docId);
return true;
} catch (error) {
console.error('Failed to open document:', error);
return false;
}
};
// 创建新文档
const createNewDocument = async (title: string): Promise<Document | null> => {
try {
const newDoc = await DocumentService.CreateDocument(title);
if (!newDoc) {
throw new Error('Failed to create document');
}
// 更新文档列表
documents.value[newDoc.id] = newDoc;
return newDoc;
} catch (error) {
console.error('Failed to create document:', error);
return null;
}
};
// 保存新文档
const saveNewDocument = async (title: string, content: string): Promise<boolean> => {
try {
const newDoc = await createNewDocument(title);
if (!newDoc) return false;
// 更新内容
await DocumentService.UpdateDocumentContent(newDoc.id, content);
newDoc.content = content;
return true;
} catch (error) {
console.error('Failed to save new document:', error);
return false;
}
};
// 更新文档元数据
const updateDocumentMetadata = async (docId: number, title: string, newPath?: string): Promise<boolean> => {
try {
await DocumentService.UpdateDocumentTitle(docId, title);
// 更新本地状态
const doc = documents.value[docId];
if (doc) {
doc.title = title;
doc.updatedAt = new Date();
}
if (currentDocument.value?.id === docId) {
currentDocument.value.title = title;
currentDocument.value.updatedAt = new Date();
}
return true;
} catch (error) {
console.error('Failed to update document metadata:', error);
return false;
}
};
// 删除文档
const deleteDocument = async (docId: number): Promise<boolean> => {
try {
// 检查是否是默认文档使用ID判断
if (docId === SCRATCH_DOCUMENT_ID) {
return false;
}
await DocumentService.DeleteDocument(docId);
// 更新本地状态
delete documents.value[docId];
recentDocumentIds.value = recentDocumentIds.value.filter(id => id !== docId);
// 如果删除的是当前文档,切换到第一个可用文档
if (currentDocumentId.value === docId) {
const availableDocs = Object.values(documents.value);
if (availableDocs.length > 0) {
await openDocument(availableDocs[0].id);
} else {
currentDocumentId.value = null;
currentDocument.value = null;
}
}
return true;
} catch (error) {
console.error('Failed to delete document:', error);
return false;
}
};
// === UI控制 ===
const openDocumentSelector = () => {
closeDialog();
showDocumentSelector.value = true;
};
const closeDialog = () => {
showDocumentSelector.value = false;
};
// === 初始化 ===
const initialize = async (urlDocumentId?: number): Promise<void> => {
try {
await updateDocuments();
// 优先使用URL参数中的文档ID
if (urlDocumentId && documents.value[urlDocumentId]) {
await openDocument(urlDocumentId);
} else if (currentDocumentId.value && documents.value[currentDocumentId.value]) {
// 如果URL中没有指定文档ID则使用持久化的文档ID
await openDocument(currentDocumentId.value);
} else {
// 否则获取第一个文档ID并打开
const firstDocId = await DocumentService.GetFirstDocumentID();
if (firstDocId && documents.value[firstDocId]) {
await openDocument(firstDocId);
}
}
} catch (error) {
console.error('Failed to initialize document store:', error);
}
};
return {
// 状态
documents,
documentList,
recentDocumentIds,
currentDocumentId,
currentDocument,
showDocumentSelector,
isLoading,
// 方法
updateDocuments,
openDocument,
openDocumentInNewWindow,
createNewDocument,
saveNewDocument,
updateDocumentMetadata,
deleteDocument,
openDocumentSelector,
closeDialog,
initialize,
};
}, {
persist: {
key: 'voidraft-document',
storage: localStorage,
pick: ['currentDocumentId']
}
};
// 加载文档
const loadDocument = () => withStateGuard(
async () => {
const doc = await DocumentService.GetActiveDocument();
activeDocument.value = doc;
return doc;
},
isLoading,
'document.loadFailed',
'document.loadSuccess'
);
// 保存文档
const saveDocument = async (content: string): Promise<boolean> => {
const result = await withStateGuard(
async () => {
await DocumentService.UpdateActiveDocumentContent(content);
lastSaved.value = new Date();
// 使用可选链更新本地副本
if (activeDocument.value) {
activeDocument.value.content = content;
activeDocument.value.meta.lastUpdated = lastSaved.value;
}
return true;
},
isSaving,
'document.saveFailed',
'document.saveSuccess'
);
return result ?? false;
};
// 强制保存文档到磁盘
const forceSaveDocument = async (): Promise<boolean> => {
const result = await withStateGuard(
async () => {
// 直接调用强制保存API
await DocumentService.ForceSave();
lastSaved.value = new Date();
// 使用可选链更新时间戳
if (activeDocument.value) {
activeDocument.value.meta.lastUpdated = lastSaved.value;
}
return true;
},
isSaving,
'document.saveFailed',
'document.manualSaveSuccess'
);
return result ?? false;
};
// 初始化
const initialize = async () => {
await loadDocument();
};
return {
// 状态
activeDocument,
isLoading,
isSaving,
lastSaved,
// 计算属性
documentContent,
documentTitle,
hasActiveDocument,
isSaveInProgress,
lastSavedTime,
// 方法
loadDocument,
saveDocument,
forceSaveDocument,
initialize
};
});
});

View File

@@ -1,51 +1,717 @@
import {defineStore} from 'pinia';
import {ref} from 'vue';
import {DocumentStats} from '@/types/editor';
import {nextTick, ref, watch} from 'vue';
import {EditorView} from '@codemirror/view';
import {EditorState, Extension} from '@codemirror/state';
import {useConfigStore} from './configStore';
import {useDocumentStore} from './documentStore';
import {useThemeStore} from './themeStore';
import {ExtensionID, SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
import {DocumentService, ExtensionService} from '@/../bindings/voidraft/internal/services';
import {ensureSyntaxTree} from "@codemirror/language";
import {createBasicSetup} from '@/views/editor/basic/basicSetup';
import {createThemeExtension, updateEditorTheme} from '@/views/editor/basic/themeExtension';
import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtension';
import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension';
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
import {createDynamicExtensions, getExtensionManager, setExtensionManagerView, removeExtensionManagerView} from '@/views/editor/manager';
import {useExtensionStore} from './extensionStore';
import createCodeBlockExtension from "@/views/editor/extensions/codeblock";
const NUM_EDITOR_INSTANCES = 5; // 最多缓存5个编辑器实例
export interface DocumentStats {
lines: number;
characters: number;
selectedCharacters: number;
}
interface EditorInstance {
view: EditorView;
documentId: number;
content: string;
isDirty: boolean;
lastModified: Date;
autoSaveTimer: number | null;
syntaxTreeCache: {
lastDocLength: number;
lastContentHash: string;
lastParsed: Date;
} | null;
}
export const useEditorStore = defineStore('editor', () => {
// 引用配置store
// === 依赖store ===
const configStore = useConfigStore();
const documentStore = useDocumentStore();
const themeStore = useThemeStore();
const extensionStore = useExtensionStore();
// 状态
// === 核心状态 ===
const editorCache = ref<{
lru: number[];
instances: Record<number, EditorInstance>;
containerElement: HTMLElement | null;
}>({
lru: [],
instances: {},
containerElement: null
});
const currentEditor = ref<EditorView | null>(null);
const documentStats = ref<DocumentStats>({
lines: 0,
characters: 0,
selectedCharacters: 0
});
// 编辑器视图
const editorView = ref<EditorView | null>(null);
// 编辑器加载状态
const isLoading = ref(false);
// 方法
function setEditorView(view: EditorView | null) {
editorView.value = view;
}
// 异步操作竞态条件控制
const operationSequence = ref(0);
const pendingOperations = ref(new Map<number, AbortController>());
const currentLoadingDocumentId = ref<number | null>(null);
// 更新文档统计信息
function updateDocumentStats(stats: DocumentStats) {
documentStats.value = stats;
}
// 自动保存设置 - 从配置动态获取
const getAutoSaveDelay = () => configStore.config.editing.autoSaveDelay;
// 应用字体大小
function applyFontSize() {
if (!editorView.value) return;
// 更新编辑器的字体大小
const editorDOM = editorView.value.dom;
if (editorDOM) {
editorDOM.style.fontSize = `${configStore.config.editing.fontSize}px`;
editorView.value?.requestMeasure();
// 生成新的操作序列号
const getNextOperationId = () => ++operationSequence.value;
// 取消之前的操作
const cancelPreviousOperations = (excludeId?: number) => {
pendingOperations.value.forEach((controller, id) => {
if (id !== excludeId) {
controller.abort();
pendingOperations.value.delete(id);
}
});
};
// 检查操作是否仍然有效
const isOperationValid = (operationId: number, documentId: number) => {
return (
pendingOperations.value.has(operationId) &&
!pendingOperations.value.get(operationId)?.signal.aborted &&
currentLoadingDocumentId.value === documentId
);
};
// === 私有方法 ===
// 生成内容哈希
const generateContentHash = (content: string): string => {
let hash = 0;
for (let i = 0; i < content.length; i++) {
const char = content.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
}
return hash.toString();
};
// 缓存化的语法树确保方法
const ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => {
const instance = editorCache.value.instances[documentId];
if (!instance) return;
const docLength = view.state.doc.length;
const content = view.state.doc.toString();
const contentHash = generateContentHash(content);
const now = new Date();
// 检查是否需要重新构建语法树
const cache = instance.syntaxTreeCache;
const shouldRebuild = !cache ||
cache.lastDocLength !== docLength ||
cache.lastContentHash !== contentHash ||
(now.getTime() - cache.lastParsed.getTime()) > 30000; // 30秒过期
if (shouldRebuild) {
try {
ensureSyntaxTree(view.state, docLength, 5000);
// 更新缓存
instance.syntaxTreeCache = {
lastDocLength: docLength,
lastContentHash: contentHash,
lastParsed: now
};
} catch (error) {
console.warn('Failed to ensure syntax tree:', error);
}
}
};
// 创建编辑器实例
const createEditorInstance = async (
content: string,
operationId: number,
documentId: number
): Promise<EditorView> => {
if (!editorCache.value.containerElement) {
throw new Error('Editor container not set');
}
// 检查操作是否仍然有效
if (!isOperationValid(operationId, documentId)) {
throw new Error('Operation cancelled');
}
// 获取基本扩展
const basicExtensions = createBasicSetup();
// 获取主题扩展
const themeExtension = createThemeExtension(
configStore.config.appearance.systemTheme || SystemThemeType.SystemThemeAuto
);
// Tab相关扩展
const tabExtensions = getTabExtensions(
configStore.config.editing.tabSize,
configStore.config.editing.enableTabIndent,
configStore.config.editing.tabType
);
// 字体扩展
const fontExtension = createFontExtensionFromBackend({
fontFamily: configStore.config.editing.fontFamily,
fontSize: configStore.config.editing.fontSize,
lineHeight: configStore.config.editing.lineHeight,
fontWeight: configStore.config.editing.fontWeight
});
// 统计扩展
const statsExtension = createStatsUpdateExtension(updateDocumentStats);
// 内容变化扩展
const contentChangeExtension = createContentChangePlugin();
// 代码块扩展
const codeBlockExtension = createCodeBlockExtension({
showBackground: true,
enableAutoDetection: true
});
// 再次检查操作有效性
if (!isOperationValid(operationId, documentId)) {
throw new Error('Operation cancelled');
}
// 快捷键扩展
const keymapExtension = await createDynamicKeymapExtension();
// 检查操作有效性
if (!isOperationValid(operationId, documentId)) {
throw new Error('Operation cancelled');
}
// 动态扩展传递文档ID以便扩展管理器可以预初始化
const dynamicExtensions = await createDynamicExtensions(documentId);
// 最终检查操作有效性
if (!isOperationValid(operationId, documentId)) {
throw new Error('Operation cancelled');
}
// 组合所有扩展
const extensions: Extension[] = [
keymapExtension,
...basicExtensions,
themeExtension,
...tabExtensions,
fontExtension,
statsExtension,
contentChangeExtension,
codeBlockExtension,
...dynamicExtensions
];
// 创建编辑器状态
const state = EditorState.create({
doc: content,
extensions
});
// 创建编辑器视图
const view = new EditorView({
state
});
// 将光标定位到文档末尾并滚动到该位置
const docLength = view.state.doc.length;
view.dispatch({
selection: {anchor: docLength, head: docLength},
scrollIntoView: true
});
return view;
};
// 添加编辑器到缓存
const addEditorToCache = (documentId: number, view: EditorView, content: string) => {
// 如果缓存已满,移除最少使用的编辑器
if (editorCache.value.lru.length >= NUM_EDITOR_INSTANCES) {
const oldestId = editorCache.value.lru.shift();
if (oldestId && editorCache.value.instances[oldestId]) {
const oldInstance = editorCache.value.instances[oldestId];
// 清除自动保存定时器
if (oldInstance.autoSaveTimer) {
clearTimeout(oldInstance.autoSaveTimer);
}
// 移除DOM元素
if (oldInstance.view.dom.parentElement) {
oldInstance.view.dom.remove();
}
oldInstance.view.destroy();
delete editorCache.value.instances[oldestId];
}
}
// 添加新的编辑器实例
editorCache.value.instances[documentId] = {
view,
documentId,
content,
isDirty: false,
lastModified: new Date(),
autoSaveTimer: null,
syntaxTreeCache: null
};
// 添加到LRU列表
editorCache.value.lru.push(documentId);
// 初始化语法树缓存
ensureSyntaxTreeCached(view, documentId);
};
// 更新LRU
const updateLRU = (documentId: number) => {
const lru = editorCache.value.lru;
const index = lru.indexOf(documentId);
if (index > -1) {
lru.splice(index, 1);
}
lru.push(documentId);
};
// 获取或创建编辑器
const getOrCreateEditor = async (
documentId: number,
content: string,
operationId: number
): Promise<EditorView> => {
// 检查缓存
const cached = editorCache.value.instances[documentId];
if (cached) {
updateLRU(documentId);
return cached.view;
}
// 检查操作是否仍然有效
if (!isOperationValid(operationId, documentId)) {
throw new Error('Operation cancelled');
}
// 创建新的编辑器实例
const view = await createEditorInstance(content, operationId, documentId);
// 最终检查操作有效性
if (!isOperationValid(operationId, documentId)) {
// 如果操作已取消,清理创建的实例
view.destroy();
throw new Error('Operation cancelled');
}
addEditorToCache(documentId, view, content);
return view;
};
// 显示编辑器
const showEditor = (documentId: number) => {
const instance = editorCache.value.instances[documentId];
if (!instance || !editorCache.value.containerElement) return;
try {
// 移除当前编辑器DOM
if (currentEditor.value && currentEditor.value.dom && currentEditor.value.dom.parentElement) {
currentEditor.value.dom.remove();
}
// 确保容器为空
editorCache.value.containerElement.innerHTML = '';
// 将目标编辑器DOM添加到容器
editorCache.value.containerElement.appendChild(instance.view.dom);
currentEditor.value = instance.view;
// 设置扩展管理器视图
setExtensionManagerView(instance.view, documentId);
// 更新LRU
updateLRU(documentId);
// 重新测量和聚焦编辑器
nextTick(() => {
// 将光标定位到文档末尾并滚动到该位置
const docLength = instance.view.state.doc.length;
instance.view.dispatch({
selection: {anchor: docLength, head: docLength},
scrollIntoView: true
});
// 滚动到文档底部(将光标位置滚动到可见区域)
instance.view.focus();
// 使用缓存的语法树确保方法
ensureSyntaxTreeCached(instance.view, documentId);
});
} catch (error) {
console.error('Error showing editor:', error);
}
};
// 保存编辑器内容
const saveEditorContent = async (documentId: number): Promise<boolean> => {
const instance = editorCache.value.instances[documentId];
if (!instance || !instance.isDirty) return true;
try {
const content = instance.view.state.doc.toString();
const lastModified = instance.lastModified;
await DocumentService.UpdateDocumentContent(documentId, content);
// 检查在保存期间内容是否又被修改了
if (instance.lastModified === lastModified) {
instance.content = content;
instance.isDirty = false;
instance.lastModified = new Date();
}
// 如果内容在保存期间被修改了,保持 isDirty 状态
return true;
} catch (error) {
console.error('Failed to save editor content:', error);
return false;
}
};
// 内容变化处理
const onContentChange = (documentId: number) => {
const instance = editorCache.value.instances[documentId];
if (!instance) return;
instance.isDirty = true;
instance.lastModified = new Date();
// 清理语法树缓存,下次访问时重新构建
instance.syntaxTreeCache = null;
// 清除之前的定时器
if (instance.autoSaveTimer) {
clearTimeout(instance.autoSaveTimer);
}
// 设置新的自动保存定时器
instance.autoSaveTimer = window.setTimeout(() => {
saveEditorContent(documentId);
}, getAutoSaveDelay());
};
// === 公共API ===
// 设置编辑器容器
const setEditorContainer = (container: HTMLElement | null) => {
editorCache.value.containerElement = container;
// 如果设置容器时已有当前文档,立即加载编辑器
if (container && documentStore.currentDocument) {
loadEditor(documentStore.currentDocument.id, documentStore.currentDocument.content);
}
};
// 加载编辑器
const loadEditor = async (documentId: number, content: string) => {
// 设置加载状态
isLoading.value = true;
// 生成新的操作ID
const operationId = getNextOperationId();
const abortController = new AbortController();
try {
// 验证参数
if (!documentId) {
throw new Error('Invalid parameters for loadEditor');
}
// 取消之前的操作并设置当前操作
cancelPreviousOperations();
currentLoadingDocumentId.value = documentId;
pendingOperations.value.set(operationId, abortController);
// 保存当前编辑器内容
if (currentEditor.value) {
const currentDocId = documentStore.currentDocumentId;
if (currentDocId && currentDocId !== documentId) {
await saveEditorContent(currentDocId);
// 检查操作是否仍然有效
if (!isOperationValid(operationId, documentId)) {
return;
}
}
}
// 获取或创建编辑器
const view = await getOrCreateEditor(documentId, content, operationId);
// 检查操作是否仍然有效
if (!isOperationValid(operationId, documentId)) {
return;
}
// 更新内容(如果需要)
const instance = editorCache.value.instances[documentId];
if (instance && instance.content !== content) {
// 确保编辑器视图有效
if (view && view.state && view.dispatch) {
view.dispatch({
changes: {
from: 0,
to: view.state.doc.length,
insert: content
}
});
instance.content = content;
instance.isDirty = false;
// 清理语法树缓存,因为内容已更新
instance.syntaxTreeCache = null;
}
}
// 最终检查操作有效性
if (!isOperationValid(operationId, documentId)) {
return;
}
// 显示编辑器
showEditor(documentId);
} catch (error) {
if (error instanceof Error && error.message === 'Operation cancelled') {
console.log(`Editor loading cancelled for document ${documentId}`);
} else {
console.error('Failed to load editor:', error);
}
} finally {
// 清理操作记录
pendingOperations.value.delete(operationId);
if (currentLoadingDocumentId.value === documentId) {
currentLoadingDocumentId.value = null;
}
// 延迟一段时间后再取消加载状态
setTimeout(() => {
isLoading.value = false;
}, 800);
}
};
// 移除编辑器
const removeEditor = (documentId: number) => {
const instance = editorCache.value.instances[documentId];
if (instance) {
try {
// 如果正在加载这个文档,取消操作
if (currentLoadingDocumentId.value === documentId) {
cancelPreviousOperations();
currentLoadingDocumentId.value = null;
}
// 清除自动保存定时器
if (instance.autoSaveTimer) {
clearTimeout(instance.autoSaveTimer);
instance.autoSaveTimer = null;
}
// 从扩展管理器中移除视图
removeExtensionManagerView(documentId);
// 移除DOM元素
if (instance.view && instance.view.dom && instance.view.dom.parentElement) {
instance.view.dom.remove();
}
// 销毁编辑器
if (instance.view && instance.view.destroy) {
instance.view.destroy();
}
// 清理引用
if (currentEditor.value === instance.view) {
currentEditor.value = null;
}
delete editorCache.value.instances[documentId];
const lruIndex = editorCache.value.lru.indexOf(documentId);
if (lruIndex > -1) {
editorCache.value.lru.splice(lruIndex, 1);
}
} catch (error) {
console.error('Error removing editor:', error);
}
}
};
// 更新文档统计
const updateDocumentStats = (stats: DocumentStats) => {
documentStats.value = stats;
};
// 应用字体设置
const applyFontSettings = () => {
Object.values(editorCache.value.instances).forEach(instance => {
updateFontConfig(instance.view, {
fontFamily: configStore.config.editing.fontFamily,
fontSize: configStore.config.editing.fontSize,
lineHeight: configStore.config.editing.lineHeight,
fontWeight: configStore.config.editing.fontWeight
});
});
};
// 应用主题设置
const applyThemeSettings = () => {
Object.values(editorCache.value.instances).forEach(instance => {
updateEditorTheme(instance.view,
themeStore.currentTheme || SystemThemeType.SystemThemeAuto
);
});
};
// 应用Tab设置
const applyTabSettings = () => {
Object.values(editorCache.value.instances).forEach(instance => {
updateTabConfig(
instance.view,
configStore.config.editing.tabSize,
configStore.config.editing.enableTabIndent,
configStore.config.editing.tabType
);
});
};
// 应用快捷键设置
const applyKeymapSettings = async () => {
// 确保所有编辑器实例的快捷键都更新
await Promise.all(
Object.values(editorCache.value.instances).map(instance =>
updateKeymapExtension(instance.view)
)
);
};
// 清空所有编辑器
const clearAllEditors = () => {
// 取消所有挂起的操作
cancelPreviousOperations();
currentLoadingDocumentId.value = null;
Object.values(editorCache.value.instances).forEach(instance => {
// 清除自动保存定时器
if (instance.autoSaveTimer) {
clearTimeout(instance.autoSaveTimer);
}
// 从扩展管理器移除
removeExtensionManagerView(instance.documentId);
// 移除DOM元素
if (instance.view.dom.parentElement) {
instance.view.dom.remove();
}
// 销毁编辑器
instance.view.destroy();
});
editorCache.value.instances = {};
editorCache.value.lru = [];
currentEditor.value = null;
};
// 更新扩展
const updateExtension = async (id: ExtensionID, enabled: boolean, config?: any) => {
// 如果只是更新启用状态
if (config === undefined) {
await ExtensionService.UpdateExtensionEnabled(id, enabled);
} else {
// 如果需要更新配置
await ExtensionService.UpdateExtensionState(id, enabled, config);
}
// 更新前端编辑器扩展 - 应用于所有实例
const manager = getExtensionManager();
if (manager) {
// 使用立即更新模式,跳过防抖
manager.updateExtensionImmediate(id, enabled, config || {});
}
// 重新加载扩展配置
await extensionStore.loadExtensions();
// 不再需要单独更新当前编辑器的快捷键映射,因为扩展管理器会更新所有实例
// 但我们仍需要确保快捷键配置在所有编辑器上更新
await applyKeymapSettings();
};
// 监听文档切换
watch(() => documentStore.currentDocument, (newDoc) => {
if (newDoc && editorCache.value.containerElement) {
// 使用 nextTick 确保DOM更新完成后再加载编辑器
nextTick(() => {
loadEditor(newDoc.id, newDoc.content);
});
}
});
// 监听配置变化
watch(() => configStore.config.editing.fontSize, applyFontSettings);
watch(() => configStore.config.editing.fontFamily, applyFontSettings);
watch(() => configStore.config.editing.lineHeight, applyFontSettings);
watch(() => configStore.config.editing.fontWeight, applyFontSettings);
watch(() => configStore.config.editing.tabSize, applyTabSettings);
watch(() => configStore.config.editing.enableTabIndent, applyTabSettings);
watch(() => configStore.config.editing.tabType, applyTabSettings);
watch(() => themeStore.currentTheme, applyThemeSettings);
return {
// 状态
currentEditor,
documentStats,
editorView,
isLoading,
// 方法
setEditorView,
updateDocumentStats,
applyFontSize
setEditorContainer,
loadEditor,
removeEditor,
clearAllEditors,
onContentChange,
// 配置更新方法
applyFontSettings,
applyThemeSettings,
applyTabSettings,
applyKeymapSettings,
// 扩展管理方法
updateExtension,
editorView: currentEditor,
};
});

View File

@@ -0,0 +1,49 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { Extension, ExtensionID } from '@/../bindings/voidraft/internal/models/models'
import { ExtensionService } from '@/../bindings/voidraft/internal/services'
export const useExtensionStore = defineStore('extension', () => {
// 扩展配置数据
const extensions = ref<Extension[]>([])
// 获取启用的扩展
const enabledExtensions = computed(() =>
extensions.value.filter(ext => ext.enabled)
)
// 获取启用的扩展ID列表
const enabledExtensionIds = computed(() =>
enabledExtensions.value.map(ext => ext.id)
)
/**
* 从后端加载扩展配置
*/
const loadExtensions = async (): Promise<void> => {
try {
extensions.value = await ExtensionService.GetAllExtensions()
} catch (err) {
console.error('[ExtensionStore] Failed to load extensions:', err)
}
}
/**
* 获取扩展配置
*/
const getExtensionConfig = (id: ExtensionID): any => {
const extension = extensions.value.find(ext => ext.id === id)
return extension?.config ?? {}
}
return {
// 状态
extensions,
enabledExtensions,
enabledExtensionIds,
// 方法
loadExtensions,
getExtensionConfig,
}
})

View File

@@ -0,0 +1,86 @@
import {defineStore} from 'pinia'
import {computed, ref} from 'vue'
import {ExtensionID, KeyBinding, KeyBindingCommand} from '@/../bindings/voidraft/internal/models/models'
import {GetAllKeyBindings} from '@/../bindings/voidraft/internal/services/keybindingservice'
export const useKeybindingStore = defineStore('keybinding', () => {
// 快捷键配置数据
const keyBindings = ref<KeyBinding[]>([])
// 获取启用的快捷键
const enabledKeyBindings = computed(() =>
keyBindings.value.filter(kb => kb.enabled)
)
// 按扩展分组的快捷键
const keyBindingsByExtension = computed(() => {
const groups = new Map<ExtensionID, KeyBinding[]>()
for (const binding of keyBindings.value) {
if (!groups.has(binding.extension)) {
groups.set(binding.extension, [])
}
groups.get(binding.extension)!.push(binding)
}
return groups
})
// 获取指定扩展的快捷键
const getKeyBindingsByExtension = computed(() =>
(extension: ExtensionID) =>
keyBindings.value.filter(kb => kb.extension === extension)
)
// 按命令获取快捷键
const getKeyBindingByCommand = computed(() =>
(command: KeyBindingCommand) =>
keyBindings.value.find(kb => kb.command === command)
)
/**
* 从后端加载快捷键配置
*/
const loadKeyBindings = async (): Promise<void> => {
try {
keyBindings.value = await GetAllKeyBindings()
} catch (err) {
throw err
}
}
/**
* 检查是否存在指定命令的快捷键
*/
const hasCommand = (command: KeyBindingCommand): boolean => {
return keyBindings.value.some(kb => kb.command === command && kb.enabled)
}
/**
* 获取扩展相关的所有扩展ID
*/
const getAllExtensionIds = computed(() => {
const extensionIds = new Set<ExtensionID>()
for (const binding of keyBindings.value) {
extensionIds.add(binding.extension)
}
return Array.from(extensionIds)
})
return {
// 状态
keyBindings,
enabledKeyBindings,
keyBindingsByExtension,
getAllExtensionIds,
// 计算属性
getKeyBindingByCommand,
getKeyBindingsByExtension,
// 方法
loadKeyBindings,
hasCommand,
}
})

View File

@@ -1,166 +0,0 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
// 日志级别定义
export enum LogLevel {
INFO = 'info',
WARNING = 'warning',
ERROR = 'error'
}
// 日志项结构
export interface LogItem {
id: number;
level: LogLevel;
message: string;
timestamp: Date;
shown: boolean; // 是否已显示过
}
export const useLogStore = defineStore('log', () => {
// 日志列表
const logs = ref<LogItem[]>([]);
// 显示队列 - 存储待显示的日志
const displayQueue = ref<LogItem[]>([]);
// 当前显示的日志
const currentLog = ref<LogItem | null>(null);
// 最近一条日志,用于在工具栏显示
const latestLog = ref<LogItem | null>(null);
// 是否显示日志
const showLog = ref(false);
// 自动隐藏计时器
let hideTimer: number | null = null;
// 根据日志级别获取显示时间
function getDisplayTimeByLevel(level: LogLevel): number {
switch(level) {
case LogLevel.ERROR:
return 8000; // 错误显示更长时间
case LogLevel.WARNING:
return 6000; // 警告显示中等时间
case LogLevel.INFO:
default:
return 4000; // 信息显示较短时间
}
}
// 显示下一条日志
function showNextLog() {
if (hideTimer) {
window.clearTimeout(hideTimer);
hideTimer = null;
}
// 如果队列为空,则不显示
if (displayQueue.value.length === 0) {
showLog.value = false;
currentLog.value = null;
return;
}
// 取出队列中第一条日志显示
const nextLog = displayQueue.value.shift()!;
currentLog.value = nextLog;
showLog.value = true;
// 设置自动隐藏和切换到下一条
const displayTime = getDisplayTimeByLevel(nextLog.level);
hideTimer = window.setTimeout(() => {
showNextLog();
}, displayTime);
}
// 添加日志
function addLog(level: LogLevel, message: string, autoHideDelay = 0) {
const id = Date.now();
const logItem: LogItem = {
id,
level,
message,
timestamp: new Date(),
shown: false
};
// 添加到日志列表
logs.value.push(logItem);
// 保持日志列表在合理大小
if (logs.value.length > 100) {
logs.value = logs.value.slice(-100);
}
// 设置最新日志
latestLog.value = logItem;
// 添加到显示队列
displayQueue.value.push(logItem);
// 如果当前没有显示日志,则开始显示
if (!currentLog.value && !hideTimer) {
showNextLog();
}
return id;
}
// 添加不同级别的日志的便捷方法
function info(message: string) {
return addLog(LogLevel.INFO, message);
}
function warning(message: string) {
return addLog(LogLevel.WARNING, message);
}
function error(message: string) {
return addLog(LogLevel.ERROR, message);
}
// 清除日志
function clearLogs() {
logs.value = [];
displayQueue.value = [];
latestLog.value = null;
currentLog.value = null;
showLog.value = false;
if (hideTimer) {
window.clearTimeout(hideTimer);
hideTimer = null;
}
}
// 手动隐藏当前日志
function hideCurrentLog() {
if (hideTimer) {
window.clearTimeout(hideTimer);
hideTimer = null;
}
showLog.value = false;
currentLog.value = null;
displayQueue.value = []; // 清空队列
}
return {
// 状态
logs,
latestLog,
currentLog,
showLog,
displayQueue,
// 方法
addLog,
info,
warning,
error,
clearLogs,
hideCurrentLog,
showNextLog
};
});

View File

@@ -0,0 +1,66 @@
import {defineStore} from 'pinia';
import {computed, ref} from 'vue';
import * as runtime from '@wailsio/runtime';
export interface SystemEnvironment {
OS: string;
Arch: string;
Debug: boolean;
OSInfo: {
Name: string;
Branding: string;
Version: string;
ID: string;
};
PlatformInfo?: Record<string, string>;
}
export const useSystemStore = defineStore('system', () => {
// 状态
const environment = ref<SystemEnvironment | null>(null);
const isLoading = ref(false);
// 计算属性
const isWindows = computed(() => environment.value?.OS === 'windows');
const isMacOS = computed(() => environment.value?.OS === 'darwin');
const isLinux = computed(() => environment.value?.OS === 'linux');
// 获取标题栏高度
const titleBarHeight = computed(() => {
if (isWindows.value) return '32px';
if (isMacOS.value) return '28px';
return '34px'; // Linux 默认
});
// 初始化系统信息
const initializeSystemInfo = async (): Promise<void> => {
if (isLoading.value) return;
isLoading.value = true;
try {
environment.value = await runtime.System.Environment();
} catch (err) {
environment.value = null;
} finally {
isLoading.value = false;
}
};
return {
// 状态
environment,
isLoading,
// 计算属性
isWindows,
isMacOS,
isLinux,
titleBarHeight,
// 方法
initializeSystemInfo,
};
});

View File

@@ -0,0 +1,145 @@
import {defineStore} from 'pinia';
import {computed, reactive} from 'vue';
import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
import {useConfigStore} from './configStore';
import {useEditorStore} from './editorStore';
import {defaultDarkColors} from '@/views/editor/theme/dark';
import {defaultLightColors} from '@/views/editor/theme/light';
/**
* 主题管理 Store
* 职责:管理主题状态和颜色配置
*/
export const useThemeStore = defineStore('theme', () => {
const configStore = useConfigStore();
// 响应式状态 - 存储当前使用的主题颜色
const themeColors = reactive({
darkTheme: { ...defaultDarkColors },
lightTheme: { ...defaultLightColors }
});
// 计算属性 - 当前选择的主题类型
const currentTheme = computed(() =>
configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
);
// 初始化主题颜色 - 从配置加载
const initializeThemeColors = () => {
const customTheme = configStore.config?.appearance?.customTheme;
if (customTheme) {
if (customTheme.darkTheme) {
Object.assign(themeColors.darkTheme, customTheme.darkTheme);
}
if (customTheme.lightTheme) {
Object.assign(themeColors.lightTheme, customTheme.lightTheme);
}
}
};
// 应用主题到 DOM
const applyThemeToDOM = (theme: SystemThemeType) => {
document.documentElement.setAttribute('data-theme',
theme === SystemThemeType.SystemThemeAuto ? 'auto' :
theme === SystemThemeType.SystemThemeDark ? 'dark' : 'light'
);
};
// 初始化主题
const initializeTheme = () => {
const theme = configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto;
applyThemeToDOM(theme);
initializeThemeColors();
};
// 设置主题
const setTheme = async (theme: SystemThemeType) => {
await configStore.setSystemTheme(theme);
applyThemeToDOM(theme);
refreshEditorTheme();
};
// 更新主题颜色
const updateThemeColors = (darkColors: any = null, lightColors: any = null): boolean => {
let hasChanges = false;
if (darkColors) {
Object.entries(darkColors).forEach(([key, value]) => {
if (value !== undefined && themeColors.darkTheme[key] !== value) {
themeColors.darkTheme[key] = value;
hasChanges = true;
}
});
}
if (lightColors) {
Object.entries(lightColors).forEach(([key, value]) => {
if (value !== undefined && themeColors.lightTheme[key] !== value) {
themeColors.lightTheme[key] = value;
hasChanges = true;
}
});
}
return hasChanges;
};
// 保存主题颜色到配置
const saveThemeColors = async () => {
const customTheme = {
darkTheme: { ...themeColors.darkTheme },
lightTheme: { ...themeColors.lightTheme }
};
await configStore.setCustomTheme(customTheme);
};
// 重置主题颜色
const resetThemeColors = async (themeType: 'darkTheme' | 'lightTheme') => {
try {
// 1. 更新内存中的颜色状态
if (themeType === 'darkTheme') {
Object.assign(themeColors.darkTheme, defaultDarkColors);
}
if (themeType === 'lightTheme') {
Object.assign(themeColors.lightTheme, defaultLightColors);
}
// 2. 保存到配置
await saveThemeColors();
// 3. 刷新编辑器主题
refreshEditorTheme();
return true;
} catch (error) {
console.error('Failed to reset theme colors:', error);
return false;
}
};
// 刷新编辑器主题(在主题颜色更改后调用)
const refreshEditorTheme = () => {
// 使用当前主题重新应用DOM主题
const theme = currentTheme.value;
applyThemeToDOM(theme);
const editorStore = useEditorStore();
if (editorStore) {
editorStore.applyThemeSettings();
}
};
return {
currentTheme,
themeColors,
setTheme,
initializeTheme,
applyThemeToDOM,
updateThemeColors,
saveThemeColors,
resetThemeColors,
refreshEditorTheme
};
});

View File

@@ -0,0 +1,594 @@
import {defineStore} from 'pinia';
import {computed, ref, watch} from 'vue';
import {TranslationService} from '@/../bindings/voidraft/internal/services';
import {franc} from 'franc-min';
export interface TranslationResult {
sourceText: string;
translatedText: string;
sourceLang: string;
targetLang: string;
translatorType: string;
error?: string;
}
/**
* ISO 639-3 到 ISO 639-1/2 语言代码的映射
* franc-min 返回的是 ISO 639-3 代码需要转换为翻译API常用的 ISO 639-1/2 代码
*/
const ISO_LANGUAGE_MAP: Record<string, string> = {
// 常见语言
'cmn': 'zh', // 中文 (Mandarin Chinese)
'eng': 'en', // 英文 (English)
'jpn': 'ja', // 日语 (Japanese)
'kor': 'ko', // 韩语 (Korean)
'fra': 'fr', // 法语 (French)
'deu': 'de', // 德语 (German)
'spa': 'es', // 西班牙语 (Spanish)
'rus': 'ru', // 俄语 (Russian)
'ita': 'it', // 意大利语 (Italian)
'nld': 'nl', // 荷兰语 (Dutch)
'por': 'pt', // 葡萄牙语 (Portuguese)
'vie': 'vi', // 越南语 (Vietnamese)
'arb': 'ar', // 阿拉伯语 (Arabic)
'hin': 'hi', // 印地语 (Hindi)
'ben': 'bn', // 孟加拉语 (Bengali)
'tha': 'th', // 泰语 (Thai)
'tur': 'tr', // 土耳其语 (Turkish)
'heb': 'he', // 希伯来语 (Hebrew)
'pol': 'pl', // 波兰语 (Polish)
'swe': 'sv', // 瑞典语 (Swedish)
'fin': 'fi', // 芬兰语 (Finnish)
'dan': 'da', // 丹麦语 (Danish)
'ron': 'ro', // 罗马尼亚语 (Romanian)
'hun': 'hu', // 匈牙利语 (Hungarian)
'ces': 'cs', // 捷克语 (Czech)
'ell': 'el', // 希腊语 (Greek)
'bul': 'bg', // 保加利亚语 (Bulgarian)
'cat': 'ca', // 加泰罗尼亚语 (Catalan)
'ukr': 'uk', // 乌克兰语 (Ukrainian)
'hrv': 'hr', // 克罗地亚语 (Croatian)
'ind': 'id', // 印尼语 (Indonesian)
'mal': 'ms', // 马来语 (Malay)
'nob': 'no', // 挪威语 (Norwegian)
'lat': 'la', // 拉丁语 (Latin)
'lit': 'lt', // 立陶宛语 (Lithuanian)
'slk': 'sk', // 斯洛伐克语 (Slovak)
'slv': 'sl', // 斯洛文尼亚语 (Slovenian)
'srp': 'sr', // 塞尔维亚语 (Serbian)
'est': 'et', // 爱沙尼亚语 (Estonian)
'lav': 'lv', // 拉脱维亚语 (Latvian)
'fil': 'tl', // 菲律宾语/他加禄语 (Filipino/Tagalog)
// 未知/不确定
'und': 'auto' // 未知语言
};
// 语言代码的通用映射关系,适用于大部分翻译器
const COMMON_LANGUAGE_ALIASES: Record<string, string[]> = {
'zh': ['zh-CN', 'zh-TW', 'zh-Hans', 'zh-Hant', 'chinese', 'zhong'],
'en': ['en-US', 'en-GB', 'english', 'eng'],
'ja': ['jp', 'jpn', 'japanese'],
'ko': ['kr', 'kor', 'korean'],
'fr': ['fra', 'french'],
'de': ['deu', 'german', 'ger'],
'es': ['spa', 'spanish', 'esp'],
'ru': ['rus', 'russian'],
'pt': ['por', 'portuguese'],
'it': ['ita', 'italian'],
'nl': ['nld', 'dutch'],
'ar': ['ara', 'arabic'],
'hi': ['hin', 'hindi'],
'th': ['tha', 'thai'],
'tr': ['tur', 'turkish'],
'vi': ['vie', 'vietnamese'],
'id': ['ind', 'indonesian'],
'ms': ['mal', 'malay'],
'fi': ['fin', 'finnish'],
};
/**
* 翻译存储
*/
export const useTranslationStore = defineStore('translation', () => {
// 状态
const availableTranslators = ref<string[]>([]);
const isTranslating = ref(false);
const lastResult = ref<TranslationResult | null>(null);
const error = ref<string | null>(null);
// 语言列表 - 将类型设置为any以避免类型错误
const languageMaps = ref<Record<string, Record<string, any>>>({});
// 语言使用频率计数 - 使用pinia持久化
const languageUsageCount = ref<Record<string, number>>({});
// 最近使用的翻译语言 - 最多记录10个
const recentLanguages = ref<string[]>([]);
// 默认配置
// 注意:确保默认值在初始化和持久化后正确设置
const defaultTargetLang = ref('zh');
const defaultTranslator = ref('bing');
// 检测到的源语言,初始为空字符串表示尚未检测
const detectedSourceLang = ref('');
// 计算属性
const hasTranslators = computed(() => availableTranslators.value.length > 0);
const currentLanguageMap = computed(() => {
return languageMaps.value[defaultTranslator.value] || {};
});
// 监听默认语言变更,确保目标语言在当前翻译器支持的范围内
watch([defaultTranslator], () => {
// 当切换翻译器时,验证默认目标语言是否支持
if (Object.keys(languageMaps.value).length > 0) {
const validatedLang = validateLanguage(defaultTargetLang.value, defaultTranslator.value);
if (validatedLang !== defaultTargetLang.value) {
console.log(`目标语言 ${defaultTargetLang.value} 不受支持,已切换到 ${validatedLang}`);
defaultTargetLang.value = validatedLang;
}
}
});
/**
* 加载可用翻译器
*/
const loadAvailableTranslators = async (): Promise<void> => {
try {
const translators = await TranslationService.GetAvailableTranslators();
availableTranslators.value = translators;
// 如果默认翻译器不在可用列表中,则使用第一个可用的翻译器
if (translators.length > 0 && !translators.includes(defaultTranslator.value)) {
defaultTranslator.value = translators[0];
}
// 加载所有翻译器的语言列表
await Promise.all(translators.map(loadTranslatorLanguages));
// 在加载完所有语言列表后,确保默认目标语言有效
if (defaultTargetLang.value) {
const validatedLang = validateLanguage(defaultTargetLang.value, defaultTranslator.value);
if (validatedLang !== defaultTargetLang.value) {
console.log(`目标语言 ${defaultTargetLang.value} 不受支持,已切换到 ${validatedLang}`);
defaultTargetLang.value = validatedLang;
}
}
} catch (err) {
error.value = 'no available translators';
}
};
/**
* 加载指定翻译器的语言列表
* @param translatorType 翻译器类型
*/
const loadTranslatorLanguages = async (translatorType: string): Promise<void> => {
try {
const languages = await TranslationService.GetTranslatorLanguages(translatorType as any);
if (languages) {
languageMaps.value[translatorType] = languages;
}
} catch (err) {
console.error(`Failed to load languages for ${translatorType}:`, err);
}
};
/**
* 检测文本语言
* @param text 待检测的文本
* @returns 检测到的语言代码,如未检测到则返回空字符串
*/
const detectLanguage = (text: string): string => {
if (!text || text.trim().length < 10) {
return '';
}
try {
// franc返回ISO 639-3代码
const detectedIso639_3 = franc(text);
// 如果是未知语言,返回空字符串
if (detectedIso639_3 === 'und') {
return '';
}
// 转换为常用语言代码
return ISO_LANGUAGE_MAP[detectedIso639_3] || '';
} catch (err) {
console.error('语言检测失败:', err);
return '';
}
};
/**
* 在翻译器语言列表中查找相似的语言代码
* @param langCode 待查找的语言代码
* @param translatorType 翻译器类型
* @returns 找到的语言代码或空字符串
*/
const findSimilarLanguage = (langCode: string, translatorType: string): string => {
if (!langCode) return '';
const languageMap = languageMaps.value[translatorType] || {};
const langCodeLower = langCode.toLowerCase();
// 1. 尝试精确匹配
if (languageMap[langCode]) {
return langCode;
}
// 2. 检查通用别名映射
const possibleAliases = Object.entries(COMMON_LANGUAGE_ALIASES).find(
([code, aliases]) => code === langCodeLower || aliases.includes(langCodeLower)
);
if (possibleAliases) {
// 检查主代码是否可用
const [mainCode, aliases] = possibleAliases;
if (languageMap[mainCode]) {
return mainCode;
}
// 检查别名是否可用
for (const alias of aliases) {
if (languageMap[alias]) {
return alias;
}
}
}
// 3. 尝试正则表达式匹配
// 创建一个基于语言代码的正则表达式:例如 'en' 会匹配 'en-US', 'en_GB' 等
const codePattern = new RegExp(`^${langCodeLower}[-_]?`, 'i');
// 在语言列表中查找匹配的语言代码
const availableCodes = Object.keys(languageMap);
const matchedCode = availableCodes.find(code =>
codePattern.test(code.toLowerCase())
);
if (matchedCode) {
return matchedCode;
}
// 4. 反向匹配,例如 'zh-CN' 应该能匹配到 'zh'
if (langCodeLower.includes('-') || langCodeLower.includes('_')) {
const baseCode = langCodeLower.split(/[-_]/)[0];
if (languageMap[baseCode]) {
return baseCode;
}
// 通过基础代码查找别名
const baseCodeAliases = Object.entries(COMMON_LANGUAGE_ALIASES).find(
([code, aliases]) => code === baseCode || aliases.includes(baseCode)
);
if (baseCodeAliases) {
const [mainCode, aliases] = baseCodeAliases;
if (languageMap[mainCode]) {
return mainCode;
}
for (const alias of aliases) {
if (languageMap[alias]) {
return alias;
}
}
}
}
// 5. 最后尝试查找与部分代码匹配的任何语言
const partialMatch = availableCodes.find(code =>
code.toLowerCase().includes(langCodeLower) ||
langCodeLower.includes(code.toLowerCase())
);
if (partialMatch) {
return partialMatch;
}
// 如果所有匹配都失败,返回英语作为默认值
return 'en';
};
/**
* 验证语言代码是否受当前翻译器支持
* @param langCode 语言代码
* @param translatorType 翻译器类型(可选,默认使用当前翻译器)
* @returns 验证后的语言代码
*/
const validateLanguage = (langCode: string, translatorType?: string): string => {
// 如果语言代码为空返回auto作为API调用的默认值
if (!langCode) return 'auto';
const currentType = translatorType || defaultTranslator.value;
// 尝试在指定翻译器的语言列表中查找相似的语言代码
return findSimilarLanguage(langCode, currentType) || 'auto';
};
/**
* 增加语言使用次数并添加到最近使用列表
* @param langCode 语言代码
* @param weight 权重默认为1
*/
const incrementLanguageUsage = (langCode: string, weight: number = 1): void => {
if (!langCode || langCode === 'auto') return;
// 转换为小写,确保一致性
const normalizedCode = langCode.toLowerCase();
// 更新使用次数,乘以权重
const currentCount = languageUsageCount.value[normalizedCode] || 0;
languageUsageCount.value[normalizedCode] = currentCount + weight;
// 更新最近使用的语言列表
updateRecentLanguages(normalizedCode);
};
/**
* 更新最近使用的语言列表
* @param langCode 语言代码
*/
const updateRecentLanguages = (langCode: string): void => {
if (!langCode) return;
// 如果已经在列表中,先移除它
const index = recentLanguages.value.indexOf(langCode);
if (index !== -1) {
recentLanguages.value.splice(index, 1);
}
// 添加到列表开头
recentLanguages.value.unshift(langCode);
// 保持列表最多10个元素
if (recentLanguages.value.length > 10) {
recentLanguages.value = recentLanguages.value.slice(0, 10);
}
};
/**
* 获取按使用频率排序的语言列表
* @param translatorType 翻译器类型
* @param grouped 是否分组返回(常用/其他)
* @returns 排序后的语言列表或分组后的语言列表
*/
const getSortedLanguages = (translatorType: string, grouped: boolean = false): [string, any][] | {frequent: [string, any][], others: [string, any][]} => {
const languageMap = languageMaps.value[translatorType] || {};
// 获取语言列表
const languages = Object.entries(languageMap);
// 按使用频率排序
const sortedLanguages = languages.sort(([codeA, infoA], [codeB, infoB]) => {
// 获取使用次数默认为0
const countA = languageUsageCount.value[codeA.toLowerCase()] || 0;
const countB = languageUsageCount.value[codeB.toLowerCase()] || 0;
// 首先按使用频率降序排序
if (countB !== countA) {
return countB - countA;
}
// 其次按最近使用情况排序
const recentIndexA = recentLanguages.value.indexOf(codeA.toLowerCase());
const recentIndexB = recentLanguages.value.indexOf(codeB.toLowerCase());
if (recentIndexA !== -1 && recentIndexB !== -1) {
return recentIndexA - recentIndexB;
} else if (recentIndexA !== -1) {
return -1;
} else if (recentIndexB !== -1) {
return 1;
}
// 如果使用频率和最近使用情况都相同,按名称排序
const nameA = infoA.Name || infoA.name || codeA;
const nameB = infoB.Name || infoB.name || codeB;
return nameA.localeCompare(nameB);
});
// 如果不需要分组,直接返回排序后的列表
if (!grouped) {
return sortedLanguages;
}
// 分组:将有使用记录的语言归为常用组,其他归为其他组
const frequentLanguages: [string, any][] = [];
const otherLanguages: [string, any][] = [];
sortedLanguages.forEach(lang => {
const [code] = lang;
const usageCount = languageUsageCount.value[code.toLowerCase()] || 0;
const isInRecent = recentLanguages.value.includes(code.toLowerCase());
if (usageCount > 0 || isInRecent) {
frequentLanguages.push(lang);
} else {
otherLanguages.push(lang);
}
});
return {
frequent: frequentLanguages,
others: otherLanguages
};
};
/**
* 翻译文本
* @param text 待翻译文本
* @param to 目标语言代码
* @param translatorType 翻译器类型
* @returns 翻译结果
*/
const translateText = async (
text: string,
to?: string,
translatorType?: string
): Promise<TranslationResult> => {
// 使用提供的参数或默认值
const targetLang = to || defaultTargetLang.value;
const translator = translatorType || defaultTranslator.value;
// 处理空文本
if (!text) {
return {
sourceText: '',
translatedText: '',
sourceLang: '',
targetLang: targetLang,
translatorType: translator,
error: 'no text to translate'
};
}
// 检测源语言
const detected = detectLanguage(text);
if (detected) {
detectedSourceLang.value = detected;
}
// 使用检测到的语言或回退到auto
let actualSourceLang = detectedSourceLang.value || 'auto';
// 确认语言代码有效并针对当前翻译器进行匹配
actualSourceLang = validateLanguage(actualSourceLang, translator);
const actualTargetLang = validateLanguage(targetLang, translator);
// 如果源语言和目标语言相同,则直接返回原文
if (actualSourceLang !== 'auto' && actualSourceLang === actualTargetLang) {
return {
sourceText: text,
translatedText: text,
sourceLang: actualSourceLang,
targetLang: actualTargetLang,
translatorType: translator
};
}
isTranslating.value = true;
error.value = null;
try {
console.log(`翻译文本: 从 ${actualSourceLang}${actualTargetLang} 使用 ${translator} 翻译器`);
// 调用翻译服务
const translatedText = await TranslationService.TranslateWith(
text,
actualSourceLang,
actualTargetLang,
translator
);
// 增加目标语言的使用频率,使用较大的权重
incrementLanguageUsage(actualTargetLang, 3);
// 如果源语言不是auto也记录其使用情况但权重较小
if (actualSourceLang !== 'auto') {
incrementLanguageUsage(actualSourceLang, 1);
}
// 构建结果
const result: TranslationResult = {
sourceText: text,
translatedText,
sourceLang: actualSourceLang,
targetLang: actualTargetLang,
translatorType: translator
};
lastResult.value = result;
return result;
} catch (err) {
// 处理错误
const errorMessage = err instanceof Error ? err.message : 'translation failed';
error.value = errorMessage;
const result: TranslationResult = {
sourceText: text,
translatedText: '',
sourceLang: actualSourceLang,
targetLang: actualTargetLang,
translatorType: translator,
error: errorMessage
};
lastResult.value = result;
return result;
} finally {
isTranslating.value = false;
}
};
/**
* 设置默认翻译配置
* @param config 配置对象
*/
const setDefaultConfig = (config: {
targetLang?: string;
translatorType?: string;
}): void => {
let changed = false;
if (config.translatorType && config.translatorType !== defaultTranslator.value) {
defaultTranslator.value = config.translatorType;
// 切换翻译器时清空检测到的源语言,以便重新检测
detectedSourceLang.value = '';
changed = true;
}
if (config.targetLang) {
// 验证目标语言是否支持
const validLang = validateLanguage(config.targetLang, defaultTranslator.value);
defaultTargetLang.value = validLang;
changed = true;
}
if (changed) {
console.log(`已更新默认翻译配置:翻译器=${defaultTranslator.value},目标语言=${defaultTargetLang.value}`);
}
};
// 初始化时加载可用翻译器
loadAvailableTranslators();
return {
// 状态
availableTranslators,
isTranslating,
lastResult,
error,
detectedSourceLang,
defaultTargetLang,
defaultTranslator,
languageMaps,
languageUsageCount,
recentLanguages,
// 计算属性
hasTranslators,
currentLanguageMap,
// 方法
loadAvailableTranslators,
loadTranslatorLanguages,
translateText,
setDefaultConfig,
detectLanguage,
validateLanguage,
findSimilarLanguage,
getSortedLanguages,
incrementLanguageUsage
};
}, {
persist: {
key: 'voidraft-translation',
storage: localStorage,
pick: ['languageUsageCount', 'defaultTargetLang', 'defaultTranslator', 'recentLanguages']
}
});

View File

@@ -0,0 +1,135 @@
import {defineStore} from 'pinia'
import {computed, ref} from 'vue'
import {CheckForUpdates, ApplyUpdate, RestartApplication} from '@/../bindings/voidraft/internal/services/selfupdateservice'
import {SelfUpdateResult} from '@/../bindings/voidraft/internal/services/models'
import {useConfigStore} from './configStore'
import * as runtime from "@wailsio/runtime"
export const useUpdateStore = defineStore('update', () => {
// 状态
const isChecking = ref(false)
const isUpdating = ref(false)
const updateResult = ref<SelfUpdateResult | null>(null)
const hasCheckedOnStartup = ref(false)
const updateSuccess = ref(false)
const errorMessage = ref('')
// 计算属性
const hasUpdate = computed(() => updateResult.value?.hasUpdate || false)
// 检查更新
const checkForUpdates = async (): Promise<boolean> => {
if (isChecking.value) return false
// 重置错误信息
errorMessage.value = ''
isChecking.value = true
try {
const result = await CheckForUpdates()
if (result) {
updateResult.value = result
if (result.error) {
errorMessage.value = result.error
return false
}
return true
}
return false
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : 'Network error'
return false
} finally {
isChecking.value = false
}
}
// 应用更新
const applyUpdate = async (): Promise<boolean> => {
if (isUpdating.value) return false
// 重置错误信息
errorMessage.value = ''
isUpdating.value = true
try {
const result = await ApplyUpdate()
if (result) {
updateResult.value = result
if (result.error) {
errorMessage.value = result.error
return false
}
if (result.updateApplied) {
updateSuccess.value = true
return true
}
}
return false
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : 'Update failed'
return false
} finally {
isUpdating.value = false
}
}
// 重启应用
const restartApplication = async (): Promise<boolean> => {
try {
await RestartApplication()
return true
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : 'Restart failed'
return false
}
}
// 启动时检查更新
const checkOnStartup = async () => {
if (hasCheckedOnStartup.value) return
const configStore = useConfigStore()
if (configStore.config.updates.autoUpdate) {
await checkForUpdates()
}
hasCheckedOnStartup.value = true
}
// 打开发布页面
const openReleaseURL = async () => {
if (updateResult.value?.assetURL) {
await runtime.Browser.OpenURL(updateResult.value.assetURL)
}
}
// 重置状态
const reset = () => {
updateResult.value = null
isChecking.value = false
isUpdating.value = false
updateSuccess.value = false
errorMessage.value = ''
}
return {
// 状态
isChecking,
isUpdating,
updateResult,
hasCheckedOnStartup,
updateSuccess,
errorMessage,
// 计算属性
hasUpdate,
// 方法
checkForUpdates,
applyUpdate,
restartApplication,
checkOnStartup,
openReleaseURL,
reset
}
})

View File

@@ -0,0 +1,31 @@
import {computed} from 'vue';
import {defineStore} from 'pinia';
import {IsDocumentWindowOpen} from "@/../bindings/voidraft/internal/services/windowservice";
export const useWindowStore = defineStore('window', () => {
// 判断是否为主窗口
const isMainWindow = computed(() => {
const urlParams = new URLSearchParams(window.location.search);
return !urlParams.has('documentId');
});
// 获取当前窗口的documentId
const currentDocumentId = computed(() => {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('documentId');
});
/**
* 判断文档窗口是否打开
* @param documentId 文档ID
*/
async function isDocumentWindowOpen(documentId: number) {
return IsDocumentWindowOpen(documentId);
}
return {
isMainWindow,
currentDocumentId,
isDocumentWindowOpen
};
});

View File

@@ -1,5 +0,0 @@
export interface DocumentStats {
lines: number;
characters: number;
selectedCharacters: number;
}

View File

@@ -1,4 +0,0 @@
// 统一类型导出
export type { ThemeType, LanguageType, SystemThemeType } from '@/../bindings/voidraft/internal/models';
export * from './theme';
export * from './editor';

View File

@@ -1,204 +0,0 @@
import type { ThemeType } from '@/../bindings/voidraft/internal/models';
// 主题配置信息
export interface ThemeInfo {
id: ThemeType;
name: string;
displayName: string;
isDark: boolean;
previewColors: {
background: string;
foreground: string;
keyword: string;
string: string;
function: string;
comment: string;
};
}
// 可用主题列表
export const AVAILABLE_THEMES: ThemeInfo[] = [
{
id: 'default-dark' as ThemeType,
name: 'default-dark',
displayName: '深色默认',
isDark: true,
previewColors: {
background: '#252B37',
foreground: '#9BB586',
keyword: '#FF79C6',
string: '#F1FA8C',
function: '#50FA7B',
comment: '#6272A4'
}
},
{
id: 'dracula' as ThemeType,
name: 'dracula',
displayName: 'Dracula',
isDark: true,
previewColors: {
background: '#282A36',
foreground: '#F8F8F2',
keyword: '#FF79C6',
string: '#F1FA8C',
function: '#50FA7B',
comment: '#6272A4'
}
},
{
id: 'aura' as ThemeType,
name: 'aura',
displayName: 'Aura',
isDark: true,
previewColors: {
background: '#21202e',
foreground: '#edecee',
keyword: '#a277ff',
string: '#61ffca',
function: '#ffca85',
comment: '#6d6d6d'
}
},
{
id: 'github-dark' as ThemeType,
name: 'github-dark',
displayName: 'GitHub 深色',
isDark: true,
previewColors: {
background: '#24292e',
foreground: '#d1d5da',
keyword: '#f97583',
string: '#9ecbff',
function: '#79b8ff',
comment: '#6a737d'
}
},
{
id: 'github-light' as ThemeType,
name: 'github-light',
displayName: 'GitHub 浅色',
isDark: false,
previewColors: {
background: '#fff',
foreground: '#444d56',
keyword: '#d73a49',
string: '#032f62',
function: '#005cc5',
comment: '#6a737d'
}
},
{
id: 'material-dark' as ThemeType,
name: 'material-dark',
displayName: 'Material 深色',
isDark: true,
previewColors: {
background: '#263238',
foreground: '#EEFFFF',
keyword: '#C792EA',
string: '#C3E88D',
function: '#82AAFF',
comment: '#546E7A'
}
},
{
id: 'material-light' as ThemeType,
name: 'material-light',
displayName: 'Material 浅色',
isDark: false,
previewColors: {
background: '#FAFAFA',
foreground: '#90A4AE',
keyword: '#7C4DFF',
string: '#91B859',
function: '#6182B8',
comment: '#90A4AE'
}
},
{
id: 'solarized-dark' as ThemeType,
name: 'solarized-dark',
displayName: 'Solarized 深色',
isDark: true,
previewColors: {
background: '#002B36',
foreground: '#93A1A1',
keyword: '#859900',
string: '#2AA198',
function: '#268BD2',
comment: '#586E75'
}
},
{
id: 'solarized-light' as ThemeType,
name: 'solarized-light',
displayName: 'Solarized 浅色',
isDark: false,
previewColors: {
background: '#FDF6E3',
foreground: '#586E75',
keyword: '#859900',
string: '#2AA198',
function: '#268BD2',
comment: '#93A1A1'
}
},
{
id: 'tokyo-night' as ThemeType,
name: 'tokyo-night',
displayName: 'Tokyo Night',
isDark: true,
previewColors: {
background: '#1a1b26',
foreground: '#787c99',
keyword: '#bb9af7',
string: '#9ece6a',
function: '#7aa2f7',
comment: '#444b6a'
}
},
{
id: 'tokyo-night-storm' as ThemeType,
name: 'tokyo-night-storm',
displayName: 'Tokyo Night Storm',
isDark: true,
previewColors: {
background: '#24283b',
foreground: '#7982a9',
keyword: '#bb9af7',
string: '#9ece6a',
function: '#7aa2f7',
comment: '#565f89'
}
},
{
id: 'tokyo-night-day' as ThemeType,
name: 'tokyo-night-day',
displayName: 'Tokyo Night Day',
isDark: false,
previewColors: {
background: '#e1e2e7',
foreground: '#6a6f8e',
keyword: '#9854f1',
string: '#587539',
function: '#2e7de9',
comment: '#9da3c2'
}
}
];
// 根据主题ID获取主题信息
export function getThemeInfo(themeId: ThemeType): ThemeInfo | undefined {
return AVAILABLE_THEMES.find(theme => theme.id === themeId);
}
// 获取所有深色主题
export function getDarkThemes(): ThemeInfo[] {
return AVAILABLE_THEMES.filter(theme => theme.isDark);
}
// 获取所有浅色主题
export function getLightThemes(): ThemeInfo[] {
return AVAILABLE_THEMES.filter(theme => !theme.isDark);
}

View File

@@ -1,97 +0,0 @@
import { useLogStore } from '@/stores/logStore';
import { useI18n } from 'vue-i18n';
/**
* 创建组合式函数,用于在组件中使用错误处理
*/
export function useErrorHandler() {
const logStore = useLogStore();
const { t } = useI18n();
const handleError = (error: unknown, messageKey: string) => {
logStore.error(t(messageKey));
};
const safeCall = async <T>(
operation: () => Promise<T>,
errorMessageKey: string,
successMessageKey?: string
): Promise<T | null> => {
try {
const result = await operation();
if (successMessageKey) {
logStore.info(t(successMessageKey));
}
return result;
} catch (error) {
logStore.error(t(errorMessageKey));
return null;
}
};
/**
* 静默处理错误,不显示用户提示
*/
const silentCall = async <T>(
operation: () => Promise<T>
): Promise<T | null> => {
try {
return await operation();
} catch (error) {
// 静默忽略错误
return null;
}
};
/**
* 创建带错误处理的数值调整器
*/
const createSafeAdjuster = (
adjustFn: () => Promise<void>,
errorMessageKey: string
) => () => safeCall(adjustFn, errorMessageKey);
return {
handleError,
safeCall,
silentCall,
createSafeAdjuster
};
}
/**
* 错误处理工具函数集合不依赖Vue上下文
*/
export const ErrorUtils = {
/**
* 静默处理错误
*/
silent: async <T>(operation: () => Promise<T>): Promise<T | null> => {
try {
return await operation();
} catch (error) {
return null;
}
},
/**
* 带重试的错误处理
*/
withRetry: async <T>(
operation: () => Promise<T>,
maxRetries: number = 3,
delay: number = 1000
): Promise<T | null> => {
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
if (i === maxRetries - 1) {
return null;
}
await new Promise(resolve => setTimeout(resolve, delay));
}
}
return null;
}
};

View File

@@ -1,243 +1,55 @@
<script setup lang="ts">
import {onBeforeUnmount, onMounted, ref, watch} from 'vue';
import {EditorState, Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {onBeforeUnmount, onMounted, ref} from 'vue';
import {useEditorStore} from '@/stores/editorStore';
import {useConfigStore} from '@/stores/configStore';
import {useDocumentStore} from '@/stores/documentStore';
import {useLogStore} from '@/stores/logStore';
import {createBasicSetup} from './extensions/basicSetup';
import {
createStatsUpdateExtension,
createWheelZoomHandler,
getTabExtensions,
updateStats,
updateTabConfig,
createAutoSavePlugin,
createSaveShortcutPlugin,
createFontExtensionFromBackend,
updateFontConfig,
} from './extensions';
import { useEditorTheme } from '@/composables/useEditorTheme';
import { useI18n } from 'vue-i18n';
import type { ThemeType } from '@/types';
import { DocumentService } from '../../../bindings/voidraft/internal/services';
import {useConfigStore} from '@/stores/configStore';
import {createWheelZoomHandler} from './basic/wheelZoomExtension';
import Toolbar from '@/components/toolbar/Toolbar.vue';
import {useWindowStore} from "@/stores/windowStore";
import LoadingScreen from '@/components/loading/LoadingScreen.vue';
const editorStore = useEditorStore();
const configStore = useConfigStore();
const documentStore = useDocumentStore();
const logStore = useLogStore();
const { t } = useI18n();
const { createThemeExtension, updateTheme } = useEditorTheme();
const props = defineProps({
initialDoc: {
type: String,
default: ''
}
});
const configStore = useConfigStore();
const windowStore = useWindowStore();
const editorElement = ref<HTMLElement | null>(null);
const editorCreated = ref(false);
let isDestroying = false;
// 创建编辑
const createEditor = async () => {
if (!editorElement.value || editorCreated.value) return;
editorCreated.value = true;
// 加载文档内容
await documentStore.initialize();
const docContent = documentStore.documentContent || props.initialDoc;
// 获取基本扩展
const basicExtensions = createBasicSetup();
// 获取主题扩展
const themeExtension = await createThemeExtension(
configStore.config.appearance.theme || 'default-dark' as ThemeType
);
// 获取Tab相关扩展
const tabExtensions = getTabExtensions(
configStore.config.editing.tabSize,
configStore.config.editing.enableTabIndent,
configStore.config.editing.tabType
);
// 创建字体扩展
const fontExtension = createFontExtensionFromBackend({
fontFamily: configStore.config.editing.fontFamily,
fontSize: configStore.config.editing.fontSize,
lineHeight: configStore.config.editing.lineHeight,
fontWeight: configStore.config.editing.fontWeight
});
// 创建统计信息更新扩展
const statsExtension = createStatsUpdateExtension(
editorStore.updateDocumentStats
);
// 创建保存快捷键插件
const saveShortcutPlugin = createSaveShortcutPlugin(() => {
if (editorStore.editorView) {
handleManualSave();
}
});
// 创建自动保存插件
const autoSavePlugin = createAutoSavePlugin({
debounceDelay: 300, // 300毫秒的输入防抖
onSave: (success) => {
if (success) {
documentStore.lastSaved = new Date();
}
}
});
// 组合所有扩展
const extensions: Extension[] = [
themeExtension,
...basicExtensions,
...tabExtensions,
fontExtension,
statsExtension,
saveShortcutPlugin,
autoSavePlugin
];
// 创建编辑器状态
const state = EditorState.create({
doc: docContent,
extensions
});
// 创建编辑器视图
const view = new EditorView({
state,
parent: editorElement.value
});
// 将编辑器实例保存到store
editorStore.setEditorView(view);
// 应用初始字体大小
editorStore.applyFontSize();
// 立即更新统计信息
updateStats(view, editorStore.updateDocumentStats);
};
// 创建滚轮事件处理器
const handleWheel = createWheelZoomHandler(
// 创建滚轮缩放处理
const wheelHandler = createWheelZoomHandler(
configStore.increaseFontSize,
configStore.decreaseFontSize
);
// 手动保存文档
const handleManualSave = async () => {
if (!editorStore.editorView || isDestroying) return;
onMounted(async () => {
if (!editorElement.value) return;
// 从URL查询参数中获取documentId
const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined;
const view = editorStore.editorView as EditorView;
const content = view.state.doc.toString();
// 先更新内容
await DocumentService.UpdateActiveDocumentContent(content);
// 然后调用强制保存方法不再传递content参数
const success = await documentStore.forceSaveDocument();
if (success) {
logStore.info(t('document.manualSaveSuccess'));
}
};
// 重新配置编辑器(仅在必要时)
const reconfigureTabSettings = () => {
if (!editorStore.editorView) return;
updateTabConfig(
editorStore.editorView as EditorView,
configStore.config.editing.tabSize,
configStore.config.editing.enableTabIndent,
configStore.config.editing.tabType
);
};
// 重新配置字体设置
const reconfigureFontSettings = () => {
if (!editorStore.editorView) return;
updateFontConfig(editorStore.editorView as EditorView, {
fontFamily: configStore.config.editing.fontFamily,
fontSize: configStore.config.editing.fontSize,
lineHeight: configStore.config.editing.lineHeight,
fontWeight: configStore.config.editing.fontWeight
});
};
// 监听Tab设置变化
watch([
() => configStore.config.editing.tabSize,
() => configStore.config.editing.enableTabIndent,
() => configStore.config.editing.tabType,
], () => {
reconfigureTabSettings();
});
// 监听字体大小变化
watch([
() => configStore.config.editing.fontFamily,
() => configStore.config.editing.fontSize,
() => configStore.config.editing.lineHeight,
() => configStore.config.editing.fontWeight,
], () => {
reconfigureFontSettings();
editorStore.applyFontSize();
});
// 监听主题变化
watch(() => configStore.config.appearance.theme, async (newTheme) => {
if (newTheme && editorStore.editorView) {
await updateTheme(editorStore.editorView as EditorView, newTheme);
}
});
onMounted(() => {
// 创建编辑器
createEditor();
// 初始化文档存储优先使用URL参数中的文档ID
await documentStore.initialize(urlDocumentId);
// 设置编辑器容器
editorStore.setEditorContainer(editorElement.value);
// 添加滚轮事件监听
if (editorElement.value) {
editorElement.value.addEventListener('wheel', handleWheel, {passive: false});
}
// 确保统计信息已更新
if (editorStore.editorView) {
setTimeout(() => {
updateStats(editorStore.editorView as EditorView, editorStore.updateDocumentStats);
}, 100);
}
editorElement.value.addEventListener('wheel', wheelHandler, {passive: false});
});
onBeforeUnmount(() => {
isDestroying = true;
// 移除滚轮事件监听
if (editorElement.value) {
editorElement.value.removeEventListener('wheel', handleWheel);
}
// 直接销毁编辑器
if (editorStore.editorView) {
editorStore.editorView.destroy();
editorStore.setEditorView(null);
editorElement.value.removeEventListener('wheel', wheelHandler);
}
});
</script>
<template>
<div class="editor-container">
<LoadingScreen v-if="editorStore.isLoading" text="VOIDRAFT" />
<div ref="editorElement" class="editor"></div>
<Toolbar />
<Toolbar/>
</div>
</template>
@@ -248,6 +60,7 @@ onBeforeUnmount(() => {
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
.editor {
width: 100%;
@@ -264,4 +77,4 @@ onBeforeUnmount(() => {
:deep(.cm-scroller) {
overflow: auto;
}
</style>
</style>

View File

@@ -15,19 +15,17 @@ import {
bracketMatching,
defaultHighlightStyle,
foldGutter,
foldKeymap,
indentOnInput,
syntaxHighlighting,
} from '@codemirror/language';
import {defaultKeymap, history, historyKeymap,} from '@codemirror/commands';
import {highlightSelectionMatches, searchKeymap} from '@codemirror/search';
import {autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap} from '@codemirror/autocomplete';
import {lintKeymap} from '@codemirror/lint';
import {history} from '@codemirror/commands';
import {highlightSelectionMatches} from '@codemirror/search';
import {autocompletion, closeBrackets, closeBracketsKeymap} from '@codemirror/autocomplete';
import createEditorContextMenu from '../contextMenu';
// 基本编辑器设置,包含常用扩展
// 基本编辑器设置
export const createBasicSetup = (): Extension[] => {
return [
// 基础UI
lineNumbers(),
highlightActiveLineGutter(),
@@ -57,15 +55,12 @@ export const createBasicSetup = (): Extension[] => {
// 自动完成
autocompletion(),
// 上下文菜单
createEditorContextMenu(),
// 键盘映射
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap
]),
];
};
};

View File

@@ -0,0 +1,39 @@
import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
import { useDocumentStore } from '@/stores/documentStore';
import { useEditorStore } from '@/stores/editorStore';
/**
* 内容变化监听插件 - 集成文档和编辑器管理
*/
export function createContentChangePlugin() {
return ViewPlugin.fromClass(
class ContentChangePlugin {
private documentStore = useDocumentStore();
private editorStore = useEditorStore();
private lastContent = '';
constructor(private view: EditorView) {
this.lastContent = view.state.doc.toString();
}
update(update: ViewUpdate) {
if (!update.docChanged) return;
const newContent = this.view.state.doc.toString();
if (newContent === this.lastContent) return;
this.lastContent = newContent;
// 通知编辑器管理器内容已变化
const currentDocId = this.documentStore.currentDocumentId;
if (currentDocId) {
this.editorStore.onContentChange(currentDocId);
}
}
destroy() {
}
}
);
}

View File

@@ -81,14 +81,6 @@ export function createFontExtension(config: Partial<FontConfig> = {}): Extension
return EditorView.theme(styles);
}
// 创建响应式字体大小扩展
export function createResponsiveFontExtension(baseFontSize: number = 14): Extension {
return fontCompartment.of(createFontExtension({
fontSize: baseFontSize,
lineHeight: 1.5
}));
}
// 从后端配置创建字体扩展
export function createFontExtensionFromBackend(backendConfig: {
fontFamily?: string;

View File

@@ -0,0 +1,65 @@
import {Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {DocumentStats} from '@/stores/editorStore';
import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state';
// 更新编辑器文档统计信息
export const updateStats = (
view: EditorView,
updateDocumentStats: (stats: DocumentStats) => void
) => {
if (!view) return;
const state = view.state;
// 获取当前光标所在的代码块
const activeBlock = getActiveNoteBlock(state as any);
if (!activeBlock) {
// 如果没有活动块,显示空统计
updateDocumentStats({
lines: 0,
characters: 0,
selectedCharacters: 0
});
return;
}
// 获取当前块的内容范围
const blockContent = state.doc.sliceString(activeBlock.content.from, activeBlock.content.to);
// 计算块内容的行数
const blockLines = blockContent.split('\n').length;
// 计算选中的字符数(只统计在当前块内的选中内容)
let selectedChars = 0;
const selections = state.selection;
if (selections) {
for (let i = 0; i < selections.ranges.length; i++) {
const range = selections.ranges[i];
// 计算选中范围与当前块内容范围的交集
const selectionStart = Math.max(range.from, activeBlock.content.from);
const selectionEnd = Math.min(range.to, activeBlock.content.to);
if (selectionStart < selectionEnd) {
selectedChars += selectionEnd - selectionStart;
}
}
}
updateDocumentStats({
lines: blockLines,
characters: blockContent.length,
selectedCharacters: selectedChars
});
};
// 创建统计信息更新监听器扩展
export const createStatsUpdateExtension = (
updateDocumentStats: (stats: DocumentStats) => void
): Extension => {
return EditorView.updateListener.of(update => {
if (update.docChanged || update.selectionSet) {
updateStats(update.view, updateDocumentStats);
}
});
};

View File

@@ -2,7 +2,7 @@ import {Compartment, Extension} from '@codemirror/state';
import {EditorView, keymap} from '@codemirror/view';
import {indentSelection} from '@codemirror/commands';
import {indentUnit} from '@codemirror/language';
import {TabType} from '../../../../bindings/voidraft/internal/models/models';
import {TabType} from '@/../bindings/voidraft/internal/models/models';
// Tab设置相关的compartment
export const tabSizeCompartment = new Compartment();

View File

@@ -0,0 +1,58 @@
import { Extension, Compartment } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { SystemThemeType } from '@/../bindings/voidraft/internal/models/models';
import { createDarkTheme } from '@/views/editor/theme/dark';
import { createLightTheme } from '@/views/editor/theme/light';
import { useThemeStore } from '@/stores/themeStore';
// 主题区间 - 用于动态切换主题
export const themeCompartment = new Compartment();
/**
* 根据主题类型获取主题扩展
*/
const getThemeExtension = (themeType: SystemThemeType): Extension => {
const themeStore = useThemeStore();
// 处理 auto 主题类型
let actualTheme: SystemThemeType = themeType;
if (themeType === SystemThemeType.SystemThemeAuto) {
actualTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
? SystemThemeType.SystemThemeDark
: SystemThemeType.SystemThemeLight;
}
// 根据主题类型创建主题
if (actualTheme === SystemThemeType.SystemThemeLight) {
return createLightTheme(themeStore.themeColors.lightTheme);
} else {
return createDarkTheme(themeStore.themeColors.darkTheme);
}
};
/**
* 创建主题扩展(用于编辑器初始化)
*/
export const createThemeExtension = (themeType: SystemThemeType = SystemThemeType.SystemThemeDark): Extension => {
const extension = getThemeExtension(themeType);
return themeCompartment.of(extension);
};
/**
* 更新编辑器主题
*/
export const updateEditorTheme = (view: EditorView, themeType: SystemThemeType): void => {
if (!view?.dispatch) {
return;
}
try {
const extension = getThemeExtension(themeType);
view.dispatch({
effects: themeCompartment.reconfigure(extension)
});
} catch (error) {
console.error('Failed to update editor theme:', error);
}
};

View File

@@ -0,0 +1,156 @@
/**
* 编辑器上下文菜单样式
* 支持系统主题自动适配
*/
.cm-context-menu {
position: fixed;
background-color: var(--settings-card-bg);
color: var(--settings-text);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 4px 0;
/* 优化阴影效果,只在右下角显示自然的阴影 */
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.12);
min-width: 200px;
max-width: 320px;
z-index: 9999;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
opacity: 0;
transform: scale(0.95);
transition: opacity 0.15s ease-out, transform 0.15s ease-out;
overflow: visible; /* 确保子菜单可以显示在外部 */
}
.cm-context-menu-item {
padding: 8px 12px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
transition: all 0.1s ease;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cm-context-menu-item:hover {
background-color: var(--toolbar-button-hover);
color: var(--toolbar-text);
}
.cm-context-menu-item-label {
display: flex;
align-items: center;
gap: 8px;
}
.cm-context-menu-item-shortcut {
opacity: 0.7;
font-size: 12px;
padding: 2px 4px;
border-radius: 4px;
background-color: var(--settings-input-bg);
color: var(--settings-text-secondary);
margin-left: 16px;
}
.cm-context-menu-item-ripple {
position: absolute;
border-radius: 50%;
background-color: var(--selection-bg);
width: 100px;
height: 100px;
opacity: 0.5;
transform: scale(0);
transition: transform 0.3s ease-out, opacity 0.3s ease-out;
}
/* 菜单分组标题样式 */
.cm-context-menu-group-title {
padding: 6px 12px;
font-size: 12px;
color: var(--text-secondary);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
user-select: none;
}
/* 菜单分隔线样式 */
.cm-context-menu-divider {
height: 1px;
background-color: var(--border-color);
margin: 4px 0;
}
/* 子菜单样式 */
.cm-context-submenu-container {
position: relative;
}
.cm-context-menu-item-with-submenu {
position: relative;
}
.cm-context-menu-item-with-submenu::after {
content: "";
position: absolute;
right: 12px;
font-size: 16px;
opacity: 0.7;
}
.cm-context-submenu {
position: fixed; /* 改为fixed定位避免受父元素影响 */
min-width: 180px;
opacity: 0;
pointer-events: none;
transform: translateX(10px);
transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 10000;
border-radius: 6px;
background-color: var(--settings-card-bg);
color: var(--settings-text);
border: 1px solid var(--border-color);
padding: 4px 0;
/* 子菜单也使用相同的阴影效果 */
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.12);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.cm-context-menu-item-with-submenu:hover .cm-context-submenu {
opacity: 1;
pointer-events: auto;
transform: translateX(0);
}
/* 深色主题下的特殊样式 */
:root[data-theme="dark"] .cm-context-menu {
/* 深色主题下阴影更深,但仍然只在右下角 */
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25);
}
:root[data-theme="dark"] .cm-context-submenu {
/* 深色主题下子菜单阴影 */
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25);
}
:root[data-theme="dark"] .cm-context-menu-divider {
background-color: var(--dark-border-color);
opacity: 0.6;
}
/* 动画相关类 */
.cm-context-menu.show {
opacity: 1;
transform: scale(1);
}
.cm-context-menu.hide {
opacity: 0;
}

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