26 Commits

Author SHA1 Message Date
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
133 changed files with 15836 additions and 3704 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
- [ ] Enhanced clipboard - Monitor and manage clipboard history
- [ ] Data synchronization - Cloud backup for configurations and documents
- [ ] 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

@@ -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,9 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import * as Service from "./service.js";
export {
Service
};
export * from "./models.js";

View File

@@ -0,0 +1,51 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Create as $Create} from "@wailsio/runtime";
export class Config {
/**
* DBSource is the database URI to use.
* The string ":memory:" can be used to create an in-memory database.
* The sqlite driver can be configured through query parameters.
* For more details see https://pkg.go.dev/modernc.org/sqlite#Driver.Open
*/
"DBSource": string;
/** Creates a new Config instance. */
constructor($$source: Partial<Config> = {}) {
if (!("DBSource" in $$source)) {
this["DBSource"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new Config instance from a string or object.
*/
static createFrom($$source: any = {}): Config {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new Config($$parsedSource as Partial<Config>);
}
}
/**
* Row holds a single row in the result of a query.
* It is a key-value map where keys are column names.
*/
export type Row = { [_: string]: any };
/**
* Rows holds the result of a query
* as an array of key-value maps where keys are column names.
*/
export type Rows = Row[];
/**
* Stmt wraps a prepared sql statement pointer.
* It provides the same methods as the [sql.Stmt] type.
*/
export type Stmt = string;

View File

@@ -0,0 +1,223 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as application$0 from "../../application/models.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as $models from "./models.js";
export {
ExecContext as Execute,
QueryContext as Query
};
import { Stmt } from "./stmt.js";
/**
* Prepare creates a prepared statement for later queries or executions.
* Multiple queries or executions may be run concurrently from the returned statement.
*
* The caller must call the statement's Close method when it is no longer needed.
* Statements are closed automatically
* when the connection they are associated with is closed.
*
* Prepare supports early cancellation.
*/
export function Prepare(query: string): Promise<Stmt | null> & { cancel(): void } {
const promise = PrepareContext(query);
const wrapper: any = (promise.then(function (id) {
return id == null ? null : new Stmt(
ClosePrepared.bind(null, id),
ExecPrepared.bind(null, id),
QueryPrepared.bind(null, id));
}));
wrapper.cancel = promise.cancel;
return wrapper;
}
/**
* Close closes the current database connection if one is open, otherwise has no effect.
* Additionally, Close closes all open prepared statements associated to the connection.
*
* Even when a non-nil error is returned,
* the database service is left in a consistent state,
* ready for a call to [Service.Open].
*/
export function Close(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1888105376) as any;
return $resultPromise;
}
/**
* ClosePrepared closes a prepared statement
* obtained with [Service.Prepare] or [Service.PrepareContext].
* ClosePrepared is idempotent:
* it has no effect on prepared statements that are already closed.
*/
function ClosePrepared(stmt: $models.Stmt | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2526200629, stmt) as any;
return $resultPromise;
}
/**
* Configure changes the database service configuration.
* The connection state at call time is preserved.
* Consumers will need to call [Service.Open] manually after Configure
* in order to reconnect with the new configuration.
*
* See [NewWithConfig] for details on configuration.
*/
export function Configure(config: $models.Config | null): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1939578712, config) as any;
return $resultPromise;
}
/**
* ExecContext executes a query without returning any rows.
* It supports early cancellation.
*/
function ExecContext(query: string, ...args: any[]): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(674944556, query, args) as any;
return $resultPromise;
}
/**
* ExecPrepared executes a prepared statement
* obtained with [Service.Prepare] or [Service.PrepareContext]
* without returning any rows.
* It supports early cancellation.
*/
function ExecPrepared(stmt: $models.Stmt | null, ...args: any[]): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2086877656, stmt, args) as any;
return $resultPromise;
}
/**
* Execute executes a query without returning any rows.
*/
export function Execute(query: string, ...args: any[]): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3811930203, query, args) as any;
return $resultPromise;
}
/**
* Open validates the current configuration,
* closes the current connection if one is present,
* then opens and validates a new connection.
*
* Even when a non-nil error is returned,
* the database service is left in a consistent state,
* ready for a new call to Open.
*/
export function Open(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(2012175612) as any;
return $resultPromise;
}
/**
* Prepare creates a prepared statement for later queries or executions.
* Multiple queries or executions may be run concurrently from the returned statement.
*
* The caller should call the statement's Close method when it is no longer needed.
* Statements are closed automatically
* when the connection they are associated with is closed.
*/
export function Prepare(query: string): Promise<$models.Stmt | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(1801965143, query) as any;
return $resultPromise;
}
/**
* PrepareContext creates a prepared statement for later queries or executions.
* Multiple queries or executions may be run concurrently from the returned statement.
*
* The caller must call the statement's Close method when it is no longer needed.
* Statements are closed automatically
* when the connection they are associated with is closed.
*
* PrepareContext supports early cancellation.
*/
function PrepareContext(query: string): Promise<$models.Stmt | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(570941694, query) as any;
return $resultPromise;
}
/**
* Query executes a query and returns a slice of key-value records,
* one per row, with column names as keys.
*/
export function Query(query: string, ...args: any[]): Promise<$models.Rows> & { cancel(): void } {
let $resultPromise = $Call.ByID(860757720, query, args) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* QueryContext executes a query and returns a slice of key-value records,
* one per row, with column names as keys.
* It supports early cancellation, returning the slice of results fetched so far.
*/
function QueryContext(query: string, ...args: any[]): Promise<$models.Rows> & { cancel(): void } {
let $resultPromise = $Call.ByID(4115542347, query, args) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* QueryPrepared executes a prepared statement
* obtained with [Service.Prepare] or [Service.PrepareContext]
* and returns a slice of key-value records, one per row, with column names as keys.
* It supports early cancellation, returning the slice of results fetched so far.
*/
function QueryPrepared(stmt: $models.Stmt | null, ...args: any[]): Promise<$models.Rows> & { cancel(): void } {
let $resultPromise = $Call.ByID(3885083725, stmt, args) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType1($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* ServiceName returns the name of the plugin.
* You should use the go module format e.g. github.com/myuser/myplugin
*/
export function ServiceName(): Promise<string> & { cancel(): void } {
let $resultPromise = $Call.ByID(1637123084) as any;
return $resultPromise;
}
/**
* ServiceShutdown closes the database connection.
* It returns a non-nil error in case of failures.
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(3650435925) as any;
return $resultPromise;
}
/**
* ServiceStartup opens the database connection.
* It returns a non-nil error in case of failures.
*/
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1113159936, options) as any;
return $resultPromise;
}
// Private type creation functions
const $$createType0 = $Create.Map($Create.Any, $Create.Any);
const $$createType1 = $Create.Array($$createType0);

View File

@@ -0,0 +1,79 @@
//@ts-check
//@ts-ignore: Unused imports
import * as $models from "./models.js";
const execSymbol = Symbol("exec"),
querySymbol = Symbol("query"),
closeSymbol = Symbol("close");
/**
* Stmt represents a prepared statement for later queries or executions.
* Multiple queries or executions may be run concurrently on the same statement.
*
* The caller must call the statement's Close method when it is no longer needed.
* Statements are closed automatically
* when the connection they are associated with is closed.
*/
export class Stmt {
/**
* Constructs a new prepared statement instance.
* @param {(...args: any[]) => Promise<void>} close
* @param {(...args: any[]) => Promise<void> & { cancel(): void }} exec
* @param {(...args: any[]) => Promise<$models.Rows> & { cancel(): void }} query
*/
constructor(close, exec, query) {
/**
* @member
* @private
* @type {typeof close}
*/
this[closeSymbol] = close;
/**
* @member
* @private
* @type {typeof exec}
*/
this[execSymbol] = exec;
/**
* @member
* @private
* @type {typeof query}
*/
this[querySymbol] = query;
}
/**
* Closes the prepared statement.
* It has no effect when the statement is already closed.
* @returns {Promise<void>}
*/
Close() {
return this[closeSymbol]();
}
/**
* Executes the prepared statement without returning any rows.
* It supports early cancellation.
*
* @param {any[]} args
* @returns {Promise<void> & { cancel(): void }}
*/
Exec(...args) {
return this[execSymbol](...args);
}
/**
* Executes the prepared statement
* and returns a slice of key-value records, one per row, with column names as keys.
* It supports early cancellation, returning the array of results fetched so far.
*
* @param {any[]} args
* @returns {Promise<$models.Rows> & { cancel(): void }}
*/
Query(...args) {
return this[querySymbol](...args);
}
}

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",
};

View File

@@ -102,6 +102,11 @@ export class AppearanceConfig {
*/
"systemTheme": SystemThemeType;
/**
* 自定义主题配置
*/
"customTheme": CustomThemeConfig;
/** Creates a new AppearanceConfig instance. */
constructor($$source: Partial<AppearanceConfig> = {}) {
if (!("language" in $$source)) {
@@ -110,6 +115,9 @@ export class AppearanceConfig {
if (!("systemTheme" in $$source)) {
this["systemTheme"] = ("" as SystemThemeType);
}
if (!("customTheme" in $$source)) {
this["customTheme"] = (new CustomThemeConfig());
}
Object.assign(this, $$source);
}
@@ -118,7 +126,11 @@ export class AppearanceConfig {
* Creates a new AppearanceConfig instance from a string or object.
*/
static createFrom($$source: any = {}): AppearanceConfig {
const $$createField2_0 = $$createType5;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("customTheme" in $$parsedSource) {
$$parsedSource["customTheme"] = $$createField2_0($$parsedSource["customTheme"]);
}
return new AppearanceConfig($$parsedSource as Partial<AppearanceConfig>);
}
}
@@ -159,27 +171,79 @@ export class ConfigMetadata {
}
/**
* Document 表示一个文档
* CustomThemeConfig 自定义主题配置
*/
export class Document {
export class CustomThemeConfig {
/**
* 元数据
* 深色主题配置
*/
"meta": DocumentMeta;
"darkTheme": ThemeColorConfig;
/**
* 文档内容
* 浅色主题配置
*/
"lightTheme": ThemeColorConfig;
/** Creates a new CustomThemeConfig instance. */
constructor($$source: Partial<CustomThemeConfig> = {}) {
if (!("darkTheme" in $$source)) {
this["darkTheme"] = (new ThemeColorConfig());
}
if (!("lightTheme" in $$source)) {
this["lightTheme"] = (new ThemeColorConfig());
}
Object.assign(this, $$source);
}
/**
* Creates a new CustomThemeConfig instance from a string or object.
*/
static createFrom($$source: any = {}): CustomThemeConfig {
const $$createField0_0 = $$createType6;
const $$createField1_0 = $$createType6;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("darkTheme" in $$parsedSource) {
$$parsedSource["darkTheme"] = $$createField0_0($$parsedSource["darkTheme"]);
}
if ("lightTheme" in $$parsedSource) {
$$parsedSource["lightTheme"] = $$createField1_0($$parsedSource["lightTheme"]);
}
return new CustomThemeConfig($$parsedSource as Partial<CustomThemeConfig>);
}
}
/**
* Document represents a document in the system
*/
export class Document {
"id": number;
"title": string;
"content": string;
"createdAt": time$0.Time;
"updatedAt": time$0.Time;
"is_deleted": boolean;
/** Creates a new Document instance. */
constructor($$source: Partial<Document> = {}) {
if (!("meta" in $$source)) {
this["meta"] = (new DocumentMeta());
if (!("id" in $$source)) {
this["id"] = 0;
}
if (!("title" in $$source)) {
this["title"] = "";
}
if (!("content" in $$source)) {
this["content"] = "";
}
if (!("createdAt" in $$source)) {
this["createdAt"] = null;
}
if (!("updatedAt" in $$source)) {
this["updatedAt"] = null;
}
if (!("is_deleted" in $$source)) {
this["is_deleted"] = false;
}
Object.assign(this, $$source);
}
@@ -188,66 +252,11 @@ export class Document {
* Creates a new Document instance from a string or object.
*/
static createFrom($$source: any = {}): Document {
const $$createField0_0 = $$createType5;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("meta" in $$parsedSource) {
$$parsedSource["meta"] = $$createField0_0($$parsedSource["meta"]);
}
return new Document($$parsedSource as Partial<Document>);
}
}
/**
* DocumentMeta 文档元数据
*/
export class DocumentMeta {
/**
* 文档唯一标识
*/
"id": string;
/**
* 文档标题
*/
"title": string;
/**
* 最后更新时间
*/
"lastUpdated": time$0.Time;
/**
* 创建时间
*/
"createdAt": time$0.Time;
/** Creates a new DocumentMeta instance. */
constructor($$source: Partial<DocumentMeta> = {}) {
if (!("id" in $$source)) {
this["id"] = "";
}
if (!("title" in $$source)) {
this["title"] = "";
}
if (!("lastUpdated" in $$source)) {
this["lastUpdated"] = null;
}
if (!("createdAt" in $$source)) {
this["createdAt"] = null;
}
Object.assign(this, $$source);
}
/**
* Creates a new DocumentMeta instance from a string or object.
*/
static createFrom($$source: any = {}): DocumentMeta {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new DocumentMeta($$parsedSource as Partial<DocumentMeta>);
}
}
/**
* EditingConfig 编辑设置配置
*/
@@ -334,6 +343,122 @@ export class EditingConfig {
}
}
/**
* Extension 单个扩展配置
*/
export class Extension {
/**
* 扩展唯一标识
*/
"id": ExtensionID;
/**
* 是否启用
*/
"enabled": boolean;
/**
* 是否为默认扩展
*/
"isDefault": boolean;
/**
* 扩展配置项
*/
"config": ExtensionConfig;
/** Creates a new Extension instance. */
constructor($$source: Partial<Extension> = {}) {
if (!("id" in $$source)) {
this["id"] = ("" as ExtensionID);
}
if (!("enabled" in $$source)) {
this["enabled"] = false;
}
if (!("isDefault" in $$source)) {
this["isDefault"] = false;
}
if (!("config" in $$source)) {
this["config"] = ({} as ExtensionConfig);
}
Object.assign(this, $$source);
}
/**
* Creates a new Extension instance from a string or object.
*/
static createFrom($$source: any = {}): Extension {
const $$createField3_0 = $$createType7;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("config" in $$parsedSource) {
$$parsedSource["config"] = $$createField3_0($$parsedSource["config"]);
}
return new Extension($$parsedSource as Partial<Extension>);
}
}
/**
* ExtensionConfig 扩展配置项
*/
export type ExtensionConfig = { [_: string]: any };
/**
* ExtensionID 扩展标识符
*/
export enum ExtensionID {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* 编辑增强扩展
* 彩虹括号
*/
ExtensionRainbowBrackets = "rainbowBrackets",
/**
* 超链接
*/
ExtensionHyperlink = "hyperlink",
/**
* 颜色选择器
*/
ExtensionColorSelector = "colorSelector",
ExtensionFold = "fold",
ExtensionTextHighlight = "textHighlight",
/**
* 选择框
*/
ExtensionCheckbox = "checkbox",
/**
* 划词翻译
*/
ExtensionTranslator = "translator",
/**
* UI增强扩展
* 小地图
*/
ExtensionMinimap = "minimap",
/**
* 工具扩展
* 搜索功能
*/
ExtensionSearch = "search",
/**
* 核心扩展
* 编辑器核心功能
*/
ExtensionEditor = "editor",
};
/**
* GeneralConfig 通用设置配置
*/
@@ -397,7 +522,7 @@ export class GeneralConfig {
* Creates a new GeneralConfig instance from a string or object.
*/
static createFrom($$source: any = {}): GeneralConfig {
const $$createField5_0 = $$createType6;
const $$createField5_0 = $$createType9;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("globalHotkey" in $$parsedSource) {
$$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]);
@@ -406,6 +531,84 @@ export class GeneralConfig {
}
}
/**
* GiteaConfig Gitea配置
*/
export class GiteaConfig {
/**
* Gitea服务器URL
*/
"baseURL": string;
/**
* 仓库所有者
*/
"owner": string;
/**
* 仓库名称
*/
"repo": string;
/** Creates a new GiteaConfig instance. */
constructor($$source: Partial<GiteaConfig> = {}) {
if (!("baseURL" in $$source)) {
this["baseURL"] = "";
}
if (!("owner" in $$source)) {
this["owner"] = "";
}
if (!("repo" in $$source)) {
this["repo"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new GiteaConfig instance from a string or object.
*/
static createFrom($$source: any = {}): GiteaConfig {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new GiteaConfig($$parsedSource as Partial<GiteaConfig>);
}
}
/**
* GithubConfig GitHub配置
*/
export class GithubConfig {
/**
* 仓库所有者
*/
"owner": string;
/**
* 仓库名称
*/
"repo": string;
/** Creates a new GithubConfig instance. */
constructor($$source: Partial<GithubConfig> = {}) {
if (!("owner" in $$source)) {
this["owner"] = "";
}
if (!("repo" in $$source)) {
this["repo"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new GithubConfig instance from a string or object.
*/
static createFrom($$source: any = {}): GithubConfig {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new GithubConfig($$parsedSource as Partial<GithubConfig>);
}
}
/**
* HotkeyCombo 热键组合定义
*/
@@ -475,9 +678,9 @@ export class KeyBinding {
"command": KeyBindingCommand;
/**
* 快捷键分类
* 所属扩展
*/
"category": KeyBindingCategory;
"extension": ExtensionID;
/**
* 快捷键组合(如 "Mod-f", "Ctrl-Shift-p"
@@ -499,8 +702,8 @@ export class KeyBinding {
if (!("command" in $$source)) {
this["command"] = ("" as KeyBindingCommand);
}
if (!("category" in $$source)) {
this["category"] = ("" as KeyBindingCategory);
if (!("extension" in $$source)) {
this["extension"] = ("" as ExtensionID);
}
if (!("key" in $$source)) {
this["key"] = "";
@@ -524,41 +727,6 @@ export class KeyBinding {
}
}
/**
* KeyBindingCategory 快捷键分类
*/
export enum KeyBindingCategory {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* 搜索相关
*/
CategorySearch = "search",
/**
* 编辑相关
*/
CategoryEdit = "edit",
/**
* 代码块相关
*/
CategoryCodeBlock = "block",
/**
* 历史记录相关
*/
CategoryHistory = "history",
/**
* 代码折叠相关
*/
CategoryFold = "fold",
};
/**
* KeyBindingCommand 快捷键命令
*/
@@ -569,7 +737,7 @@ export enum KeyBindingCommand {
$zero = "",
/**
* 搜索相关
* 搜索扩展相关
* 显示搜索
*/
ShowSearchCommand = "showSearch",
@@ -605,7 +773,7 @@ export enum KeyBindingCommand {
SearchReplaceAllCommand = "searchReplaceAll",
/**
* 代码块相关
* 代码块扩展相关
* 块内选择全部
*/
BlockSelectAllCommand = "blockSelectAll",
@@ -701,28 +869,7 @@ export enum KeyBindingCommand {
BlockPasteCommand = "blockPaste",
/**
* 历史记录相关
* 撤销
*/
HistoryUndoCommand = "historyUndo",
/**
* 重做
*/
HistoryRedoCommand = "historyRedo",
/**
* 撤销选择
*/
HistoryUndoSelectionCommand = "historyUndoSelection",
/**
* 重做选择
*/
HistoryRedoSelectionCommand = "historyRedoSelection",
/**
* 代码折叠相关
* 代码折叠扩展相关
* 折叠代码
*/
FoldCodeCommand = "foldCode",
@@ -743,7 +890,7 @@ export enum KeyBindingCommand {
UnfoldAllCommand = "unfoldAll",
/**
* 编辑相关
* 通用编辑扩展相关
* 光标按语法左移
*/
CursorSyntaxLeftCommand = "cursorSyntaxLeft",
@@ -842,86 +989,35 @@ export enum KeyBindingCommand {
* 向前删除组
*/
DeleteGroupForwardCommand = "deleteGroupForward",
/**
* 历史记录扩展相关
* 撤销
*/
HistoryUndoCommand = "historyUndo",
/**
* 重做
*/
HistoryRedoCommand = "historyRedo",
/**
* 撤销选择
*/
HistoryUndoSelectionCommand = "historyUndoSelection",
/**
* 重做选择
*/
HistoryRedoSelectionCommand = "historyRedoSelection",
/**
* 文本高亮扩展相关
* 切换文本高亮
*/
TextHighlightToggleCommand = "textHighlightToggle",
};
/**
* KeyBindingConfig 快捷键配置
*/
export class KeyBindingConfig {
/**
* 快捷键列表
*/
"keyBindings": KeyBinding[];
/**
* 配置元数据
*/
"metadata": KeyBindingMetadata;
/** Creates a new KeyBindingConfig instance. */
constructor($$source: Partial<KeyBindingConfig> = {}) {
if (!("keyBindings" in $$source)) {
this["keyBindings"] = [];
}
if (!("metadata" in $$source)) {
this["metadata"] = (new KeyBindingMetadata());
}
Object.assign(this, $$source);
}
/**
* Creates a new KeyBindingConfig instance from a string or object.
*/
static createFrom($$source: any = {}): KeyBindingConfig {
const $$createField0_0 = $$createType8;
const $$createField1_0 = $$createType9;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("keyBindings" in $$parsedSource) {
$$parsedSource["keyBindings"] = $$createField0_0($$parsedSource["keyBindings"]);
}
if ("metadata" in $$parsedSource) {
$$parsedSource["metadata"] = $$createField1_0($$parsedSource["metadata"]);
}
return new KeyBindingConfig($$parsedSource as Partial<KeyBindingConfig>);
}
}
/**
* KeyBindingMetadata 快捷键配置元数据
*/
export class KeyBindingMetadata {
/**
* 配置版本
*/
"version": string;
/**
* 最后更新时间
*/
"lastUpdated": string;
/** Creates a new KeyBindingMetadata instance. */
constructor($$source: Partial<KeyBindingMetadata> = {}) {
if (!("version" in $$source)) {
this["version"] = "";
}
if (!("lastUpdated" in $$source)) {
this["lastUpdated"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new KeyBindingMetadata instance from a string or object.
*/
static createFrom($$source: any = {}): KeyBindingMetadata {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new KeyBindingMetadata($$parsedSource as Partial<KeyBindingMetadata>);
}
}
/**
* LanguageType 语言类型定义
*/
@@ -987,6 +1083,234 @@ export enum TabType {
TabTypeTab = "tab",
};
/**
* ThemeColorConfig 主题颜色配置
*/
export class ThemeColorConfig {
/**
* 基础色调
* 主背景色
*/
"background": string;
/**
* 次要背景色
*/
"backgroundSecondary": string;
/**
* 面板背景
*/
"surface": string;
/**
* 主文本色
*/
"foreground": string;
/**
* 次要文本色
*/
"foregroundSecondary": string;
/**
* 语法高亮
* 注释色
*/
"comment": string;
/**
* 关键字
*/
"keyword": string;
/**
* 字符串
*/
"string": string;
/**
* 函数名
*/
"function": string;
/**
* 数字
*/
"number": string;
/**
* 操作符
*/
"operator": string;
/**
* 变量
*/
"variable": string;
/**
* 类型
*/
"type": string;
/**
* 界面元素
* 光标
*/
"cursor": string;
/**
* 选中背景
*/
"selection": string;
/**
* 失焦选中背景
*/
"selectionBlur": string;
/**
* 当前行高亮
*/
"activeLine": string;
/**
* 行号
*/
"lineNumber": string;
/**
* 活动行号
*/
"activeLineNumber": string;
/**
* 边框分割线
* 边框色
*/
"borderColor": string;
/**
* 浅色边框
*/
"borderLight": string;
/**
* 搜索匹配
* 搜索匹配
*/
"searchMatch": string;
/**
* 匹配括号
*/
"matchingBracket": string;
/** Creates a new ThemeColorConfig instance. */
constructor($$source: Partial<ThemeColorConfig> = {}) {
if (!("background" in $$source)) {
this["background"] = "";
}
if (!("backgroundSecondary" in $$source)) {
this["backgroundSecondary"] = "";
}
if (!("surface" in $$source)) {
this["surface"] = "";
}
if (!("foreground" in $$source)) {
this["foreground"] = "";
}
if (!("foregroundSecondary" in $$source)) {
this["foregroundSecondary"] = "";
}
if (!("comment" in $$source)) {
this["comment"] = "";
}
if (!("keyword" in $$source)) {
this["keyword"] = "";
}
if (!("string" in $$source)) {
this["string"] = "";
}
if (!("function" in $$source)) {
this["function"] = "";
}
if (!("number" in $$source)) {
this["number"] = "";
}
if (!("operator" in $$source)) {
this["operator"] = "";
}
if (!("variable" in $$source)) {
this["variable"] = "";
}
if (!("type" in $$source)) {
this["type"] = "";
}
if (!("cursor" in $$source)) {
this["cursor"] = "";
}
if (!("selection" in $$source)) {
this["selection"] = "";
}
if (!("selectionBlur" in $$source)) {
this["selectionBlur"] = "";
}
if (!("activeLine" in $$source)) {
this["activeLine"] = "";
}
if (!("lineNumber" in $$source)) {
this["lineNumber"] = "";
}
if (!("activeLineNumber" in $$source)) {
this["activeLineNumber"] = "";
}
if (!("borderColor" in $$source)) {
this["borderColor"] = "";
}
if (!("borderLight" in $$source)) {
this["borderLight"] = "";
}
if (!("searchMatch" in $$source)) {
this["searchMatch"] = "";
}
if (!("matchingBracket" in $$source)) {
this["matchingBracket"] = "";
}
Object.assign(this, $$source);
}
/**
* Creates a new ThemeColorConfig instance from a string or object.
*/
static createFrom($$source: any = {}): ThemeColorConfig {
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
return new ThemeColorConfig($$parsedSource as Partial<ThemeColorConfig>);
}
}
/**
* UpdateSourceType 更新源类型
*/
export enum UpdateSourceType {
/**
* The Go zero value for the underlying type of the enum.
*/
$zero = "",
/**
* UpdateSourceGithub GitHub更新源
*/
UpdateSourceGithub = "github",
/**
* UpdateSourceGitea Gitea更新源
*/
UpdateSourceGitea = "gitea",
};
/**
* UpdatesConfig 更新设置配置
*/
@@ -1002,9 +1326,34 @@ export class UpdatesConfig {
"autoUpdate": boolean;
/**
* 是否启用测试版
* 主要更新源
*/
"betaChannel": boolean;
"primarySource": UpdateSourceType;
/**
* 备用更新源
*/
"backupSource": UpdateSourceType;
/**
* 更新前是否备份
*/
"backupBeforeUpdate": boolean;
/**
* 更新超时时间(秒)
*/
"updateTimeout": number;
/**
* GitHub配置
*/
"github": GithubConfig;
/**
* Gitea配置
*/
"gitea": GiteaConfig;
/** Creates a new UpdatesConfig instance. */
constructor($$source: Partial<UpdatesConfig> = {}) {
@@ -1014,8 +1363,23 @@ export class UpdatesConfig {
if (!("autoUpdate" in $$source)) {
this["autoUpdate"] = false;
}
if (!("betaChannel" in $$source)) {
this["betaChannel"] = false;
if (!("primarySource" in $$source)) {
this["primarySource"] = ("" as UpdateSourceType);
}
if (!("backupSource" in $$source)) {
this["backupSource"] = ("" as UpdateSourceType);
}
if (!("backupBeforeUpdate" in $$source)) {
this["backupBeforeUpdate"] = false;
}
if (!("updateTimeout" in $$source)) {
this["updateTimeout"] = 0;
}
if (!("github" in $$source)) {
this["github"] = (new GithubConfig());
}
if (!("gitea" in $$source)) {
this["gitea"] = (new GiteaConfig());
}
Object.assign(this, $$source);
@@ -1025,7 +1389,15 @@ export class UpdatesConfig {
* Creates a new UpdatesConfig instance from a string or object.
*/
static createFrom($$source: any = {}): UpdatesConfig {
const $$createField6_0 = $$createType10;
const $$createField7_0 = $$createType11;
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
if ("github" in $$parsedSource) {
$$parsedSource["github"] = $$createField6_0($$parsedSource["github"]);
}
if ("gitea" in $$parsedSource) {
$$parsedSource["gitea"] = $$createField7_0($$parsedSource["gitea"]);
}
return new UpdatesConfig($$parsedSource as Partial<UpdatesConfig>);
}
}
@@ -1036,8 +1408,15 @@ const $$createType1 = EditingConfig.createFrom;
const $$createType2 = AppearanceConfig.createFrom;
const $$createType3 = UpdatesConfig.createFrom;
const $$createType4 = ConfigMetadata.createFrom;
const $$createType5 = DocumentMeta.createFrom;
const $$createType6 = HotkeyCombo.createFrom;
const $$createType7 = KeyBinding.createFrom;
const $$createType8 = $Create.Array($$createType7);
const $$createType9 = KeyBindingMetadata.createFrom;
const $$createType5 = CustomThemeConfig.createFrom;
const $$createType6 = ThemeColorConfig.createFrom;
var $$createType7 = (function $$initCreateType7(...args): any {
if ($$createType7 === $$initCreateType7) {
$$createType7 = $$createType8;
}
return $$createType7(...args);
});
const $$createType8 = $Create.Map($Create.Any, $Create.Any);
const $$createType9 = HotkeyCombo.createFrom;
const $$createType10 = GithubConfig.createFrom;
const $$createType11 = GiteaConfig.createFrom;

View File

@@ -0,0 +1,39 @@
// 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;
}
/**
* 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

@@ -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,90 @@ 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;
}
/**
* RestoreDocument restores a deleted document
*/
export function RestoreDocument(id: number): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(784200778, id) as any;
return $resultPromise;
}
/**
* ServiceShutdown 关闭服务
* ServiceStartup initializes the service when the application starts
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(638578044) as any;
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1474135487, options) as any;
return $resultPromise;
}
/**
* UpdateActiveDocumentContent 更新文档内容
* UpdateDocumentContent updates the content of a document
*/
export function UpdateActiveDocumentContent(content: string): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1486276638, content) as any;
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,24 +2,34 @@
// This file is automatically generated. DO NOT EDIT
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 {
ConfigService,
DatabaseService,
DialogService,
DocumentService,
ExtensionService,
HotkeyService,
KeyBindingService,
MigrationService,
SelfUpdateService,
StartupService,
SystemService,
TrayService
TranslationService,
TrayService,
WindowService
};
export * from "./models.js";

View File

@@ -10,6 +10,9 @@
// @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";
@@ -27,27 +30,13 @@ export function GetAllKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel()
}
/**
* GetKeyBindingConfig 获取完整快捷键配置
* ServiceStartup 启动时调用
*/
export function GetKeyBindingConfig(): Promise<models$0.KeyBindingConfig | null> & { cancel(): void } {
let $resultPromise = $Call.ByID(3804318356) as any;
let $typingPromise = $resultPromise.then(($result: any) => {
return $$createType3($result);
}) as any;
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
return $typingPromise;
}
/**
* ServiceShutdown 关闭服务
*/
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
let $resultPromise = $Call.ByID(1610182855) as any;
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);
const $$createType2 = models$0.KeyBindingConfig.createFrom;
const $$createType3 = $Create.Nullable($$createType2);

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 内存统计信息
*/
@@ -114,3 +118,126 @@ export enum MigrationStatus {
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,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

@@ -9,7 +9,9 @@ export {}
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']

File diff suppressed because it is too large Load Diff

View File

@@ -15,65 +15,68 @@
"@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.1",
"@codemirror/lang-lezer": "^6.0.2",
"@codemirror/lang-liquid": "^6.2.3",
"@codemirror/lang-markdown": "^6.3.3",
"@codemirror/lang-php": "^6.0.1",
"@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.2",
"@codemirror/view": "^6.38.0",
"@lezer/highlight": "^1.2.1",
"@lezer/lr": "^1.4.2",
"@types/uuid": "^10.0.0",
"@vueuse/core": "^13.3.0",
"codemirror": "^6.0.1",
"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",
"prettier": "^3.5.3",
"pinia-plugin-persistedstate": "^4.4.1",
"prettier": "^3.6.2",
"remarkable": "^2.0.1",
"sass": "^1.89.2",
"uuid": "^11.1.0",
"vue": "^3.5.17",
"vue-i18n": "^11.1.6",
"vue-i18n": "^11.1.9",
"vue-pick-colors": "^1.8.0",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@eslint/js": "^9.29.0",
"@lezer/generator": "^1.7.3",
"@types/node": "^24.0.3",
"@vitejs/plugin-vue": "^5.2.4",
"@eslint/js": "^9.30.1",
"@lezer/generator": "^1.8.0",
"@types/lodash": "^4.17.20",
"@types/node": "^24.0.12",
"@types/remarkable": "^2.0.8",
"@vitejs/plugin-vue": "^6.0.0",
"@wailsio/runtime": "latest",
"eslint": "^9.29.0",
"eslint-plugin-vue": "^10.2.0",
"globals": "^16.2.0",
"eslint": "^9.30.1",
"eslint-plugin-vue": "^10.3.0",
"globals": "^16.3.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.34.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.36.0",
"unplugin-vue-components": "^28.8.0",
"vite": "^7.0.3",
"vue-eslint-parser": "^10.2.0",
"vue-tsc": "^3.0.1"
}
}

View File

@@ -4,12 +4,14 @@ 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 WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
const configStore = useConfigStore();
const systemStore = useSystemStore();
const keybindingStore = useKeybindingStore();
const themeStore = useThemeStore();
const updateStore = useUpdateStore();
// 应用启动时加载配置和初始化系统信息
onMounted(async () => {
@@ -23,6 +25,9 @@ onMounted(async () => {
// 初始化语言和主题
await configStore.initializeLanguage();
themeStore.initializeTheme();
// 启动时检查更新
await updateStore.checkOnStartup();
});
</script>

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; // 线条也从细到粗
// 使用贝塞尔曲线平滑连接
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;
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 (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

@@ -1,10 +1,10 @@
<template>
<div class="linux-titlebar" style="--wails-draggable:drag" @contextmenu.prevent @mouseenter="checkMaximizedState" @mouseup="checkMaximizedState">
<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">voidraft</div>
<div class="titlebar-title">{{ titleText }}</div>
</div>
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
@@ -46,12 +46,15 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
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 {
@@ -96,6 +99,12 @@ const checkMaximizedState = async () => {
}
};
// 计算标题文本
const titleText = computed(() => {
const currentDoc = documentStore.currentDocument;
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
});
onMounted(async () => {
await checkMaximizedState();

View File

@@ -44,19 +44,22 @@
</div>
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
<div class="titlebar-title">voidraft</div>
<div class="titlebar-title">{{ titleText }}</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
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 {
@@ -101,6 +104,12 @@ const checkMaximizedState = async () => {
}
};
// 计算标题文本
const titleText = computed(() => {
const currentDoc = documentStore.currentDocument;
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
});
onMounted(async () => {
await checkMaximizedState();

View File

@@ -1,11 +1,10 @@
<template>
<div class="windows-titlebar" style="--wails-draggable:drag" @contextmenu.prevent @mouseenter="checkMaximizedState"
@mouseup="checkMaximizedState">
<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">voidraft</div>
<div class="titlebar-title">{{ titleText }}</div>
</div>
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
@@ -40,13 +39,22 @@
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();

View File

@@ -505,11 +505,12 @@ const scrollToCurrentLanguage = () => {
}
&.active {
background-color: rgba(181, 206, 168, 0.1);
color: #b5cea8;
background-color: var(--selection-bg);
color: var(--selection-text);
.language-alias {
color: rgba(181, 206, 168, 0.7);
color: var(--selection-text);
opacity: 0.7;
}
}

View File

@@ -0,0 +1,753 @@
<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 {
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,30 +1,38 @@
<script setup lang="ts">
import {useI18n} from 'vue-i18n';
import {onMounted, onUnmounted, ref, watch} from 'vue';
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 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 updateStore = useUpdateStore();
const windowStore = useWindowStore();
const {t} = useI18n();
const router = useRouter();
// 当前块是否支持格式化的响应式状态
const canFormatCurrentBlock = ref(false);
// 窗口置顶状态管理(仅当前窗口,不同步到配置文件)
const isCurrentWindowOnTop = ref(false);
// 设置窗口置顶
const setWindowAlwaysOnTop = async (isTop: boolean) => {
await runtime.Window.SetAlwaysOnTop(isTop);
};
// 切换窗口置顶
const toggleAlwaysOnTop = async () => {
await configStore.toggleAlwaysOnTop();
// 使用Window.SetAlwaysOnTop方法设置窗口置顶状态
await runtime.Window.SetAlwaysOnTop(configStore.config.general.alwaysOnTop);
isCurrentWindowOnTop.value = !isCurrentWindowOnTop.value;
await runtime.Window.SetAlwaysOnTop(isCurrentWindowOnTop.value);
};
// 跳转到设置页面
@@ -32,105 +40,133 @@ const goToSettings = () => {
router.push('/settings');
};
// 执行格式化
const formatCurrentBlock = () => {
if (!canFormatCurrentBlock.value || !editorStore.editorView) return;
formatBlockContent(editorStore.editorView);
};
// 当前块是否支持格式化的响应式状态
const canFormatCurrentBlock = ref(false);
// 更新格式化按钮状态
// 格式化按钮状态更新
const updateFormatButtonState = () => {
if (!editorStore.editorView) {
// 安全检查
const view = editorStore.editorView;
if (!view) {
canFormatCurrentBlock.value = false;
return;
}
try {
const state = editorStore.editorView.state;
// 获取活动块和语言信息
const state = view.state;
const activeBlock = getActiveNoteBlock(state as any);
if (!activeBlock) {
canFormatCurrentBlock.value = false;
return;
}
const language = getLanguage(activeBlock.language.name as any);
canFormatCurrentBlock.value = !!(language && language.prettier);
// 检查块和语言格式化支持
canFormatCurrentBlock.value = !!(
activeBlock &&
getLanguage(activeBlock.language.name as any)?.prettier
);
} catch (error) {
console.warn('Error checking format capability:', error);
canFormatCurrentBlock.value = false;
}
};
// 编辑器事件监听器引用,用于清理
let editorEventListeners: (() => void)[] = [];
// 创建带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, oldView) => {
// 清理旧监听器
editorEventListeners.forEach(cleanup => cleanup());
editorEventListeners = [];
() => editorStore.editorView,
(newView) => {
// 清理旧监听器
cleanupListeners.forEach(cleanup => cleanup());
cleanupListeners = [];
if (newView) {
updateFormatButtonState();
// 添加点击监听器(用于检测光标位置变化)
const clickListener = () => {
setTimeout(updateFormatButtonState, 0);
};
newView.dom.addEventListener('click', clickListener);
editorEventListeners.push(() => newView.dom.removeEventListener('click', clickListener));
// 添加键盘监听器(用于检测内容和光标变化)
const keyupListener = () => {
setTimeout(updateFormatButtonState, 0);
};
newView.dom.addEventListener('keyup', keyupListener);
editorEventListeners.push(() => newView.dom.removeEventListener('keyup', keyupListener));
} else {
canFormatCurrentBlock.value = false;
}
},
{immediate: true}
if (newView) {
// 初始更新状态
updateFormatButtonState();
// 设置新监听器
cleanupListeners = setupEditorListeners(newView);
} else {
canFormatCurrentBlock.value = false;
}
},
{ immediate: true }
);
// 定期更新格式化按钮状态(作为备用机制)
let formatButtonUpdateTimer: number | null = null;
// 组件生命周期
const isLoaded = ref(false);
onMounted(() => {
isLoaded.value = true;
// 降低定时器频率,主要作为备用机制
formatButtonUpdateTimer = setInterval(updateFormatButtonState, 2000) as any;
// 首次更新格式化状态
updateFormatButtonState();
});
onUnmounted(() => {
// 清理定时
if (formatButtonUpdateTimer) {
clearInterval(formatButtonUpdateTimer);
formatButtonUpdateTimer = null;
}
// 清理编辑器事件监听器
editorEventListeners.forEach(cleanup => cleanup());
editorEventListeners = [];
// 清理所有事件监听
cleanupListeners.forEach(cleanup => cleanup());
cleanupListeners = [];
});
// 监听置顶设置变化
watch(
() => configStore.config.general.alwaysOnTop,
async (newValue) => {
if (!isLoaded.value) return;
await runtime.Window.SetAlwaysOnTop(newValue);
}
);
// 在组件加载完成后应用置顶设置
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>
@@ -156,14 +192,18 @@ watch(isLoaded, async (newLoaded) => {
{{ configStore.config.editing.fontSize }}px
</span>
<!-- 文档选择器 -->
<DocumentSelector v-if="windowStore.isMainWindow"/>
<!-- 块语言选择器 -->
<BlockLanguageSelector/>
<!-- 格式化提示按钮 - 只在支持的语言块中显示不可点击 -->
<!-- 格式化按钮 - 支持点击操作 -->
<div
v-if="canFormatCurrentBlock"
class="format-button"
:title="t('toolbar.formatHint')"
@click="formatCurrentBlock"
>
<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">
@@ -176,10 +216,52 @@ watch(isLoaded, async (newLoaded) => {
</svg>
</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>
<!-- 窗口置顶图标按钮 -->
<div
class="pin-button"
:class="{ 'active': configStore.config.general.alwaysOnTop }"
:class="{ 'active': isCurrentWindowOnTop }"
:title="t('toolbar.alwaysOnTop')"
@click="toggleAlwaysOnTop"
>
@@ -190,7 +272,7 @@ watch(isLoaded, async (newLoaded) => {
</div>
<button class="settings-btn" :title="t('toolbar.settings')" @click="goToSettings">
<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>
@@ -240,6 +322,99 @@ watch(isLoaded, async (newLoaded) => {
cursor: help;
}
/* 更新按钮样式 */
.update-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;
/* 有更新可用状态 */
&.available {
background-color: rgba(76, 175, 80, 0.1);
animation: pulse 2s infinite;
svg {
stroke: #4caf50;
}
&:hover {
background-color: rgba(76, 175, 80, 0.2);
transform: scale(1.05);
}
}
/* 检查更新中状态 */
&.checking {
background-color: rgba(255, 193, 7, 0.1);
svg {
stroke: #ffc107;
}
}
/* 更新下载中状态 */
&.updating {
background-color: rgba(33, 150, 243, 0.1);
svg {
stroke: #2196f3;
}
}
/* 更新成功状态 */
&.success {
background-color: rgba(156, 39, 176, 0.1);
svg {
stroke: #9c27b0;
}
}
/* 旋转动画 */
.rotating {
animation: rotate 1.5s linear infinite;
}
/* 脉冲动画 */
.pulsing {
animation: pulse-strong 1.2s ease-in-out infinite;
}
@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 {
@@ -276,6 +451,7 @@ watch(isLoaded, async (newLoaded) => {
.format-button {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
@@ -285,20 +461,10 @@ watch(isLoaded, async (newLoaded) => {
border-radius: 3px;
transition: all 0.2s ease;
//&:not(.disabled) {
// cursor: pointer;
//
// &:hover {
// background-color: var(--border-color);
// opacity: 0.8;
// }
//}
//
//&.disabled {
// cursor: not-allowed;
// opacity: 0.5;
// background-color: rgba(128, 128, 128, 0.1);
//}
&:hover {
background-color: var(--border-color);
opacity: 0.8;
}
svg {
width: 14px;

View File

@@ -1,4 +1,5 @@
export default {
locale: 'en-US',
titlebar: {
minimize: 'Minimize',
maximize: 'Maximize',
@@ -17,7 +18,26 @@ export default {
blockLanguage: 'Block Language',
searchLanguage: 'Search language...',
noLanguageFound: 'No language found',
formatHint: 'Current block supports formatting, use Ctrl+Shift+F shortcut for formatting',
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': 'Chinese',
@@ -89,6 +109,7 @@ export default {
deleteCharForward: 'Delete character forward',
deleteGroupBackward: 'Delete group backward',
deleteGroupForward: 'Delete group forward',
textHighlightToggle: 'Toggle text highlight',
}
},
settings: {
@@ -101,9 +122,11 @@ export default {
keyBindings: 'Key Bindings',
updates: 'Updates',
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',
@@ -124,6 +147,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',
@@ -135,6 +212,83 @@ export default {
language: 'Interface Language',
systemTheme: 'System Theme',
saveOptions: 'Save Options',
autoSaveDelay: 'Auto Save Delay (ms)'
autoSaveDelay: 'Auto Save Delay (ms)',
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',
},
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: '最大化',
@@ -17,7 +18,26 @@ export default {
blockLanguage: '块语言',
searchLanguage: '搜索语言...',
noLanguageFound: '未找到匹配的语言',
formatHint: '当前区块支持格式化,使用 Ctrl+Shift+F 进行格式化',
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': '简体中文',
@@ -89,6 +109,7 @@ export default {
deleteCharForward: '向前删除字符',
deleteGroupBackward: '向后删除组',
deleteGroupForward: '向前删除组',
textHighlightToggle: '切换文本高亮',
}
},
settings: {
@@ -98,12 +119,15 @@ export default {
general: '常规',
editing: '编辑器',
appearance: '外观',
extensions: '扩展',
keyBindings: '快捷键',
updates: '更新',
reset: '重置',
apply: '应用',
cancel: '取消',
dangerZone: '危险操作',
resetAllSettings: '重置所有设置',
confirmReset: '再次点击确认重置',
confirmReset: '确认重置?',
globalHotkey: '全局键盘快捷键',
enableGlobalHotkey: '启用全局热键',
window: '窗口/应用程序',
@@ -135,6 +159,137 @@ export default {
language: '界面语言',
systemTheme: '系统主题',
saveOptions: '保存选项',
autoSaveDelay: '自动保存延迟(毫秒)'
autoSaveDelay: '自动保存延迟(毫秒)',
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: '无',
},
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

@@ -4,7 +4,10 @@ import '@/assets/styles/index.css';
import {createPinia} from 'pinia';
import i18n from './i18n';
import router from './router';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
const app = createApp(App);
app.use(pinia)
app.use(i18n);

View File

@@ -6,6 +6,7 @@ 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';
const routes: RouteRecordRaw[] = [
{
@@ -34,6 +35,11 @@ const routes: RouteRecordRaw[] = [
name: 'SettingsAppearance',
component: AppearancePage
},
{
path: 'extensions',
name: 'SettingsExtensions',
component: ExtensionsPage
},
{
path: 'key-bindings',
name: 'SettingsKeyBindings',

View File

@@ -9,6 +9,8 @@ import {
LanguageType,
SystemThemeType,
TabType,
UpdatesConfig,
UpdateSourceType,
} from '@/../bindings/voidraft/internal/models/models';
import {useI18n} from 'vue-i18n';
import {ConfigUtils} from '@/utils/configUtils';
@@ -42,6 +44,10 @@ type AppearanceConfigKeyMap = {
readonly [K in keyof AppearanceConfig]: string;
};
type UpdatesConfigKeyMap = {
readonly [K in keyof UpdatesConfig]: string;
};
type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight';
// 配置键映射
@@ -67,7 +73,19 @@ const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
language: 'appearance.language',
systemTheme: 'appearance.systemTheme'
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;
// 配置限制
@@ -78,26 +96,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 => {
@@ -140,12 +172,94 @@ const DEFAULT_CONFIG: AppConfig = {
},
appearance: {
language: LanguageType.LangZhCN,
systemTheme: SystemThemeType.SystemThemeAuto
systemTheme: SystemThemeType.SystemThemeAuto,
customTheme: {
darkTheme: {
// 基础色调
background: '#252B37',
backgroundSecondary: '#213644',
surface: '#474747',
foreground: '#9BB586',
foregroundSecondary: '#9c9c9c',
// 语法高亮
comment: '#6272a4',
keyword: '#ff79c6',
string: '#f1fa8c',
function: '#50fa7b',
number: '#bd93f9',
operator: '#ff79c6',
variable: '#8fbcbb',
type: '#8be9fd',
// 界面元素
cursor: '#fff',
selection: '#0865a9aa',
selectionBlur: '#225377aa',
activeLine: 'rgba(255,255,255,0.04)',
lineNumber: 'rgba(255,255,255, 0.15)',
activeLineNumber: 'rgba(255,255,255, 0.6)',
// 边框分割线
borderColor: '#1e222a',
borderLight: 'rgba(255,255,255, 0.1)',
// 搜索匹配
searchMatch: '#8fbcbb',
matchingBracket: 'rgba(255,255,255,0.1)'
},
lightTheme: {
// 基础色调
background: '#ffffff',
backgroundSecondary: '#f1faf1',
surface: '#f5f5f5',
foreground: '#444d56',
foregroundSecondary: '#6a737d',
// 语法高亮
comment: '#6a737d',
keyword: '#d73a49',
string: '#032f62',
function: '#005cc5',
number: '#005cc5',
operator: '#d73a49',
variable: '#24292e',
type: '#6f42c1',
// 界面元素
cursor: '#000',
selection: '#77baff8c',
selectionBlur: '#b2c2ca85',
activeLine: '#000000',
lineNumber: '#000000',
activeLineNumber: '#000000',
// 边框分割线
borderColor: '#dfdfdf',
borderLight: '#0000000C',
// 搜索匹配
searchMatch: '#005cc5',
matchingBracket: 'rgba(0,0,0,0.1)'
}
}
},
updates: {
version: "1.0.0",
autoUpdate: true,
betaChannel: false
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",
}
},
metadata: {
version: '1.0.0',
@@ -155,7 +269,7 @@ const DEFAULT_CONFIG: AppConfig = {
export const useConfigStore = defineStore('config', () => {
const {locale} = useI18n();
const {locale, t} = useI18n();
// 响应式状态
const state = reactive({
@@ -164,6 +278,9 @@ export const useConfigStore = defineStore('config', () => {
configLoaded: false
});
// 初始化FONT_OPTIONS国际化版本
const localizedFontOptions = computed(() => createFontOptions(t));
// 计算属性 - 使用工厂函数简化
const createLimitComputed = (key: NumberConfigKey) => computed(() => CONFIG_LIMITS[key]);
const limits = Object.fromEntries(
@@ -216,6 +333,21 @@ 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 initConfig = async (): Promise<void> => {
if (state.isLoading) return;
@@ -302,6 +434,51 @@ export const useConfigStore = defineStore('config', () => {
await updateAppearanceConfig('systemTheme', systemTheme);
};
// 更新自定义主题方法
const updateCustomTheme = async (themeType: 'darkTheme' | 'lightTheme', colorKey: string, colorValue: string): Promise<void> => {
// 确保配置已加载
if (!state.configLoaded && !state.isLoading) {
await initConfig();
}
try {
// 深拷贝当前配置
const customTheme = JSON.parse(JSON.stringify(state.config.appearance.customTheme));
// 更新对应主题的颜色值
customTheme[themeType][colorKey] = colorValue;
// 更新整个自定义主题配置到后端
await ConfigService.Set(APPEARANCE_CONFIG_KEY_MAP.customTheme, customTheme);
// 更新前端状态
state.config.appearance.customTheme = customTheme;
} catch (error) {
throw error;
}
};
// 设置整个自定义主题配置
const setCustomTheme = async (customTheme: any): Promise<void> => {
// 确保配置已加载
if (!state.configLoaded && !state.isLoading) {
await initConfig();
}
try {
// 更新整个自定义主题配置到后端
await ConfigService.Set(APPEARANCE_CONFIG_KEY_MAP.customTheme, customTheme);
// 更新前端状态
state.config.appearance.customTheme = customTheme;
// 确保Vue能检测到变化
state.config.appearance = { ...state.config.appearance };
} catch (error) {
throw error;
}
};
// 初始化语言设置
const initializeLanguage = async (): Promise<void> => {
try {
@@ -350,6 +527,7 @@ export const useConfigStore = defineStore('config', () => {
config: computed(() => state.config),
configLoaded: computed(() => state.configLoaded),
isLoading: computed(() => state.isLoading),
localizedFontOptions,
// 限制常量
...limits,
@@ -364,6 +542,8 @@ export const useConfigStore = defineStore('config', () => {
// 主题相关方法
setSystemTheme,
updateCustomTheme,
setCustomTheme,
// 字体大小操作
...adjusters.fontSize,
@@ -411,6 +591,9 @@ export const useConfigStore = defineStore('config', () => {
await updateGeneralConfig('startAtLogin', value);
// 再调用系统设置API
await StartupService.SetEnabled(value);
}
},
// 更新配置相关方法
setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value)
};
});

View File

@@ -1,106 +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';
const SCRATCH_DOCUMENT_ID = 1; // 默认草稿文档ID
export const useDocumentStore = defineStore('document', () => {
// 状态
const activeDocument = ref<Document | null>(null);
const isLoading = ref(false);
const isSaving = ref(false);
const lastSaved = ref<Date | null>(null);
// === 核心状态 ===
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 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);
// === UI状态 ===
const showDocumentSelector = ref(false);
const isLoading = ref(false);
// 加载文档
const loadDocument = async (): Promise<Document | null> => {
if (isLoading.value) return null;
// === 计算属性 ===
const documentList = computed(() =>
Object.values(documents.value).sort((a, b) => {
const aIndex = recentDocumentIds.value.indexOf(a.id);
const bIndex = recentDocumentIds.value.indexOf(b.id);
isLoading.value = true;
try {
const doc = await DocumentService.GetActiveDocument();
activeDocument.value = doc;
return doc;
} catch (error) {
return null;
} finally {
isLoading.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 saveDocument = async (content: string): Promise<boolean> => {
if (isSaving.value) return false;
isSaving.value = true;
try {
await DocumentService.UpdateActiveDocumentContent(content);
lastSaved.value = new Date();
// 更新本地副本
if (activeDocument.value) {
activeDocument.value.content = content;
activeDocument.value.meta.lastUpdated = lastSaved.value;
}
return true;
} catch (error) {
return false;
} finally {
isSaving.value = false;
}
};
// 强制保存文档到磁盘
const forceSaveDocument = async (): Promise<boolean> => {
if (isSaving.value) return false;
isSaving.value = true;
try {
await DocumentService.ForceSave();
lastSaved.value = new Date();
// 更新时间戳
if (activeDocument.value) {
activeDocument.value.meta.lastUpdated = lastSaved.value;
}
return true;
} catch (error) {
return false;
} finally {
isSaving.value = false;
}
};
// 初始化
const initialize = async (): Promise<void> => {
await loadDocument();
};
return {
// 状态
activeDocument,
isLoading,
isSaving,
lastSaved,
// 计算属性
documentContent,
documentTitle,
hasActiveDocument,
isSaveInProgress,
lastSavedTime,
// 方法
loadDocument,
saveDocument,
forceSaveDocument,
initialize
};
});

View File

@@ -1,20 +1,25 @@
import {defineStore} from 'pinia';
import {ref, watch} from 'vue';
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 {SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
import {DocumentService} from '@/../bindings/voidraft/internal/services';
import {ensureSyntaxTree} from "@codemirror/language"
import {createBasicSetup} from '@/views/editor/extensions/basicSetup';
import {createThemeExtension, updateEditorTheme} from '@/views/editor/extensions/themeExtension';
import {getTabExtensions, updateTabConfig} from '@/views/editor/extensions/tabExtension';
import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/extensions/fontExtension';
import {createStatsUpdateExtension} from '@/views/editor/extensions/statsExtension';
import {createAutoSavePlugin, createSaveShortcutPlugin} from '@/views/editor/extensions/autoSaveExtension';
import {createDynamicKeymapExtension} from '@/views/editor/extensions/keymap';
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;
@@ -22,84 +27,138 @@ export interface DocumentStats {
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 isEditorInitialized = ref(false);
// 编辑器容器元素
const editorContainer = ref<HTMLElement | null>(null);
// 方法
function setEditorView(view: EditorView | null) {
editorView.value = view;
}
// 编辑器加载状态
const isLoading = ref(false);
// 设置编辑器容器
function setEditorContainer(container: HTMLElement | null) {
editorContainer.value = container;
// 如果编辑器已经创建但容器改变了,需要重新挂载
if (editorView.value && container && editorView.value.dom.parentElement !== container) {
container.appendChild(editorView.value.dom);
// 重新挂载后立即滚动到底部
scrollEditorToBottom();
// 异步操作竞态条件控制
const operationSequence = ref(0);
const pendingOperations = ref(new Map<number, AbortController>());
const currentLoadingDocumentId = ref<number | null>(null);
// 自动保存设置 - 从配置动态获取
const getAutoSaveDelay = () => configStore.config.editing.autoSaveDelay;
// 生成新的操作序列号
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();
};
// 更新文档统计信息
function updateDocumentStats(stats: DocumentStats) {
documentStats.value = stats;
}
// 缓存化的语法树确保方法
const ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => {
const instance = editorCache.value.instances[documentId];
if (!instance) return;
// 应用字体大小
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 docLength = view.state.doc.length;
const content = view.state.doc.toString();
const contentHash = generateContentHash(content);
const now = new Date();
// 滚动到文档底部的辅助函数
const scrollToBottom = (view: EditorView) => {
if (!view) return;
// 检查是否需要重新构建语法树
const cache = instance.syntaxTreeCache;
const shouldRebuild = !cache ||
cache.lastDocLength !== docLength ||
cache.lastContentHash !== contentHash ||
(now.getTime() - cache.lastParsed.getTime()) > 30000; // 30秒过期
const lines = view.state.doc.lines;
if (lines > 0) {
const lastLinePos = view.state.doc.line(lines).to;
view.dispatch({
effects: EditorView.scrollIntoView(lastLinePos)
});
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 scrollEditorToBottom = () => {
if (editorView.value) {
scrollToBottom(editorView.value as any);
// 创建编辑器实例
const createEditorInstance = async (
content: string,
operationId: number,
documentId: number
): Promise<EditorView> => {
if (!editorCache.value.containerElement) {
throw new Error('Editor container not set');
}
};
// 创建编辑器
const createEditor = async (initialDoc: string = '') => {
if (isEditorInitialized.value || !editorContainer.value) return;
// 加载文档内容
await documentStore.initialize();
const docContent = documentStore.documentContent || initialDoc;
// 检查操作是否仍然有效
if (!isOperationValid(operationId, documentId)) {
throw new Error('Operation cancelled');
}
// 获取基本扩展
const basicExtensions = createBasicSetup();
@@ -109,14 +168,14 @@ export const useEditorStore = defineStore('editor', () => {
configStore.config.appearance.systemTheme || SystemThemeType.SystemThemeAuto
);
// 获取Tab相关扩展
// 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,
@@ -124,153 +183,535 @@ export const useEditorStore = defineStore('editor', () => {
fontWeight: configStore.config.editing.fontWeight
});
// 创建统计信息更新扩展
const statsExtension = createStatsUpdateExtension(
updateDocumentStats
);
// 统计扩展
const statsExtension = createStatsUpdateExtension(updateDocumentStats);
// 创建保存快捷键插件
const saveShortcutPlugin = createSaveShortcutPlugin(() => {
if (editorView.value) {
handleManualSave();
}
// 内容变化扩展
const contentChangeExtension = createContentChangePlugin();
// 代码块扩展
const codeBlockExtension = createCodeBlockExtension({
showBackground: true,
enableAutoDetection: true
});
// 创建自动保存插件
const autoSavePlugin = createAutoSavePlugin({
debounceDelay: 300, // 300毫秒的输入防抖
onSave: (success) => {
if (success) {
documentStore.lastSaved = new Date();
}
}
});
// 再次检查操作有效性
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,
themeExtension,
...basicExtensions,
themeExtension,
...tabExtensions,
fontExtension,
statsExtension,
saveShortcutPlugin,
autoSavePlugin
contentChangeExtension,
codeBlockExtension,
...dynamicExtensions
];
// 创建编辑器状态
const state = EditorState.create({
doc: docContent,
doc: content,
extensions
});
// 创建编辑器视图
const view = new EditorView({
state,
parent: editorContainer.value
state
});
// 将编辑器实例保存到store
setEditorView(view);
// 将光标定位到文档末尾并滚动到该位置
const docLength = view.state.doc.length;
view.dispatch({
selection: {anchor: docLength, head: docLength},
scrollIntoView: true
});
isEditorInitialized.value = true;
scrollToBottom(view);
ensureSyntaxTree(view.state, view.state.doc.length, 5000)
// 应用初始字体大小
applyFontSize();
return view;
};
// 重新配置编辑器
const reconfigureTabSettings = () => {
if (!editorView.value) return;
updateTabConfig(
editorView.value as EditorView,
configStore.config.editing.tabSize,
configStore.config.editing.enableTabIndent,
configStore.config.editing.tabType
// 添加编辑器到缓存
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 reconfigureFontSettings = () => {
if (!editorView.value) return;
updateFontConfig(editorView.value as EditorView, {
fontFamily: configStore.config.editing.fontFamily,
fontSize: configStore.config.editing.fontSize,
lineHeight: configStore.config.editing.lineHeight,
fontWeight: configStore.config.editing.fontWeight
// 清空所有编辑器
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 handleManualSave = async () => {
if (!editorView.value) return;
const view = editorView.value as EditorView;
const content = view.state.doc.toString();
// 先更新内容
await DocumentService.UpdateActiveDocumentContent(content);
// 然后调用强制保存方法
await documentStore.forceSaveDocument();
};
// 销毁编辑器
const destroyEditor = () => {
if (editorView.value) {
editorView.value.destroy();
editorView.value = null;
isEditorInitialized.value = false;
// 更新扩展
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();
};
// 监听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();
applyFontSize();
});
// 监听主题变化
watch(() => themeStore.currentTheme, (newTheme) => {
if (editorView.value && newTheme) {
updateEditorTheme(editorView.value as EditorView, newTheme);
// 监听文档切换
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,
isEditorInitialized,
editorContainer,
isLoading,
// 方法
setEditorContainer,
createEditor,
reconfigureTabSettings,
reconfigureFontSettings,
handleManualSave,
destroyEditor,
scrollEditorToBottom,
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

@@ -1,18 +1,37 @@
import {defineStore} from 'pinia'
import {computed, ref} from 'vue'
import {KeyBinding, KeyBindingCommand} from '@/../bindings/voidraft/internal/models/models'
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) =>
@@ -26,7 +45,7 @@ export const useKeybindingStore = defineStore('keybinding', () => {
try {
keyBindings.value = await GetAllKeyBindings()
} catch (err) {
console.error(err)
throw err
}
}
@@ -37,16 +56,31 @@ export const useKeybindingStore = defineStore('keybinding', () => {
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
hasCommand,
}
})

View File

@@ -3,98 +3,64 @@ 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>;
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 error = ref<string | null>(null);
// 计算属性
const isWindows = computed(() => environment.value?.OS === 'windows');
const isMacOS = computed(() => environment.value?.OS === 'darwin');
const isLinux = computed(() => environment.value?.OS === 'linux');
// 获取操作系统名称
const osName = computed(() => {
if (!environment.value) return 'Unknown';
return environment.value.OSInfo?.Name || environment.value.OS || 'Unknown';
});
// 获取架构信息
const architecture = computed(() => environment.value?.Arch || 'Unknown');
// 获取标题栏高度
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;
error.value = null;
try {
environment.value = await runtime.System.Environment();
} catch (err) {
error.value = 'Failed to get system environment';
environment.value = null;
} finally {
isLoading.value = false;
}
};
// 获取平台特定信息
const getPlatformInfo = () => {
return environment.value?.PlatformInfo || {};
};
// 检查是否支持某项功能(基于操作系统)
const supportsFeature = (feature: string): boolean => {
switch (feature) {
case 'systemTray':
return true; // 所有平台都支持
case 'globalHotkeys':
return !isLinux.value; // Linux 支持可能有限
case 'transparency':
return isWindows.value || isMacOS.value;
default:
return false;
}
};
return {
// 状态
environment,
isLoading,
error,
const environment = ref<SystemEnvironment | null>(null);
const isLoading = ref(false);
// 计算属性
isWindows,
isMacOS,
isLinux,
osName,
architecture,
titleBarHeight,
const isWindows = computed(() => environment.value?.OS === 'windows');
const isMacOS = computed(() => environment.value?.OS === 'darwin');
const isLinux = computed(() => environment.value?.OS === 'linux');
// 方法
initializeSystemInfo,
getPlatformInfo,
supportsFeature
};
// 获取标题栏高度
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

@@ -1,19 +1,41 @@
import { defineStore } from 'pinia';
import { computed } from 'vue';
import { SystemThemeType } from '@/../bindings/voidraft/internal/models/models';
import { useConfigStore } from './configStore';
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) => {
@@ -27,18 +49,97 @@ export const useThemeStore = defineStore('theme', () => {
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,39 +1,38 @@
<script setup lang="ts">
import {onBeforeUnmount, onMounted, ref} from 'vue';
import {useEditorStore} from '@/stores/editorStore';
import {useDocumentStore} from '@/stores/documentStore';
import {useConfigStore} from '@/stores/configStore';
import {createWheelZoomHandler} from './extensions';
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 documentStore = useDocumentStore();
const configStore = useConfigStore();
const props = defineProps({
initialDoc: {
type: String,
default: ''
}
});
const windowStore = useWindowStore();
const editorElement = ref<HTMLElement | null>(null);
// 创建滚轮缩放处理器
const wheelHandler = createWheelZoomHandler(
configStore.increaseFontSize,
configStore.decreaseFontSize
configStore.increaseFontSize,
configStore.decreaseFontSize
);
onMounted(async () => {
if (!editorElement.value) return;
// 从URL查询参数中获取documentId
const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined;
// 初始化文档存储优先使用URL参数中的文档ID
await documentStore.initialize(urlDocumentId);
// 设置编辑器容器
editorStore.setEditorContainer(editorElement.value);
// 如果编辑器还没有初始化,创建编辑器
if (!editorStore.isEditorInitialized) {
await editorStore.createEditor(props.initialDoc);
}
// 添加滚轮事件监听
editorElement.value.addEventListener('wheel', wheelHandler, {passive: false});
});
@@ -48,8 +47,9 @@ onBeforeUnmount(() => {
<template>
<div class="editor-container">
<LoadingScreen v-if="editorStore.isLoading" text="VOIDRAFT" />
<div ref="editorElement" class="editor"></div>
<Toolbar />
<Toolbar/>
</div>
</template>
@@ -60,6 +60,7 @@ onBeforeUnmount(() => {
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
.editor {
width: 100%;

View File

@@ -21,16 +21,8 @@ import {
import {history} from '@codemirror/commands';
import {highlightSelectionMatches} from '@codemirror/search';
import {autocompletion, closeBrackets, closeBracketsKeymap} from '@codemirror/autocomplete';
import {searchVisibilityField, vscodeSearch} from './vscodeSearch';
import createEditorContextMenu from '../contextMenu';
import {hyperLink} from './hyperlink';
import {color} from './colorSelector';
import {createTextHighlighter} from './textHighlightExtension';
import {minimap} from './minimap';
import {createCodeBlockExtension} from './codeblock/index';
import {foldingOnIndent} from './foldExtension'
import rainbowBrackets from "./rainbowBrackets";
import {createCodeBlastExtension} from './codeblast';
// 基本编辑器设置
export const createBasicSetup = (): Extension[] => {
return [
@@ -63,29 +55,8 @@ export const createBasicSetup = (): Extension[] => {
// 自动完成
autocompletion(),
vscodeSearch,
searchVisibilityField,
foldingOnIndent,
rainbowBrackets(),
createCodeBlastExtension({
effect: 1,
shake: true,
maxParticles: 300,
shakeIntensity: 3
}),
hyperLink,
color,
...createTextHighlighter('hl'),
minimap({
displayText: 'characters',
showOverlay: 'always',
autohide: false,
}),
createCodeBlockExtension({
showBackground: true,
enableAutoDetection: true,
}),
// 上下文菜单
createEditorContextMenu(),
// 键盘映射
keymap.of([

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

@@ -1,8 +1,9 @@
import { Extension, Compartment } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { SystemThemeType } from '@/../bindings/voidraft/internal/models/models';
import { dark } from '@/views/editor/theme/dark';
import { light } from '@/views/editor/theme/light';
import { createDarkTheme } from '@/views/editor/theme/dark';
import { createLightTheme } from '@/views/editor/theme/light';
import { useThemeStore } from '@/stores/themeStore';
// 主题区间 - 用于动态切换主题
export const themeCompartment = new Compartment();
@@ -11,6 +12,8 @@ export const themeCompartment = new Compartment();
*
*/
const getThemeExtension = (themeType: SystemThemeType): Extension => {
const themeStore = useThemeStore();
// 处理 auto 主题类型
let actualTheme: SystemThemeType = themeType;
if (themeType === SystemThemeType.SystemThemeAuto) {
@@ -19,13 +22,11 @@ const getThemeExtension = (themeType: SystemThemeType): Extension => {
: SystemThemeType.SystemThemeLight;
}
// 直接返回对应的主题扩展
switch (actualTheme) {
case SystemThemeType.SystemThemeLight:
return light;
case SystemThemeType.SystemThemeDark:
default:
return dark;
// 根据主题类型创建主题
if (actualTheme === SystemThemeType.SystemThemeLight) {
return createLightTheme(themeStore.themeColors.lightTheme);
} else {
return createDarkTheme(themeStore.themeColors.darkTheme);
}
};
@@ -45,9 +46,13 @@ export const updateEditorTheme = (view: EditorView, themeType: SystemThemeType):
return;
}
const extension = getThemeExtension(themeType);
view.dispatch({
effects: themeCompartment.reconfigure(extension)
});
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;
}

View File

@@ -0,0 +1,426 @@
/**
* 上下文菜单视图实现
* 处理菜单的创建、定位和事件绑定
* 优化为单例模式避免频繁创建和销毁DOM元素
*/
import { EditorView } from "@codemirror/view";
import { MenuItem } from "../contextMenu";
import "./contextMenu.css";
// 为Window对象添加cmSubmenus属性
declare global {
interface Window {
cmSubmenus?: Map<string, HTMLElement>;
}
}
// 菜单DOM元素缓存
let menuElement: HTMLElement | null = null;
let clickOutsideHandler: ((e: MouseEvent) => void) | null = null;
// 子菜单缓存池
let submenuPool: Map<string, HTMLElement> = new Map();
/**
* 获取或创建菜单DOM元素
*/
function getOrCreateMenuElement(): HTMLElement {
if (!menuElement) {
menuElement = document.createElement("div");
menuElement.className = "cm-context-menu";
menuElement.style.display = "none";
document.body.appendChild(menuElement);
// 阻止菜单内右键点击冒泡
menuElement.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
});
}
return menuElement;
}
/**
* 创建或获取子菜单元素
* @param id 子菜单唯一标识
*/
function getOrCreateSubmenu(id: string): HTMLElement {
if (!submenuPool.has(id)) {
const submenu = document.createElement("div");
submenu.className = "cm-context-menu cm-context-submenu";
submenu.style.display = "none";
document.body.appendChild(submenu);
submenuPool.set(id, submenu);
// 阻止子菜单点击事件冒泡
submenu.addEventListener('click', (e) => {
e.stopPropagation();
});
}
return submenuPool.get(id)!;
}
/**
* 创建菜单项DOM元素
*/
function createMenuItemElement(item: MenuItem, view: EditorView): HTMLElement {
// 创建菜单项容器
const menuItem = document.createElement("div");
menuItem.className = "cm-context-menu-item";
// 如果有子菜单,添加相应类
if (item.submenu && item.submenu.length > 0) {
menuItem.classList.add("cm-context-menu-item-with-submenu");
}
// 创建内容容器
const contentContainer = document.createElement("div");
contentContainer.className = "cm-context-menu-item-label";
// 标签文本
const label = document.createElement("span");
label.textContent = item.label;
contentContainer.appendChild(label);
menuItem.appendChild(contentContainer);
// 快捷键提示(如果有)
if (item.shortcut) {
const shortcut = document.createElement("span");
shortcut.className = "cm-context-menu-item-shortcut";
shortcut.textContent = item.shortcut;
menuItem.appendChild(shortcut);
}
// 如果有子菜单,创建或获取子菜单
if (item.submenu && item.submenu.length > 0) {
// 使用菜单项标签作为子菜单ID
const submenuId = `submenu-${item.label.replace(/\s+/g, '-').toLowerCase()}`;
const submenu = getOrCreateSubmenu(submenuId);
// 清空现有子菜单内容
while (submenu.firstChild) {
submenu.removeChild(submenu.firstChild);
}
// 添加子菜单项
item.submenu.forEach(subItem => {
const subMenuItemElement = createMenuItemElement(subItem, view);
submenu.appendChild(subMenuItemElement);
});
// 初始状态设置为隐藏
submenu.style.opacity = '0';
submenu.style.pointerEvents = 'none';
submenu.style.visibility = 'hidden';
submenu.style.display = 'block';
// 当鼠标悬停在菜单项上时,显示子菜单
menuItem.addEventListener('mouseenter', () => {
const rect = menuItem.getBoundingClientRect();
// 计算子菜单位置
submenu.style.left = `${rect.right}px`;
submenu.style.top = `${rect.top}px`;
// 检查子菜单是否会超出屏幕右侧
setTimeout(() => {
const submenuRect = submenu.getBoundingClientRect();
if (submenuRect.right > window.innerWidth) {
// 如果会超出右侧,则显示在左侧
submenu.style.left = `${rect.left - submenuRect.width}px`;
}
// 检查子菜单是否会超出屏幕底部
if (submenuRect.bottom > window.innerHeight) {
// 如果会超出底部,则向上调整
const newTop = rect.top - (submenuRect.bottom - window.innerHeight);
submenu.style.top = `${Math.max(0, newTop)}px`;
}
}, 0);
// 显示子菜单
submenu.style.opacity = '1';
submenu.style.pointerEvents = 'auto';
submenu.style.visibility = 'visible';
submenu.style.transform = 'translateX(0)';
});
// 当鼠标离开菜单项时,隐藏子菜单
menuItem.addEventListener('mouseleave', (e) => {
// 检查是否移动到子菜单上
const toElement = e.relatedTarget as HTMLElement;
if (submenu.contains(toElement)) {
return; // 如果移动到子菜单上,不隐藏
}
// 隐藏子菜单
submenu.style.opacity = '0';
submenu.style.pointerEvents = 'none';
submenu.style.transform = 'translateX(10px)';
// 延迟设置visibility以便过渡动画能够完成
setTimeout(() => {
if (submenu.style.opacity === '0') {
submenu.style.visibility = 'hidden';
}
}, 200);
});
// 当鼠标离开子菜单时,隐藏它
submenu.addEventListener('mouseleave', (e) => {
// 检查是否移动回父菜单项
const toElement = e.relatedTarget as HTMLElement;
if (menuItem.contains(toElement)) {
return; // 如果移动回父菜单项,不隐藏
}
// 隐藏子菜单
submenu.style.opacity = '0';
submenu.style.pointerEvents = 'none';
submenu.style.transform = 'translateX(10px)';
// 延迟设置visibility以便过渡动画能够完成
setTimeout(() => {
if (submenu.style.opacity === '0') {
submenu.style.visibility = 'hidden';
}
}, 200);
});
// 记录子菜单
if (!window.cmSubmenus) {
window.cmSubmenus = new Map();
}
window.cmSubmenus.set(submenuId, submenu);
}
// 点击事件仅当有command时添加
if (item.command) {
menuItem.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
// 添加点击动画效果
const ripple = document.createElement("div");
ripple.className = "cm-context-menu-item-ripple";
// 计算相对位置
const rect = menuItem.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
ripple.style.left = (x - 50) + "px";
ripple.style.top = (y - 50) + "px";
menuItem.appendChild(ripple);
// 执行点击动画
setTimeout(() => {
ripple.style.transform = "scale(1)";
ripple.style.opacity = "0";
// 动画完成后移除ripple元素
setTimeout(() => {
if (ripple.parentNode === menuItem) {
menuItem.removeChild(ripple);
}
}, 300);
}, 10);
// 执行命令
item.command!(view);
// 隐藏菜单
hideContextMenu();
});
}
return menuItem;
}
/**
* 创建分隔线
*/
function createDivider(): HTMLElement {
const divider = document.createElement("div");
divider.className = "cm-context-menu-divider";
return divider;
}
/**
* 添加菜单组
* @param menuElement 菜单元素
* @param title 菜单组标题
* @param items 菜单项
* @param view 编辑器视图
*/
function addMenuGroup(menuElement: HTMLElement, title: string | null, items: MenuItem[], view: EditorView): void {
// 如果有标题,添加组标题
if (title) {
const groupTitle = document.createElement("div");
groupTitle.className = "cm-context-menu-group-title";
groupTitle.textContent = title;
menuElement.appendChild(groupTitle);
}
// 添加菜单项
items.forEach(item => {
const menuItemElement = createMenuItemElement(item, view);
menuElement.appendChild(menuItemElement);
});
}
/**
* 显示上下文菜单
*/
export function showContextMenu(view: EditorView, clientX: number, clientY: number, items: MenuItem[]): void {
// 获取或创建菜单元素
const menu = getOrCreateMenuElement();
// 如果已经有菜单显示,先隐藏所有子菜单
hideAllSubmenus();
// 清空现有菜单项
while (menu.firstChild) {
menu.removeChild(menu.firstChild);
}
// 添加主菜单项
items.forEach(item => {
const menuItemElement = createMenuItemElement(item, view);
menu.appendChild(menuItemElement);
});
// 显示菜单
menu.style.display = "block";
// 定位菜单
positionMenu(menu, clientX, clientY);
// 添加点击外部关闭事件
if (clickOutsideHandler) {
document.removeEventListener("click", clickOutsideHandler, true);
}
clickOutsideHandler = (e: MouseEvent) => {
// 检查点击是否在菜单外
if (menu && !menu.contains(e.target as Node)) {
let isInSubmenu = false;
// 检查是否点击在子菜单内
if (window.cmSubmenus) {
window.cmSubmenus.forEach((submenu) => {
if (submenu.contains(e.target as Node)) {
isInSubmenu = true;
}
});
}
if (!isInSubmenu) {
hideContextMenu();
}
}
};
// 使用捕获阶段确保事件被处理
document.addEventListener("click", clickOutsideHandler, true);
// ESC键关闭
document.addEventListener("keydown", handleKeyDown);
// 触发显示动画
setTimeout(() => {
if (menu) {
menu.classList.add("show");
}
}, 10);
}
/**
* 隐藏所有子菜单
*/
function hideAllSubmenus(): void {
if (window.cmSubmenus) {
window.cmSubmenus.forEach((submenu) => {
submenu.style.opacity = '0';
submenu.style.pointerEvents = 'none';
submenu.style.visibility = 'hidden';
submenu.style.transform = 'translateX(10px)';
});
}
}
/**
* 处理键盘事件
*/
function handleKeyDown(e: KeyboardEvent): void {
if (e.key === "Escape") {
hideContextMenu();
document.removeEventListener("keydown", handleKeyDown);
}
}
/**
* 隐藏上下文菜单
*/
export function hideContextMenu(): void {
// 隐藏所有子菜单
hideAllSubmenus();
if (menuElement) {
// 添加淡出动画
menuElement.classList.remove("show");
menuElement.classList.add("hide");
// 等待动画完成后隐藏不移除DOM元素
setTimeout(() => {
if (menuElement) {
menuElement.style.display = "none";
menuElement.classList.remove("hide");
}
}, 150);
}
if (clickOutsideHandler) {
document.removeEventListener("click", clickOutsideHandler, true);
clickOutsideHandler = null;
}
document.removeEventListener("keydown", handleKeyDown);
}
/**
* 定位菜单元素
*/
function positionMenu(menu: HTMLElement, clientX: number, clientY: number): void {
// 获取窗口尺寸
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// 初始位置设置
let left = clientX;
let top = clientY;
// 确保菜单在视窗内
setTimeout(() => {
// 计算菜单尺寸
const menuWidth = menu.offsetWidth;
const menuHeight = menu.offsetHeight;
// 确保菜单不会超出右侧边界
if (left + menuWidth > windowWidth) {
left = windowWidth - menuWidth - 5;
}
// 确保菜单不会超出底部边界
if (top + menuHeight > windowHeight) {
top = windowHeight - menuHeight - 5;
}
// 应用位置
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
}, 0);
}

View File

@@ -0,0 +1,229 @@
/**
* 编辑器上下文菜单实现
* 提供基本的复制、剪切、粘贴等操作,支持动态快捷键显示
*/
import { EditorView } from "@codemirror/view";
import { Extension } from "@codemirror/state";
import { copyCommand, cutCommand, pasteCommand } from "../extensions/codeblock/copyPaste";
import { KeyBindingCommand } from "@/../bindings/voidraft/internal/models/models";
import { useKeybindingStore } from "@/stores/keybindingStore";
import {
undo, redo
} from "@codemirror/commands";
import {
deleteBlock, formatCurrentBlock,
addNewBlockAfterCurrent, addNewBlockAfterLast, addNewBlockBeforeCurrent
} from "../extensions/codeblock/commands";
import { commandRegistry } from "@/views/editor/keymap";
import i18n from "@/i18n";
import {useSystemStore} from "@/stores/systemStore";
/**
* 菜单项类型定义
*/
export interface MenuItem {
/** 菜单项显示文本 */
label: string;
/** 点击时执行的命令 (如果有子菜单可以为null) */
command?: (view: EditorView) => boolean;
/** 快捷键提示文本 (可选) */
shortcut?: string;
/** 子菜单项 (可选) */
submenu?: MenuItem[];
}
// 导入相关功能
import { showContextMenu } from "./contextMenuView";
/**
* 获取翻译文本
* @param key 翻译键
* @returns 翻译后的文本
*/
function t(key: string): string {
return i18n.global.t(key);
}
/**
* 获取快捷键显示文本
* @param command 命令ID
* @returns 快捷键显示文本
*/
function getShortcutText(command: KeyBindingCommand): string {
try {
const keybindingStore = useKeybindingStore();
// 如果找到该命令的快捷键配置
const binding = keybindingStore.keyBindings.find(kb =>
kb.command === command && kb.enabled
);
if (binding && binding.key) {
// 格式化快捷键显示
return formatKeyBinding(binding.key);
}
} catch (error) {
console.warn("An error occurred while getting the shortcut:", error);
}
return "";
}
/**
* 格式化快捷键显示
* @param keyBinding 快捷键字符串
* @returns 格式化后的显示文本
*/
function formatKeyBinding(keyBinding: string): string {
// 获取系统信息
const systemStore = useSystemStore();
const isMac = systemStore.isMacOS;
// 替换修饰键名称为更友好的显示
return keyBinding
.replace("Mod", isMac ? "⌘" : "Ctrl")
.replace("Shift", isMac ? "⇧" : "Shift")
.replace("Alt", isMac ? "⌥" : "Alt")
.replace("Ctrl", isMac ? "⌃" : "Ctrl")
.replace(/-/g, " + ");
}
/**
* 从命令注册表获取命令处理程序和翻译键
* @param command 命令ID
* @returns 命令处理程序和翻译键
*/
function getCommandInfo(command: KeyBindingCommand): { handler: (view: EditorView) => boolean, descriptionKey: string } | undefined {
return commandRegistry[command];
}
/**
* 创建编辑菜单项
*/
function createEditItems(): MenuItem[] {
return [
{
label: t("keybindings.commands.blockCopy"),
command: copyCommand,
shortcut: getShortcutText(KeyBindingCommand.BlockCopyCommand)
},
{
label: t("keybindings.commands.blockCut"),
command: cutCommand,
shortcut: getShortcutText(KeyBindingCommand.BlockCutCommand)
},
{
label: t("keybindings.commands.blockPaste"),
command: pasteCommand,
shortcut: getShortcutText(KeyBindingCommand.BlockPasteCommand)
}
];
}
/**
* 创建历史操作菜单项
*/
function createHistoryItems(): MenuItem[] {
return [
{
label: t("keybindings.commands.historyUndo"),
command: undo,
shortcut: getShortcutText(KeyBindingCommand.HistoryUndoCommand)
},
{
label: t("keybindings.commands.historyRedo"),
command: redo,
shortcut: getShortcutText(KeyBindingCommand.HistoryRedoCommand)
}
];
}
/**
* 创建代码块相关菜单项
*/
function createCodeBlockItems(): MenuItem[] {
const defaultOptions = { defaultBlockToken: 'text', defaultBlockAutoDetect: true };
return [
// 格式化
{
label: t("keybindings.commands.blockFormat"),
command: formatCurrentBlock,
shortcut: getShortcutText(KeyBindingCommand.BlockFormatCommand)
},
// 删除
{
label: t("keybindings.commands.blockDelete"),
command: deleteBlock(defaultOptions),
shortcut: getShortcutText(KeyBindingCommand.BlockDeleteCommand)
},
// 在当前块后添加新块
{
label: t("keybindings.commands.blockAddAfterCurrent"),
command: addNewBlockAfterCurrent(defaultOptions),
shortcut: getShortcutText(KeyBindingCommand.BlockAddAfterCurrentCommand)
},
// 在当前块前添加新块
{
label: t("keybindings.commands.blockAddBeforeCurrent"),
command: addNewBlockBeforeCurrent(defaultOptions),
shortcut: getShortcutText(KeyBindingCommand.BlockAddBeforeCurrentCommand)
},
// 在最后添加新块
{
label: t("keybindings.commands.blockAddAfterLast"),
command: addNewBlockAfterLast(defaultOptions),
shortcut: getShortcutText(KeyBindingCommand.BlockAddAfterLastCommand)
}
];
}
/**
* 创建主菜单项
*/
function createMainMenuItems(): MenuItem[] {
// 基本编辑操作放在主菜单
const basicItems = createEditItems();
// 历史操作放在主菜单
const historyItems = createHistoryItems();
// 构建主菜单
return [
...basicItems,
...historyItems,
{
label: t("extensions.codeblock.name"),
submenu: createCodeBlockItems()
}
];
}
/**
* 创建编辑器上下文菜单
*/
export function createEditorContextMenu(): Extension {
// 为编辑器添加右键事件处理
return EditorView.domEventHandlers({
contextmenu: (event, view) => {
// 阻止默认右键菜单
event.preventDefault();
// 获取菜单项
const menuItems = createMainMenuItems();
// 显示上下文菜单
showContextMenu(view, event.clientX, event.clientY, menuItems);
return true;
}
});
}
/**
* 默认导出
*/
export default createEditorContextMenu;

View File

@@ -1,98 +0,0 @@
import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
import { DocumentService } from '../../../../bindings/voidraft/internal/services';
import { useDebounceFn } from '@vueuse/core';
// 定义自动保存配置选项
export interface AutoSaveOptions {
// 保存回调
onSave?: (success: boolean) => void;
// 内容变更延迟传递(毫秒)- 输入时不会立即发送,有一个小延迟,避免频繁调用后端
debounceDelay?: number;
}
/**
* 创建自动保存插件
*
* @param options 配置选项
* @returns EditorView.Plugin
*/
export function createAutoSavePlugin(options: AutoSaveOptions = {}) {
const {
onSave = () => {},
debounceDelay = 2000
} = options;
return ViewPlugin.fromClass(
class {
private isActive: boolean = true;
private isSaving: boolean = false;
private readonly contentUpdateFn: (view: EditorView) => void;
constructor(private view: EditorView) {
// 创建内容更新函数,简单传递内容给后端
this.contentUpdateFn = this.createDebouncedUpdateFn(debounceDelay);
}
/**
* 创建防抖的内容更新函数
*/
private createDebouncedUpdateFn(delay: number): (view: EditorView) => void {
// 使用VueUse的防抖函数创建一个新函数
return useDebounceFn(async (view: EditorView) => {
// 如果插件已不活跃或正在保存中,不发送
if (!this.isActive || this.isSaving) return;
this.isSaving = true;
const content = view.state.doc.toString();
try {
// 简单将内容传递给后端,让后端处理保存策略
await DocumentService.UpdateActiveDocumentContent(content);
onSave(true);
} catch (err) {
// 静默处理错误,不在控制台打印
onSave(false);
} finally {
this.isSaving = false;
}
}, delay);
}
update(update: ViewUpdate) {
// 如果内容没有变化,直接返回
if (!update.docChanged) return;
// 调用防抖函数
this.contentUpdateFn(this.view);
}
destroy() {
// 标记插件不再活跃
this.isActive = false;
// 静默发送最终内容,忽略错误
const content = this.view.state.doc.toString();
DocumentService.UpdateActiveDocumentContent(content).then();
}
}
);
}
/**
* 创建处理保存快捷键的插件
* @param onSave 保存回调
* @returns EditorView.Plugin
*/
export function createSaveShortcutPlugin(onSave: () => void) {
return EditorView.domEventHandlers({
keydown: (event) => {
// Ctrl+S / Cmd+S
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault();
onSave();
return true;
}
return false;
}
});
}

View File

@@ -0,0 +1,186 @@
import { EditorView, Decoration } from "@codemirror/view"
import { WidgetType } from "@codemirror/view"
import { ViewUpdate, ViewPlugin, DecorationSet } from "@codemirror/view"
import { Extension, Compartment, StateEffect } from "@codemirror/state"
// 创建字体变化效果
const fontChangeEffect = StateEffect.define<void>()
/**
* 复选框小部件类
*/
class CheckboxWidget extends WidgetType {
constructor(readonly checked: boolean) {
super()
}
eq(other: CheckboxWidget) {
return other.checked == this.checked
}
toDOM() {
let wrap = document.createElement("span")
wrap.setAttribute("aria-hidden", "true")
wrap.className = "cm-checkbox-toggle"
let box = document.createElement("input")
box.type = "checkbox"
box.checked = this.checked
box.tabIndex = -1
box.style.margin = "0"
box.style.padding = "0"
box.style.cursor = "pointer"
box.style.position = "relative"
box.style.top = "0.1em"
box.style.marginRight = "0.5em"
// 设置相对单位,让复选框跟随字体大小变化
box.style.width = "1em"
box.style.height = "1em"
wrap.appendChild(box)
return wrap
}
ignoreEvent() {
return false
}
}
/**
* 查找并创建复选框装饰
*/
function findCheckboxes(view: EditorView) {
let widgets: any = []
const doc = view.state.doc
for (let { from, to } of view.visibleRanges) {
// 使用正则表达式查找 [x] 或 [ ] 模式
const text = doc.sliceString(from, to)
const checkboxRegex = /\[([ x])\]/gi
let match
while ((match = checkboxRegex.exec(text)) !== null) {
const matchPos = from + match.index
const matchEnd = matchPos + match[0].length
// 检查前后是否有合适的空格或行首
const beforeChar = matchPos > 0 ? doc.sliceString(matchPos - 1, matchPos) : ""
const afterChar = matchEnd < doc.length ? doc.sliceString(matchEnd, matchEnd + 1) : ""
// 只在行首或空格后,且后面跟空格或行尾时才渲染
if ((beforeChar === "" || beforeChar === " " || beforeChar === "\t" || beforeChar === "\n") &&
(afterChar === "" || afterChar === " " || afterChar === "\t" || afterChar === "\n")) {
const isChecked = match[1].toLowerCase() === "x"
let deco = Decoration.replace({
widget: new CheckboxWidget(isChecked),
inclusive: false,
})
widgets.push(deco.range(matchPos, matchEnd))
}
}
}
return Decoration.set(widgets)
}
/**
* 切换复选框状态
*/
function toggleCheckbox(view: EditorView, pos: number) {
const doc = view.state.doc
// 查找当前位置附近的复选框模式
for (let offset = -3; offset <= 0; offset++) {
const checkPos = pos + offset
if (checkPos >= 0 && checkPos + 3 <= doc.length) {
const text = doc.sliceString(checkPos, checkPos + 3).toLowerCase()
let change
if (text === "[x]") {
change = { from: checkPos, to: checkPos + 3, insert: "[ ]" }
} else if (text === "[ ]") {
change = { from: checkPos, to: checkPos + 3, insert: "[x]" }
}
if (change) {
view.dispatch({ changes: change })
return true
}
}
}
return false
}
// 创建字体变化效果的便捷函数
export const triggerFontChange = (view: EditorView) => {
view.dispatch({
effects: fontChangeEffect.of(undefined)
})
}
/**
* 创建复选框扩展
*/
export function createCheckboxExtension(): Extension {
return [
// 主要的复选框插件
ViewPlugin.fromClass(class {
decorations: DecorationSet
constructor(view: EditorView) {
this.decorations = findCheckboxes(view)
}
update(update: ViewUpdate) {
// 检查是否需要重新渲染复选框
const shouldUpdate = update.docChanged ||
update.viewportChanged ||
update.geometryChanged ||
update.transactions.some(tr => tr.effects.some(e => e.is(fontChangeEffect)))
if (shouldUpdate) {
this.decorations = findCheckboxes(update.view)
}
}
}, {
decorations: v => v.decorations,
eventHandlers: {
mousedown: (e, view) => {
let target = e.target as HTMLElement
if (target.nodeName == "INPUT" && target.parentElement!.classList.contains("cm-checkbox-toggle")) {
const pos = view.posAtDOM(target)
return toggleCheckbox(view, pos)
}
}
}
}),
// 复选框样式
EditorView.theme({
".cm-checkbox-toggle": {
display: "inline-block",
verticalAlign: "baseline",
},
".cm-checkbox-toggle input[type=checkbox]": {
margin: "0",
padding: "0",
verticalAlign: "baseline",
cursor: "pointer",
// 确保复选框大小跟随字体
fontSize: "inherit",
}
})
]
}
// 默认导出
export const checkboxExtension = createCheckboxExtension()
// 导出类型和工具函数
export {
CheckboxWidget,
toggleCheckbox,
findCheckboxes
}

View File

@@ -1,368 +0,0 @@
import { Extension } from '@codemirror/state';
import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
import './styles.css';
// 粒子接口定义
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
alpha: number;
size: number;
color: number[];
theta?: number;
drag?: number;
wander?: number;
}
// 配置接口
interface CodeBlastConfig {
effect?: 1 | 2; // effect 1: 随机粒子, effect 2: 追逐粒子
shake?: boolean; // 启用震动效果
maxParticles?: number; // 最大粒子数
particleRange?: { min: number; max: number }; // 粒子大小范围
shakeIntensity?: number; // 震动强度
gravity?: number; // 重力加速度 (仅 effect: 1)
alphaFadeout?: number; // 粒子透明度衰减
velocityRange?: { // 粒子速度范围
x: [number, number]; // x轴方向速度范围
y: [number, number]; // y轴方向速度范围
};
}
class CodeBlastEffect {
private canvas: HTMLCanvasElement | null = null;
private ctx: CanvasRenderingContext2D | null = null;
private particles: Particle[] = [];
private particlePointer = 0;
private isActive = false;
private lastTime = 0;
private shakeTime = 0;
private shakeTimeMax = 0;
private animationId: number | null = null;
// 配置参数
private config: Required<CodeBlastConfig> = {
effect: 2,
shake: true,
maxParticles: 500,
particleRange: { min: 5, max: 10 },
shakeIntensity: 5,
gravity: 0.08,
alphaFadeout: 0.96,
velocityRange: {
x: [-1, 1],
y: [-3.5, -1.5]
}
};
constructor(config?: CodeBlastConfig) {
if (config) {
this.config = { ...this.config, ...config };
}
this.particles = new Array(this.config.maxParticles);
}
private getRGBComponents(element: Element): number[] {
try {
const style = getComputedStyle(element);
const color = style.color;
if (color) {
const match = color.match(/(\d+),\s*(\d+),\s*(\d+)/);
if (match) {
return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])];
}
}
} catch (e) {
console.warn('Failed to get RGB components:', e);
}
return [255, 255, 255]; // 默认白色
}
private random(min: number, max?: number): number {
if (max === undefined) {
max = min;
min = 0;
}
return min + Math.floor(Math.random() * (max - min + 1));
}
private createParticle(x: number, y: number, color: number[]): Particle {
const particle: Particle = {
x,
y: y + 10,
alpha: 1,
color,
vx: 0,
vy: 0,
size: 0
};
if (this.config.effect === 1) {
particle.size = this.random(2, 4);
particle.vx = this.config.velocityRange.x[0] +
Math.random() * (this.config.velocityRange.x[1] - this.config.velocityRange.x[0]);
particle.vy = this.config.velocityRange.y[0] +
Math.random() * (this.config.velocityRange.y[1] - this.config.velocityRange.y[0]);
} else if (this.config.effect === 2) {
particle.size = this.random(2, 8);
particle.drag = 0.92;
particle.vx = this.random(-3, 3);
particle.vy = this.random(-3, 3);
particle.wander = 0.15;
particle.theta = this.random(0, 360) * Math.PI / 180;
}
return particle;
}
private spawnParticles(view: EditorView): void {
if (!this.ctx) return;
try {
// 获取光标位置
const selection = view.state.selection.main;
const coords = view.coordsAtPos(selection.head);
if (!coords) return;
// 获取光标处的元素来确定颜色
const element = document.elementFromPoint(coords.left, coords.top);
const color = element ? this.getRGBComponents(element) : [255, 255, 255];
const numParticles = this.random(
this.config.particleRange.min,
this.config.particleRange.max
);
for (let i = 0; i < numParticles; i++) {
this.particles[this.particlePointer] = this.createParticle(
coords.left + 10,
coords.top,
color
);
this.particlePointer = (this.particlePointer + 1) % this.config.maxParticles;
}
} catch (error) {
// 如果在更新期间无法读取坐标,静默忽略
console.warn('Failed to spawn particles:', error);
}
}
private effect1(particle: Particle): void {
if (!this.ctx) return;
particle.vy += this.config.gravity;
particle.x += particle.vx;
particle.y += particle.vy;
particle.alpha *= this.config.alphaFadeout;
this.ctx.fillStyle = `rgba(${particle.color[0]}, ${particle.color[1]}, ${particle.color[2]}, ${particle.alpha})`;
this.ctx.fillRect(
Math.round(particle.x - 1),
Math.round(particle.y - 1),
particle.size,
particle.size
);
}
private effect2(particle: Particle): void {
if (!this.ctx || particle.theta === undefined || particle.drag === undefined) return;
particle.x += particle.vx;
particle.y += particle.vy;
particle.vx *= particle.drag;
particle.vy *= particle.drag;
particle.theta += this.random(-0.5, 0.5);
particle.vx += Math.sin(particle.theta) * 0.1;
particle.vy += Math.cos(particle.theta) * 0.1;
particle.size *= 0.96;
this.ctx.fillStyle = `rgba(${particle.color[0]}, ${particle.color[1]}, ${particle.color[2]}, ${particle.alpha})`;
this.ctx.beginPath();
this.ctx.arc(
Math.round(particle.x - 1),
Math.round(particle.y - 1),
particle.size,
0,
2 * Math.PI
);
this.ctx.fill();
}
private drawParticles(): void {
if (!this.ctx) return;
for (let i = 0; i < this.particles.length; i++) {
const particle = this.particles[i];
if (!particle || particle.alpha < 0.01 || particle.size <= 0.5) {
continue;
}
if (this.config.effect === 1) {
this.effect1(particle);
} else if (this.config.effect === 2) {
this.effect2(particle);
}
}
}
private shake(view: EditorView, duration: number): void {
if (!this.config.shake) return;
this.shakeTime = this.shakeTimeMax = duration;
const editorElement = view.dom;
const shakeAnimation = () => {
if (this.shakeTime <= 0) {
editorElement.style.transform = '';
return;
}
const magnitude = (this.shakeTime / this.shakeTimeMax) * this.config.shakeIntensity;
const shakeX = this.random(-magnitude, magnitude);
const shakeY = this.random(-magnitude, magnitude);
editorElement.style.transform = `translate(${shakeX}px, ${shakeY}px)`;
this.shakeTime -= 0.016; // ~60fps
requestAnimationFrame(shakeAnimation);
};
requestAnimationFrame(shakeAnimation);
}
private loop = (): void => {
if (!this.isActive || !this.ctx || !this.canvas) return;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.drawParticles();
this.animationId = requestAnimationFrame(this.loop);
};
public init(view: EditorView): void {
if (this.isActive) return;
this.isActive = true;
if (!this.canvas) {
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
if (!this.ctx) {
console.error('Failed to get canvas context');
return;
}
this.canvas.id = 'code-blast-canvas';
this.canvas.style.position = 'absolute';
this.canvas.style.top = '0';
this.canvas.style.left = '0';
this.canvas.style.zIndex = '1';
this.canvas.style.pointerEvents = 'none';
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
document.body.appendChild(this.canvas);
this.loop();
}
}
public destroy(): void {
this.isActive = false;
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
if (this.canvas) {
this.canvas.remove();
this.canvas = null;
this.ctx = null;
}
}
public onDocumentChange(view: EditorView): void {
if (this.config.shake) {
this.shake(view, 0.3);
}
// 使用 requestIdleCallback 或 setTimeout 延迟执行粒子生成
// 确保在 DOM 更新完成后再读取坐标
if (window.requestIdleCallback) {
window.requestIdleCallback(() => {
this.spawnParticles(view);
});
} else {
setTimeout(() => {
this.spawnParticles(view);
}, 16); // ~60fps
}
}
public updateCanvasSize(): void {
if (this.canvas) {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
}
}
// 节流函数
function throttle<T extends (...args: any[]) => void>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle: boolean;
return function(this: any, ...args: Parameters<T>) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 创建 CodeBlast 扩展
export function createCodeBlastExtension(config?: CodeBlastConfig): Extension {
let effect: CodeBlastEffect | null = null;
const plugin = ViewPlugin.fromClass(class {
private throttledOnChange: (view: EditorView) => void;
constructor(private view: EditorView) {
effect = new CodeBlastEffect(config);
effect.init(view);
this.throttledOnChange = throttle((view: EditorView) => {
effect?.onDocumentChange(view);
}, 100);
// 监听窗口大小变化
window.addEventListener('resize', this.handleResize);
}
update(update: ViewUpdate) {
if (update.docChanged) {
// 延迟执行,确保更新完成后再触发效果
setTimeout(() => {
this.throttledOnChange(this.view);
}, 0);
}
}
destroy() {
effect?.destroy();
effect = null;
window.removeEventListener('resize', this.handleResize);
}
private handleResize = () => {
effect?.updateCanvasSize();
};
});
return plugin;
}
// 默认导出
export const codeBlast = createCodeBlastExtension();

View File

@@ -1,52 +0,0 @@
#code-blast-canvas {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 1000;
pointer-events: none;
background: transparent;
}
/* 确保编辑器在震动时不会影响布局 */
.cm-editor {
transition: transform 0.1s ease-out;
}
/* 打字机效果的粒子样式 */
.code-blast-particle {
position: absolute;
pointer-events: none;
border-radius: 50%;
animation: particle-fade 2s ease-out forwards;
}
@keyframes particle-fade {
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.5);
}
}
/* 震动效果的缓动 */
.code-blast-shake {
animation: shake 0.3s ease-out;
}
@keyframes shake {
0%, 100% { transform: translate(0, 0); }
10% { transform: translate(-2px, -1px); }
20% { transform: translate(2px, 1px); }
30% { transform: translate(-1px, 2px); }
40% { transform: translate(1px, -2px); }
50% { transform: translate(-2px, 1px); }
60% { transform: translate(2px, -1px); }
70% { transform: translate(-1px, -2px); }
80% { transform: translate(1px, 2px); }
90% { transform: translate(-2px, -1px); }
}

View File

@@ -6,65 +6,63 @@ import { getLanguage } from "./lang-parser/languages"
import { SupportedLanguage } from "./types"
export const formatBlockContent = (view) => {
const state = view.state
if (state.readOnly)
if (!view || view.state.readOnly)
return false
const block = getActiveNoteBlock(state)
// 获取初始信息但不缓存state对象
const initialState = view.state
const block = getActiveNoteBlock(initialState)
if (!block) {
return false
}
const language = getLanguage(block.language.name as SupportedLanguage)
const blockFrom = block.content.from
const blockTo = block.content.to
const blockLanguageName = block.language.name as SupportedLanguage
const language = getLanguage(blockLanguageName)
if (!language || !language.prettier) {
return false
}
// get current cursor position
const cursorPos = state.selection.asSingle().ranges[0].head
// get block content
const content = state.sliceDoc(block.content.from, block.content.to)
// 获取初始需要的信息
const cursorPos = initialState.selection.asSingle().ranges[0].head
const content = initialState.sliceDoc(blockFrom, blockTo)
const tabSize = initialState.tabSize
let useFormat = false
if (cursorPos == block.content.from || cursorPos == block.content.to) {
useFormat = true
}
// 检查光标是否在块的开始或结束
const cursorAtEdge = cursorPos == blockFrom || cursorPos == blockTo
// 执行异步格式化,但在回调中获取最新状态
// 执行异步格式化
const performFormat = async () => {
let formattedContent
try {
if (useFormat) {
formattedContent = {
formatted: await prettier.format(content, {
parser: language.prettier!.parser,
plugins: language.prettier!.plugins,
tabWidth: state.tabSize,
}),
}
formattedContent.cursorOffset = cursorPos == block.content.from ? 0 : formattedContent.formatted.length
} else {
// formatWithCursor 有性能问题,改用简单格式化 + 光标位置计算
const formatted = await prettier.format(content, {
parser: language.prettier!.parser,
plugins: language.prettier!.plugins,
tabWidth: state.tabSize,
})
formattedContent = {
formatted: formatted,
cursorOffset: Math.min(cursorPos - block.content.from, formatted.length)
}
// 格式化代码
const formatted = await prettier.format(content, {
parser: language.prettier!.parser,
plugins: language.prettier!.plugins,
tabWidth: tabSize,
})
// 计算新光标位置
const cursorOffset = cursorAtEdge
? (cursorPos == blockFrom ? 0 : formatted.length)
: Math.min(cursorPos - blockFrom, formatted.length)
formattedContent = {
formatted,
cursorOffset
}
} catch (e) {
const hyphens = "----------------------------------------------------------------------------"
const errorMessage = (e as Error).message;
console.log(`Error when trying to format block:\n${hyphens}\n${errorMessage}\n${hyphens}`)
return false
}
try {
// 重新获取当前状态和块信息,确保状态一致
// 格式化完成后再次获取最新状态
const currentState = view.state
// 重新获取当前块的位置
const currentBlock = getActiveNoteBlock(currentState)
if (!currentBlock) {
@@ -72,17 +70,22 @@ export const formatBlockContent = (view) => {
return false
}
view.dispatch(currentState.update({
// 使用当前块的实际位置
const currentBlockFrom = currentBlock.content.from
const currentBlockTo = currentBlock.content.to
// 基于最新状态创建更新
view.dispatch({
changes: {
from: currentBlock.content.from,
to: currentBlock.content.to,
from: currentBlockFrom,
to: currentBlockTo,
insert: formattedContent.formatted,
},
selection: EditorSelection.cursor(currentBlock.content.from + Math.min(formattedContent.cursorOffset, formattedContent.formatted.length)),
}, {
userEvent: "input",
selection: EditorSelection.cursor(currentBlockFrom + Math.min(formattedContent.cursorOffset, formattedContent.formatted.length)),
scrollIntoView: true,
}))
userEvent: "input"
})
return true;
} catch (error) {
console.error('Failed to apply formatting changes:', error);

View File

@@ -8,7 +8,7 @@ import {
ViewUpdate,
} from '@codemirror/view';
import { Extension, Range } from '@codemirror/state';
import * as runtime from "@wailsio/runtime"
const pathStr = `<svg viewBox="0 0 1024 1024" width="16" height="16" fill="currentColor"><path d="M607.934444 417.856853c-6.179746-6.1777-12.766768-11.746532-19.554358-16.910135l-0.01228 0.011256c-6.986111-6.719028-16.47216-10.857279-26.930349-10.857279-21.464871 0-38.864146 17.400299-38.864146 38.864146 0 9.497305 3.411703 18.196431 9.071609 24.947182l-0.001023 0c0.001023 0.001023 0.00307 0.00307 0.005117 0.004093 2.718925 3.242857 5.953595 6.03853 9.585309 8.251941 3.664459 3.021823 7.261381 5.997598 10.624988 9.361205l3.203972 3.204995c40.279379 40.229237 28.254507 109.539812-12.024871 149.820214L371.157763 796.383956c-40.278355 40.229237-105.761766 40.229237-146.042167 0l-3.229554-3.231601c-40.281425-40.278355-40.281425-105.809861 0-145.991002l75.93546-75.909877c9.742898-7.733125 15.997346-19.668968 15.997346-33.072233 0-23.312962-18.898419-42.211381-42.211381-42.211381-8.797363 0-16.963347 2.693342-23.725354 7.297197-0.021489-0.045025-0.044002-0.088004-0.066515-0.134053l-0.809435 0.757247c-2.989077 2.148943-5.691629 4.669346-8.025791 7.510044l-78.913281 73.841775c-74.178443 74.229608-74.178443 195.632609 0 269.758863l3.203972 3.202948c74.178443 74.127278 195.529255 74.127278 269.707698 0l171.829484-171.880649c74.076112-74.17435 80.357166-191.184297 6.282077-265.311575L607.934444 417.856853z"></path><path d="M855.61957 165.804257l-3.203972-3.203972c-74.17742-74.178443-195.528232-74.178443-269.706675 0L410.87944 334.479911c-74.178443 74.178443-78.263481 181.296089-4.085038 255.522628l3.152806 3.104711c3.368724 3.367701 6.865361 6.54302 10.434653 9.588379 2.583848 2.885723 5.618974 5.355985 8.992815 7.309476 0.025583 0.020466 0.052189 0.041956 0.077771 0.062422l0.011256-0.010233c5.377474 3.092431 11.608386 4.870938 18.257829 4.870938 20.263509 0 36.68962-16.428158 36.68962-36.68962 0-5.719258-1.309832-11.132548-3.645017-15.95846l0 0c-4.850471-10.891048-13.930267-17.521049-20.210297-23.802102l-3.15383-3.102664c-40.278355-40.278355-24.982998-98.79612 15.295358-139.074476l171.930791-171.830507c40.179095-40.280402 105.685018-40.280402 145.965419 0l3.206018 3.152806c40.279379 40.281425 40.279379 105.838513 0 146.06775l-75.686796 75.737962c-10.296507 7.628748-16.97358 19.865443-16.97358 33.662681 0 23.12365 18.745946 41.87062 41.87062 41.87062 8.048303 0 15.563464-2.275833 21.944801-6.211469 0.048095 0.081864 0.093121 0.157589 0.141216 0.240477l1.173732-1.083681c3.616364-2.421142 6.828522-5.393847 9.529027-8.792247l79.766718-73.603345C929.798013 361.334535 929.798013 239.981676 855.61957 165.804257z"></path></svg>`;
const defaultRegexp = /\b((?:https?|ftp):\/\/[^\s/$.?#].[^\s]*)\b/gi;
@@ -30,11 +30,15 @@ class HyperLinkIcon extends WidgetType {
toDOM() {
const wrapper = document.createElement('a');
wrapper.href = this.state.url;
wrapper.target = '_blank';
wrapper.innerHTML = pathStr;
wrapper.className = 'cm-hyper-link-icon';
wrapper.rel = 'nofollow';
wrapper.className = 'cm-hyper-link-icon cm-hyper-link-underline';
wrapper.title = this.state.url;
wrapper.setAttribute('data-url', this.state.url);
wrapper.onclick = (e) => {
e.preventDefault();
runtime.Browser.OpenURL(this.state.url);
return false;
};
const anchor = this.state.anchor && this.state.anchor(wrapper);
return anchor || wrapper;
}
@@ -141,10 +145,12 @@ export const hyperLinkStyle = EditorView.baseTheme({
color: '#0969da',
cursor: 'pointer',
transition: 'color 0.2s ease',
textDecoration: 'none',
textDecoration: 'underline',
textDecorationColor: '#0969da',
textDecorationThickness: '1px',
textUnderlineOffset: '2px',
'&:hover': {
color: '#0550ae',
textDecoration: 'underline',
}
},
@@ -160,9 +166,9 @@ export const hyperLinkStyle = EditorView.baseTheme({
verticalAlign: 'middle',
marginLeft: '0.2ch',
color: '#656d76',
textDecoration: 'none',
opacity: 0.7,
transition: 'opacity 0.2s ease, color 0.2s ease',
cursor: 'pointer',
'&:hover': {
opacity: 1,
color: '#0969da',
@@ -197,11 +203,17 @@ export const hyperLinkStyle = EditorView.baseTheme({
export const hyperLinkClickHandler = EditorView.domEventHandlers({
click: (event, view) => {
const target = event.target as HTMLElement;
let urlElement = target;
if (target.classList.contains('cm-hyper-link-text')) {
const url = target.getAttribute('data-url');
while (urlElement && !urlElement.hasAttribute('data-url')) {
urlElement = urlElement.parentElement as HTMLElement;
if (!urlElement || urlElement === document.body) break;
}
if (urlElement && urlElement.hasAttribute('data-url')) {
const url = urlElement.getAttribute('data-url');
if (url) {
window.open(url, '_blank', 'noopener,noreferrer');
runtime.Browser.OpenURL(url)
event.preventDefault();
return true;
}

View File

@@ -1,10 +0,0 @@
// 统一导出所有扩展
export * from './tabExtension';
export * from './wheelZoomExtension';
export * from './statsExtension';
export * from './autoSaveExtension';
export * from './fontExtension';
export * from './themeExtension';
export * from './codeblast';
export * from './codeblock';
export * from './keymap';

View File

@@ -1,23 +0,0 @@
import { Extension } from '@codemirror/state'
import { useKeybindingStore } from '@/stores/keybindingStore'
import { KeymapManager } from './keymapManager'
/**
* 异步创建快捷键扩展
* 确保快捷键配置已加载
*/
export const createDynamicKeymapExtension = async (): Promise<Extension> => {
const keybindingStore = useKeybindingStore()
// 确保快捷键配置已加载
if (keybindingStore.keyBindings.length === 0) {
await keybindingStore.loadKeyBindings()
}
return KeymapManager.createKeymapExtension(keybindingStore.enabledKeyBindings)
}
// 导出相关模块
export { KeymapManager } from './keymapManager'
export { commandRegistry, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commandRegistry'
export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types'

View File

@@ -97,6 +97,26 @@ const minimapClass = ViewPlugin.fromClass(
}
}
// 阻止小地图上的右键菜单
this.dom.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
});
// 阻止小地图内部元素和画布上的右键菜单
this.inner.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
});
this.canvas.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
});
if (config.autohide) {
this.dom.classList.add('cm-minimap-autohide');
}
@@ -122,6 +142,14 @@ const minimapClass = ViewPlugin.fromClass(
}
if (now) {
if (prev && this.dom && prev.autohide !== now.autohide) {
if (now.autohide) {
this.dom.classList.add('cm-minimap-autohide');
} else {
this.dom.classList.remove('cm-minimap-autohide');
}
}
this.text.update(update);
this.selection.update(update);
this.diagnostic.update(update);
@@ -279,7 +307,7 @@ const minimapClass = ViewPlugin.fromClass(
}
);
// 使用type定义而不是interface
// 使用type定义
export type MinimapConfig = Omit<Options, "enabled"> & {
/**
* A function that creates the element that contains the minimap

View File

@@ -3,8 +3,7 @@ import { Range } from '@codemirror/state';
// 生成彩虹颜色数组
function generateColors(): string[] {
return [
'red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet',
return ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'
];
}
@@ -70,18 +69,18 @@ const rainbowBracketsPlugin = ViewPlugin.fromClass(RainbowBracketsView, {
decorations: (v) => v.decorations,
});
export default function rainbowBrackets() {
export default function rainbowBracketsExtension() {
return [
rainbowBracketsPlugin,
EditorView.baseTheme({
// 为每种颜色定义CSS样式
'.cm-rainbow-bracket-red': { color: 'red' },
'.cm-rainbow-bracket-orange': { color: 'orange' },
'.cm-rainbow-bracket-yellow': { color: 'yellow' },
'.cm-rainbow-bracket-green': { color: 'green' },
'.cm-rainbow-bracket-blue': { color: 'blue' },
'.cm-rainbow-bracket-indigo': { color: 'indigo' },
'.cm-rainbow-bracket-violet': { color: 'violet' },
'.cm-rainbow-bracket-red': { color: '#FF6B6B' },
'.cm-rainbow-bracket-orange': { color: '#FF9E6B' },
'.cm-rainbow-bracket-yellow': { color: '#FFD166' },
'.cm-rainbow-bracket-green': { color: '#06D6A0' },
'.cm-rainbow-bracket-blue': { color: '#118AB2' },
'.cm-rainbow-bracket-indigo': { color: '#6B5B95' },
'.cm-rainbow-bracket-violet': { color: '#9B5DE5' },
}),
];
}

View File

@@ -0,0 +1,213 @@
import { EditorState, StateEffect, StateField, Facet } from "@codemirror/state";
import { Decoration, DecorationSet, EditorView } from "@codemirror/view";
// 高亮配置接口
export interface TextHighlightConfig {
backgroundColor?: string;
opacity?: number;
}
// 默认配置
const DEFAULT_CONFIG: Required<TextHighlightConfig> = {
backgroundColor: '#FFD700', // 金黄色
opacity: 0.3
};
// 定义添加和移除高亮的状态效果
const addHighlight = StateEffect.define<{from: number, to: number}>({
map: ({from, to}, change) => ({
from: change.mapPos(from),
to: change.mapPos(to)
})
});
const removeHighlight = StateEffect.define<{from: number, to: number}>({
map: ({from, to}, change) => ({
from: change.mapPos(from),
to: change.mapPos(to)
})
});
// 配置facet
const highlightConfigFacet = Facet.define<TextHighlightConfig, Required<TextHighlightConfig>>({
combine: (configs) => {
let result = { ...DEFAULT_CONFIG };
for (const config of configs) {
if (config.backgroundColor !== undefined) {
result.backgroundColor = config.backgroundColor;
}
if (config.opacity !== undefined) {
result.opacity = config.opacity;
}
}
return result;
}
});
// 创建高亮装饰
function createHighlightMark(config: Required<TextHighlightConfig>): Decoration {
const { backgroundColor, opacity } = config;
const rgbaColor = hexToRgba(backgroundColor, opacity);
return Decoration.mark({
attributes: {
style: `background-color: ${rgbaColor}; border-radius: 2px;`
}
});
}
// 将十六进制颜色转换为RGBA
function hexToRgba(hex: string, opacity: number): string {
// 移除 # 符号
hex = hex.replace('#', '');
// 处理短格式 (如 #FFF -> #FFFFFF)
if (hex.length === 3) {
hex = hex.split('').map(char => char + char).join('');
}
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}
// 存储高亮范围的状态字段 - 支持撤销
const highlightState = StateField.define<DecorationSet>({
create() {
return Decoration.none;
},
update(decorations, tr) {
// 映射现有装饰以适应文档变化
decorations = decorations.map(tr.changes);
// 处理效果
for (const effect of tr.effects) {
if (effect.is(addHighlight)) {
const { from, to } = effect.value;
const config = tr.state.facet(highlightConfigFacet);
const highlightMark = createHighlightMark(config);
decorations = decorations.update({
add: [highlightMark.range(from, to)]
});
}
else if (effect.is(removeHighlight)) {
const { from, to } = effect.value;
decorations = decorations.update({
filter: (rangeFrom, rangeTo) => {
// 移除与指定范围重叠的装饰
return !(rangeFrom < to && rangeTo > from);
}
});
}
}
return decorations;
},
provide: field => EditorView.decorations.from(field)
});
// 查找与给定范围重叠的所有高亮
function findHighlightsInRange(state: EditorState, from: number, to: number): Array<{from: number, to: number}> {
const highlights: Array<{from: number, to: number}> = [];
state.field(highlightState).between(from, to, (rangeFrom, rangeTo) => {
if (rangeFrom < to && rangeTo > from) {
highlights.push({ from: rangeFrom, to: rangeTo });
}
});
return highlights;
}
// 查找指定位置包含的高亮
function findHighlightsAt(state: EditorState, pos: number): Array<{from: number, to: number}> {
const highlights: Array<{from: number, to: number}> = [];
state.field(highlightState).between(pos, pos, (from, to) => {
highlights.push({ from, to });
});
return highlights;
}
// 添加高亮范围
function addHighlightRange(view: EditorView, from: number, to: number): boolean {
if (from === to) return false; // 不高亮空选择
// 检查是否已经完全高亮
const overlappingHighlights = findHighlightsInRange(view.state, from, to);
const isFullyHighlighted = overlappingHighlights.some(range =>
range.from <= from && range.to >= to
);
if (isFullyHighlighted) return false;
view.dispatch({
effects: addHighlight.of({from, to})
});
return true;
}
// 移除高亮范围
function removeHighlightRange(view: EditorView, from: number, to: number): boolean {
const highlights = findHighlightsInRange(view.state, from, to);
if (highlights.length === 0) return false;
view.dispatch({
effects: removeHighlight.of({from, to})
});
return true;
}
// 切换高亮状态
function toggleHighlight(view: EditorView): boolean {
const selection = view.state.selection.main;
// 如果有选择文本
if (!selection.empty) {
const {from, to} = selection;
// 检查选择范围内是否已经有高亮
const highlights = findHighlightsInRange(view.state, from, to);
if (highlights.length > 0) {
// 如果已有高亮,则移除
return removeHighlightRange(view, from, to);
} else {
// 如果没有高亮,则添加
return addHighlightRange(view, from, to);
}
}
// 如果是光标
else {
const pos = selection.from;
const highlightsAtCursor = findHighlightsAt(view.state, pos);
if (highlightsAtCursor.length > 0) {
// 移除光标位置的高亮
const highlight = highlightsAtCursor[0];
return removeHighlightRange(view, highlight.from, highlight.to);
}
}
return false;
}
// 导出文本高亮切换命令,供快捷键系统使用
export const textHighlightToggleCommand = toggleHighlight;
// 创建文本高亮扩展
export function createTextHighlighter(config: TextHighlightConfig = {}) {
return [
highlightConfigFacet.of(config),
highlightState
];
}

View File

@@ -1,359 +0,0 @@
import { EditorState, StateEffect, StateField, Transaction, Range } from "@codemirror/state";
import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
import { keymap } from "@codemirror/view";
// 全局高亮存储 - 以文档ID为键高亮范围数组为值
interface HighlightInfo {
from: number;
to: number;
}
class GlobalHighlightStore {
private static instance: GlobalHighlightStore;
private highlightMap: Map<string, HighlightInfo[]> = new Map();
private constructor() {}
public static getInstance(): GlobalHighlightStore {
if (!GlobalHighlightStore.instance) {
GlobalHighlightStore.instance = new GlobalHighlightStore();
}
return GlobalHighlightStore.instance;
}
// 保存文档的高亮
saveHighlights(documentId: string, highlights: HighlightInfo[]): void {
this.highlightMap.set(documentId, [...highlights]);
}
// 获取文档的高亮
getHighlights(documentId: string): HighlightInfo[] {
return this.highlightMap.get(documentId) || [];
}
// 添加高亮
addHighlight(documentId: string, highlight: HighlightInfo): void {
const highlights = this.getHighlights(documentId);
highlights.push(highlight);
this.saveHighlights(documentId, highlights);
}
// 移除高亮
removeHighlights(documentId: string, from: number, to: number): void {
const highlights = this.getHighlights(documentId);
const filtered = highlights.filter(h => !(h.from < to && h.to > from));
this.saveHighlights(documentId, filtered);
}
// 清除文档的所有高亮
clearHighlights(documentId: string): void {
this.highlightMap.delete(documentId);
}
}
// 获取全局高亮存储实例
const highlightStore = GlobalHighlightStore.getInstance();
// 定义添加和移除高亮的状态效果
const addHighlight = StateEffect.define<{from: number, to: number, documentId: string}>({
map: ({from, to, documentId}, change) => ({
from: change.mapPos(from),
to: change.mapPos(to),
documentId
})
});
const removeHighlight = StateEffect.define<{from: number, to: number, documentId: string}>({
map: ({from, to, documentId}, change) => ({
from: change.mapPos(from),
to: change.mapPos(to),
documentId
})
});
// 初始化高亮效果 - 用于页面加载时恢复高亮
const initHighlights = StateEffect.define<{highlights: HighlightInfo[], documentId: string}>();
// 高亮样式
const highlightMark = Decoration.mark({
attributes: {style: `background-color: rgba(255, 215, 0, 0.3)`}
});
// 存储高亮范围的状态字段
const highlightState = StateField.define<DecorationSet>({
create() {
return Decoration.none;
},
update(decorations, tr) {
// 先映射现有的装饰,以适应文档变化
decorations = decorations.map(tr.changes);
// 处理添加和移除高亮的效果
for (const effect of tr.effects) {
if (effect.is(addHighlight)) {
const { from, to, documentId } = effect.value;
decorations = decorations.update({
add: [highlightMark.range(from, to)]
});
// 同步到全局存储
highlightStore.addHighlight(documentId, { from, to });
}
else if (effect.is(removeHighlight)) {
const { from, to, documentId } = effect.value;
decorations = decorations.update({
filter: (rangeFrom, rangeTo) => {
// 移除与指定范围重叠的装饰
return !(rangeFrom < to && rangeTo > from);
}
});
// 同步到全局存储
highlightStore.removeHighlights(documentId, from, to);
}
else if (effect.is(initHighlights)) {
const { highlights } = effect.value;
const ranges = highlights.map(h => highlightMark.range(h.from, h.to));
if (ranges.length > 0) {
decorations = decorations.update({ add: ranges });
}
}
}
return decorations;
},
provide: field => EditorView.decorations.from(field)
});
// 定义高亮范围接口
interface HighlightRange {
from: number;
to: number;
decoration: Decoration;
}
// 查找指定位置包含的高亮
function findHighlightsAt(state: EditorState, pos: number): HighlightRange[] {
const highlights: HighlightRange[] = [];
state.field(highlightState).between(pos, pos, (from, to, deco) => {
highlights.push({ from, to, decoration: deco });
});
return highlights;
}
// 查找与给定范围重叠的所有高亮
function findHighlightsInRange(state: EditorState, from: number, to: number): HighlightRange[] {
const highlights: HighlightRange[] = [];
state.field(highlightState).between(from, to, (rangeFrom, rangeTo, deco) => {
// 只添加与指定范围有重叠的高亮
if (rangeFrom < to && rangeTo > from) {
highlights.push({ from: rangeFrom, to: rangeTo, decoration: deco });
}
});
return highlights;
}
// 收集当前所有高亮信息
function collectAllHighlights(state: EditorState): HighlightInfo[] {
const highlights: HighlightInfo[] = [];
state.field(highlightState).between(0, state.doc.length, (from, to) => {
highlights.push({ from, to });
});
return highlights;
}
// 添加高亮
function addHighlightRange(view: EditorView, from: number, to: number, documentId: string): boolean {
if (from === to) return false; // 不高亮空选择
// 检查是否已经完全高亮
const overlappingHighlights = findHighlightsInRange(view.state, from, to);
const isFullyHighlighted = overlappingHighlights.some(range =>
range.from <= from && range.to >= to
);
if (isFullyHighlighted) return false;
view.dispatch({
effects: addHighlight.of({from, to, documentId})
});
return true;
}
// 移除高亮
function removeHighlightRange(view: EditorView, from: number, to: number, documentId: string): boolean {
const highlights = findHighlightsInRange(view.state, from, to);
if (highlights.length === 0) return false;
view.dispatch({
effects: removeHighlight.of({from, to, documentId})
});
return true;
}
// 切换高亮状态
function toggleHighlight(view: EditorView, documentId: string): boolean {
const selection = view.state.selection.main;
// 如果有选择文本
if (!selection.empty) {
const {from, to} = selection;
// 检查选择范围内是否已经有高亮
const highlights = findHighlightsInRange(view.state, from, to);
if (highlights.length > 0) {
// 如果已有高亮,则移除
return removeHighlightRange(view, from, to, documentId);
} else {
// 如果没有高亮,则添加
return addHighlightRange(view, from, to, documentId);
}
}
// 如果是光标
else {
const pos = selection.from;
const highlightsAtCursor = findHighlightsAt(view.state, pos);
if (highlightsAtCursor.length > 0) {
// 移除光标位置的高亮
const highlight = highlightsAtCursor[0];
return removeHighlightRange(view, highlight.from, highlight.to, documentId);
}
}
return false;
}
// 创建高亮快捷键需要文档ID
function createHighlightKeymap(documentId: string) {
return keymap.of([
{key: "Mod-h", run: (view) => toggleHighlight(view, documentId)}
]);
}
// 高亮刷新管理器类
class HighlightRefreshManager {
private view: EditorView;
private refreshPending = false;
private initialSetupDone = false;
private rafId: number | null = null;
private documentId: string;
constructor(view: EditorView, documentId: string) {
this.view = view;
this.documentId = documentId;
}
/**
* 使用requestAnimationFrame安排视图更新
*/
scheduleRefresh(): void {
if (this.refreshPending) return;
this.refreshPending = true;
this.rafId = requestAnimationFrame(() => {
this.executeRefresh();
});
}
/**
* 执行视图更新
*/
private executeRefresh(): void {
this.refreshPending = false;
this.rafId = null;
if (!this.view.state) return;
try {
// 触发一个空的更新,确保视图刷新
this.view.dispatch({});
} catch (e) {
console.debug("highlight refresh error:", e);
}
}
/**
* 初始化高亮 - 应用保存的高亮
*/
initHighlights(): void {
const savedHighlights = highlightStore.getHighlights(this.documentId);
if (savedHighlights.length > 0) {
this.view.dispatch({
effects: initHighlights.of({
highlights: savedHighlights,
documentId: this.documentId
})
});
}
}
/**
* 执行初始化设置
*/
performInitialSetup(): void {
if (this.initialSetupDone) return;
Promise.resolve().then(() => {
this.initHighlights();
this.scheduleRefresh();
});
this.initialSetupDone = true;
}
/**
* 清理资源
*/
dispose(): void {
if (this.rafId !== null) {
cancelAnimationFrame(this.rafId);
}
}
}
// 创建高亮扩展
export function createTextHighlighter(documentId: string) {
// 视图插件
const highlightSetupPlugin = ViewPlugin.define((view) => {
// 创建刷新管理器实例
const refreshManager = new HighlightRefreshManager(view, documentId);
// 执行初始化设置
refreshManager.performInitialSetup();
return {
update(update: ViewUpdate) {
// 页面有内容变化时,保存最新的高亮状态
if (update.docChanged || update.transactions.some(tr =>
tr.effects.some(e => e.is(addHighlight) || e.is(removeHighlight))
)) {
// 延迟收集高亮信息,确保所有效果都已应用
setTimeout(() => {
const allHighlights = collectAllHighlights(view.state);
highlightStore.saveHighlights(documentId, allHighlights);
}, 0);
}
},
destroy() {
// 清理资源
refreshManager.dispose();
}
};
});
return [
highlightState,
createHighlightKeymap(documentId),
highlightSetupPlugin
];
}

View File

@@ -0,0 +1,364 @@
import { Extension, StateField, StateEffect } from '@codemirror/state';
import { EditorView, showTooltip, Tooltip } from '@codemirror/view';
import { createTranslationTooltip } from './tooltip';
/**
* 翻译器扩展配置
*/
export interface TranslatorConfig {
/** 默认翻译服务提供商 */
defaultTranslator: string;
/** 最小选择字符数才显示翻译按钮 */
minSelectionLength: number;
/** 最大翻译字符数 */
maxTranslationLength: number;
}
/**
* 默认翻译器配置
*/
export const defaultConfig: TranslatorConfig = {
defaultTranslator: 'bing',
minSelectionLength: 2,
maxTranslationLength: 5000,
};
// 全局配置存储
let currentConfig: TranslatorConfig = {...defaultConfig};
// 存储选择的文本用于翻译
let selectedTextForTranslation = "";
/**
* 翻译图标SVG
*/
const translationIconSvg = `
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<path d="M599.68 485.056h-8l30.592 164.672c20.352-7.04 38.72-17.344 54.912-31.104a271.36 271.36 0 0 1-40.704-64.64l32.256-4.032c8.896 17.664 19.072 33.28 30.592 46.72 23.872-27.968 42.24-65.152 55.04-111.744l-154.688 0.128z m121.92 133.76c18.368 15.36 39.36 26.56 62.848 33.472l14.784 4.416-8.64 30.336-14.72-4.352a205.696 205.696 0 0 1-76.48-41.728c-20.672 17.92-44.928 31.552-71.232 40.064l20.736 110.912H519.424l-9.984 72.512h385.152c18.112 0 32.704-14.144 32.704-31.616V295.424a32.128 32.128 0 0 0-32.704-31.552H550.528l35.2 189.696h79.424v-31.552h61.44v31.552h102.4v31.616h-42.688c-14.272 55.488-35.712 100.096-64.64 133.568zM479.36 791.68H193.472c-36.224 0-65.472-28.288-65.472-63.168V191.168C128 156.16 157.312 128 193.472 128h327.68l20.544 104.32h352.832c36.224 0 65.472 28.224 65.472 63.104v537.408c0 34.944-29.312 63.168-65.472 63.168H468.608l10.688-104.32zM337.472 548.352v-33.28H272.768v-48.896h60.16V433.28h-60.16v-41.728h64.704v-32.896h-102.4v189.632h102.4z m158.272 0V453.76c0-17.216-4.032-30.272-12.16-39.488-8.192-9.152-20.288-13.696-36.032-13.696a55.04 55.04 0 0 0-24.768 5.376 39.04 39.04 0 0 0-17.088 15.936h-1.984l-5.056-18.56h-28.352V548.48h37.12V480c0-17.088 2.304-29.376 6.912-36.736 4.608-7.424 12.16-11.072 22.528-11.072 7.616 0 13.248 2.56 16.64 7.872 3.52 5.248 5.312 13.056 5.312 23.488v84.736h36.928z" fill="currentColor"></path>
</svg>`;
// 用于设置翻译气泡的状态效果
const setTranslationTooltip = StateEffect.define<Tooltip | null>();
/**
* 翻译气泡的状态字段
*/
const translationTooltipField = StateField.define<readonly Tooltip[]>({
create() {
return [];
},
update(tooltips, tr) {
// 如果文档或选择变化,隐藏气泡
if (tr.docChanged || tr.selection) {
return [];
}
// 检查是否有特定的状态效果来更新tooltips
for (const effect of tr.effects) {
if (effect.is(setTranslationTooltip)) {
return effect.value ? [effect.value] : [];
}
}
return tooltips;
},
provide: field => showTooltip.computeN([field], state => state.field(field))
});
/**
* 根据当前选择获取翻译按钮tooltip
*/
function getTranslationButtonTooltips(state: any): readonly Tooltip[] {
// 如果气泡已显示,则不显示按钮
if (state.field(translationTooltipField).length > 0) return [];
const selection = state.selection.main;
// 如果没有选中文本,不显示按钮
if (selection.empty) return [];
// 获取选中的文本
const selectedText = state.sliceDoc(selection.from, selection.to);
// 检查文本是否只包含空格
if (!selectedText.trim()) {
return [];
}
// 检查文本长度条件
if (selectedText.length < currentConfig.minSelectionLength ||
selectedText.length > currentConfig.maxTranslationLength) {
return [];
}
// 保存选中的文本用于翻译
selectedTextForTranslation = selectedText;
// 返回翻译按钮tooltip配置
return [{
pos: selection.to,
above: false,
strictSide: true,
arrow: false,
create: (view) => {
// 创建按钮DOM
const dom = document.createElement('div');
dom.className = 'cm-translator-button';
dom.innerHTML = translationIconSvg;
// 点击事件
dom.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
// 显示翻译气泡
showTranslationTooltip(view);
});
return { dom };
}
}];
}
/**
* 显示翻译气泡
*/
function showTranslationTooltip(view: EditorView) {
if (!selectedTextForTranslation) return;
// 创建翻译气泡
const tooltip = createTranslationTooltip(view, selectedTextForTranslation);
// 更新状态以显示气泡
view.dispatch({
effects: setTranslationTooltip.of(tooltip)
});
}
/**
* 翻译按钮的状态字段
*/
const translationButtonField = StateField.define<readonly Tooltip[]>({
create(state) {
return getTranslationButtonTooltips(state);
},
update(tooltips, tr) {
// 如果文档或选择变化重新计算tooltip
if (tr.docChanged || tr.selection) {
return getTranslationButtonTooltips(tr.state);
}
// 检查是否有翻译气泡显示,如果有则不显示按钮
if (tr.state.field(translationTooltipField).length > 0) {
return [];
}
return tooltips;
},
provide: field => showTooltip.computeN([field], state => state.field(field))
});
/**
* 创建翻译扩展
*/
export function createTranslatorExtension(config?: Partial<TranslatorConfig>): Extension {
// 更新配置
currentConfig = { ...defaultConfig, ...config };
return [
// 翻译按钮tooltip
translationButtonField,
// 翻译气泡tooltip
translationTooltipField,
// 添加基础样式
EditorView.baseTheme({
".cm-translator-button": {
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
background: "var(--bg-secondary, transparent)",
color: "var(--text-muted, #4285f4)",
border: "1px solid var(--border-color, #dadce0)",
borderRadius: "3px",
padding: "2px",
width: "24px",
height: "24px",
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.08)",
userSelect: "none",
"&:hover": {
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
}
},
// 翻译气泡样式
".cm-translation-tooltip": {
background: "var(--bg-secondary, #fff)",
color: "var(--text-primary, #333)",
border: "1px solid var(--border-color, #dadce0)",
borderRadius: "3px",
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
padding: "8px",
maxWidth: "300px",
maxHeight: "200px",
display: "flex",
flexDirection: "column",
overflow: "hidden",
fontFamily: "var(--font-family, system-ui, -apple-system, sans-serif)",
fontSize: "11px"
},
".cm-translation-header": {
marginBottom: "8px",
flexShrink: "0"
},
".cm-translation-controls": {
display: "flex",
alignItems: "center",
gap: "4px",
flexWrap: "nowrap"
},
".cm-translation-select": {
padding: "2px 4px",
borderRadius: "3px",
border: "1px solid var(--border-color, #dadce0)",
background: "var(--bg-primary, #f5f5f5)",
fontSize: "11px",
color: "var(--text-primary, #333)",
flex: "1",
minWidth: "0",
maxWidth: "80px"
},
".cm-translation-swap": {
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "16px",
height: "16px",
borderRadius: "3px",
border: "1px solid var(--border-color, #dadce0)",
background: "var(--bg-primary, transparent)",
color: "var(--text-muted, #666)",
cursor: "pointer",
padding: "0",
flexShrink: "0",
"&:hover": {
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
}
},
// 滚动容器
".cm-translation-scroll-container": {
overflowY: "auto",
flex: "1",
minHeight: "0"
},
".cm-translation-result": {
display: "flex",
flexDirection: "column"
},
".cm-translation-result-header": {
display: "flex",
justifyContent: "flex-end",
marginBottom: "4px"
},
".cm-translation-result-wrapper": {
position: "relative",
width: "100%"
},
".cm-translation-copy-btn": {
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "20px",
height: "20px",
borderRadius: "3px",
border: "1px solid var(--border-color, #dadce0)",
background: "var(--bg-primary, transparent)",
color: "var(--text-muted, #666)",
cursor: "pointer",
padding: "0",
position: "absolute",
top: "4px",
right: "4px",
zIndex: "2",
opacity: "0.7",
"&:hover": {
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))",
opacity: "1"
},
"&.copied": {
background: "var(--bg-success, #4caf50)",
color: "white",
border: "1px solid var(--bg-success, #4caf50)",
opacity: "1"
}
},
".cm-translation-target": {
padding: "6px",
paddingRight: "28px", // 为复制按钮留出空间
background: "var(--bg-primary, rgba(66, 133, 244, 0.05))",
color: "var(--text-primary, #333)",
borderRadius: "3px",
whiteSpace: "pre-wrap",
wordBreak: "break-word"
},
".cm-translation-notice": {
fontSize: "10px",
color: "var(--text-muted, #888)",
padding: "2px 0",
fontStyle: "italic",
textAlign: "center",
marginBottom: "2px"
},
".cm-translation-error": {
color: "var(--text-danger, #d32f2f)",
fontStyle: "italic"
},
".cm-translation-loading": {
padding: "8px",
textAlign: "center",
color: "var(--text-muted, #666)",
fontSize: "11px",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "6px"
},
".cm-translation-loading::before": {
content: "''",
display: "inline-block",
width: "12px",
height: "12px",
borderRadius: "50%",
border: "2px solid var(--text-muted, #666)",
borderTopColor: "transparent",
animation: "cm-translation-spin 1s linear infinite"
},
"@keyframes cm-translation-spin": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" }
}
})
];
}
export default createTranslatorExtension;

View File

@@ -0,0 +1,507 @@
import { EditorView, Tooltip, TooltipView } from '@codemirror/view';
import { useTranslationStore } from '@/stores/translationStore';
// 创建翻译气泡弹窗
export class TranslationTooltip implements TooltipView {
dom: HTMLElement;
sourceText: string;
translationStore: ReturnType<typeof useTranslationStore>;
// UI元素
private translatorSelector: HTMLSelectElement;
private sourceLangSelector: HTMLSelectElement;
private targetLangSelector: HTMLSelectElement;
private resultContainer: HTMLDivElement;
private loadingIndicator: HTMLDivElement;
private translatedText: string = '';
private detectedSourceLang: string = ''; // 保存检测到的语言代码
constructor(_view: EditorView, text: string) {
this.sourceText = text;
this.translationStore = useTranslationStore();
// 创建气泡弹窗容器
this.dom = document.createElement('div');
this.dom.className = 'cm-translation-tooltip';
// 创建头部控制区域 - 固定在顶部
const header = document.createElement('div');
header.className = 'cm-translation-header';
// 控制选项容器 - 所有选择器在一行
const controlsContainer = document.createElement('div');
controlsContainer.className = 'cm-translation-controls';
// 创建选择器(初始为空,稍后填充)
this.sourceLangSelector = document.createElement('select');
this.sourceLangSelector.className = 'cm-translation-select';
// 交换语言按钮
const swapButton = document.createElement('button');
swapButton.className = 'cm-translation-swap';
swapButton.innerHTML = `<svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M7.5 21L3 16.5L7.5 12L9 13.5L7 15.5H15V13H17V17.5H7L9 19.5L7.5 21M16.5 3L21 7.5L16.5 12L15 10.5L17 8.5H9V11H7V6.5H17L15 4.5L16.5 3Z"/></svg>`;
// 目标语言选择
this.targetLangSelector = document.createElement('select');
this.targetLangSelector.className = 'cm-translation-select';
// 创建一个临时的翻译器选择器,稍后会被替换
this.translatorSelector = document.createElement('select');
this.translatorSelector.className = 'cm-translation-select';
const tempOption = document.createElement('option');
tempOption.textContent = 'Loading...';
this.translatorSelector.appendChild(tempOption);
// 添加所有控制元素到一行
controlsContainer.appendChild(this.sourceLangSelector);
controlsContainer.appendChild(swapButton);
controlsContainer.appendChild(this.targetLangSelector);
controlsContainer.appendChild(this.translatorSelector);
// 添加到头部
header.appendChild(controlsContainer);
// 创建内容滚动区域
const scrollContainer = document.createElement('div');
scrollContainer.className = 'cm-translation-scroll-container';
// 创建结果区域
this.resultContainer = document.createElement('div');
this.resultContainer.className = 'cm-translation-result';
// 加载指示器
this.loadingIndicator = document.createElement('div');
this.loadingIndicator.className = 'cm-translation-loading';
this.loadingIndicator.textContent = 'Translation...';
this.loadingIndicator.style.display = 'none';
// 将结果和加载指示器添加到滚动区域
scrollContainer.appendChild(this.loadingIndicator);
scrollContainer.appendChild(this.resultContainer);
// 将所有元素添加到主容器
this.dom.appendChild(header);
this.dom.appendChild(scrollContainer);
// 添加事件监听
this.sourceLangSelector.addEventListener('change', () => {
// 检查源语言和目标语言是否相同
this.handleLanguageChange();
this.translate();
});
this.targetLangSelector.addEventListener('change', () => {
// 检查源语言和目标语言是否相同
this.handleLanguageChange();
// 增加选中语言的使用频率
const targetLang = this.targetLangSelector.value;
if (targetLang) {
this.translationStore.incrementLanguageUsage(targetLang);
}
this.translate();
});
swapButton.addEventListener('click', () => {
// 交换语言
const temp = this.sourceLangSelector.value;
this.sourceLangSelector.value = this.targetLangSelector.value;
this.targetLangSelector.value = temp;
this.translate();
});
// 显示加载中
this.loadingIndicator.style.display = 'block';
this.resultContainer.innerHTML = '<div class="cm-translation-loading">Loading...</div>';
// 加载翻译器选项
this.loadTranslators().then(() => {
// 尝试自动检测语言
if (this.sourceText.length >= 10) {
this.detectedSourceLang = this.translationStore.detectLanguage(this.sourceText);
if (this.detectedSourceLang) {
// 如果检测到语言,更新选择器
this.updateLanguageSelectorsForDetectedLanguage(this.detectedSourceLang);
}
}
// 初始翻译
this.translate();
});
}
// 处理语言变更,防止源和目标语言相同
private handleLanguageChange() {
// 防止源语言和目标语言相同
if (this.sourceLangSelector.value === this.targetLangSelector.value) {
// 寻找一个不同的目标语言
const options = Array.from(this.targetLangSelector.options);
for (const option of options) {
if (option.value !== this.sourceLangSelector.value) {
this.targetLangSelector.value = option.value;
break;
}
}
}
}
// 加载翻译器选项
private async loadTranslators() {
try {
// 确保翻译器列表已加载
if (!this.translationStore.hasTranslators) {
await this.translationStore.loadAvailableTranslators();
}
// 清空现有选项
while (this.translatorSelector.firstChild) {
this.translatorSelector.removeChild(this.translatorSelector.firstChild);
}
// 添加翻译器选项
const translators = this.translationStore.availableTranslators;
if (translators.length === 0) {
// 如果没有可用翻译器,添加一个默认选项
const option = document.createElement('option');
option.value = 'bing';
option.textContent = 'Bing';
this.translatorSelector.appendChild(option);
} else {
translators.forEach(translator => {
const option = document.createElement('option');
option.value = translator;
option.textContent = this.getTranslatorDisplayName(translator);
option.selected = translator === this.translationStore.defaultTranslator;
this.translatorSelector.appendChild(option);
});
}
// 添加事件监听
this.translatorSelector.addEventListener('change', () => {
// 更新当前翻译器
this.translationStore.setDefaultConfig({
translatorType: this.translatorSelector.value
});
// 重置检测到的语言
this.detectedSourceLang = '';
// 当切换翻译器时,可能需要重新排序语言列表
// 加载该翻译器的语言列表
this.updateLanguageSelectors();
// 执行翻译
this.translate();
});
// 加载默认翻译器的语言列表
await this.updateLanguageSelectors();
return true;
} catch (error) {
console.error('Failed to load translators:', error);
// 清空现有选项
while (this.translatorSelector.firstChild) {
this.translatorSelector.removeChild(this.translatorSelector.firstChild);
}
// 添加默认翻译器选项
const defaultTranslators = ['bing', 'google', 'youdao', 'deepl'];
defaultTranslators.forEach(translator => {
const option = document.createElement('option');
option.value = translator;
option.textContent = this.getTranslatorDisplayName(translator);
option.selected = translator === 'bing';
this.translatorSelector.appendChild(option);
});
// 添加事件监听
this.translatorSelector.addEventListener('change', () => {
// 更新选择器并重新翻译
this.updateLanguageSelectors();
this.translate();
});
// 加载默认翻译器的语言列表
await this.updateLanguageSelectors();
return false;
}
}
// 更新语言选择器
private async updateLanguageSelectors() {
const currentTranslator = this.translatorSelector.value;
// 保存当前选中的语言
const currentSourceLang = this.sourceLangSelector.value || '';
const currentTargetLang = this.targetLangSelector.value || 'zh';
// 清空源语言选择器
while (this.sourceLangSelector.firstChild) {
this.sourceLangSelector.removeChild(this.sourceLangSelector.firstChild);
}
// 清空目标语言选择器
while (this.targetLangSelector.firstChild) {
this.targetLangSelector.removeChild(this.targetLangSelector.firstChild);
}
// 获取当前翻译器的语言列表
const languageMap = this.translationStore.currentLanguageMap;
// 如果语言列表为空,直接返回
if (!languageMap || Object.keys(languageMap).length === 0) {
return;
}
// 获取按使用频率排序的语言列表
const sortedLanguages = this.translationStore.getSortedLanguages(currentTranslator);
// 添加所有语言选项
if (Array.isArray(sortedLanguages)) {
// 处理非分组返回值
sortedLanguages.forEach(([code, langInfo]) => {
this.addLanguageOption(code, langInfo);
});
} else {
// 处理分组返回值
// 先添加常用语言
sortedLanguages.frequent.forEach(([code, langInfo]) => {
this.addLanguageOption(code, langInfo);
});
// 再添加其他语言
sortedLanguages.others.forEach(([code, langInfo]) => {
this.addLanguageOption(code, langInfo);
});
}
// 匹配之前的语言选项或使用默认值
this.updateSelectedLanguages(currentSourceLang, currentTargetLang, currentTranslator);
}
// 添加语言选项到选择器
private addLanguageOption(code: string, langInfo: any) {
// 使用后端提供的名称,而不是代码
const displayName = langInfo.Name || langInfo.name || code;
// 源语言选项
const sourceOption = document.createElement('option');
sourceOption.value = code;
sourceOption.textContent = displayName;
this.sourceLangSelector.appendChild(sourceOption);
// 目标语言选项
const targetOption = document.createElement('option');
targetOption.value = code;
// 不再显示使用次数,直接使用语言名称
targetOption.textContent = displayName;
this.targetLangSelector.appendChild(targetOption);
}
// 更新选中的语言选项,确保语言代码在当前翻译器中有效
private updateSelectedLanguages(sourceLang: string, targetLang: string, translatorType: string) {
// 尝试在当前翻译器中找到匹配的语言代码
const validSourceLang = this.translationStore.validateLanguage(sourceLang, translatorType);
// 如果找到有效的语言代码,且该代码在选择器中存在,则选中它
if (validSourceLang && this.hasLanguageOption(this.sourceLangSelector, validSourceLang)) {
this.sourceLangSelector.value = validSourceLang;
} else if (this.detectedSourceLang) {
// 如果没有找到匹配但有检测到的语言,尝试使用它
const validDetectedLang = this.translationStore.validateLanguage(this.detectedSourceLang, translatorType);
if (this.hasLanguageOption(this.sourceLangSelector, validDetectedLang)) {
this.sourceLangSelector.value = validDetectedLang;
} else if (this.sourceLangSelector.options.length > 0) {
// 如果没有检测到的语言,使用第一个可用选项
this.sourceLangSelector.selectedIndex = 0;
}
} else if (this.sourceLangSelector.options.length > 0) {
// 如果没有检测到的语言,使用第一个可用选项
this.sourceLangSelector.selectedIndex = 0;
}
// 对于目标语言,尝试找到匹配的语言代码
const validTargetLang = this.translationStore.validateLanguage(targetLang, translatorType);
if (this.hasLanguageOption(this.targetLangSelector, validTargetLang)) {
this.targetLangSelector.value = validTargetLang;
} else {
// 如果没有找到匹配,使用默认目标语言或第一个可用选项
const defaultTarget = this.translationStore.defaultTargetLang;
if (this.hasLanguageOption(this.targetLangSelector, defaultTarget)) {
this.targetLangSelector.value = defaultTarget;
} else if (this.targetLangSelector.options.length > 0) {
this.targetLangSelector.selectedIndex = 0;
}
}
// 确保源语言和目标语言不同
this.handleLanguageChange();
}
// 检查选择器是否有指定语言选项
private hasLanguageOption(selector: HTMLSelectElement, langCode: string): boolean {
return Array.from(selector.options).some(option => option.value === langCode);
}
// 为检测到的语言更新语言选择器
private updateLanguageSelectorsForDetectedLanguage(detectedLang: string) {
if (!detectedLang) return;
// 根据当前翻译器验证检测到的语言
const currentTranslator = this.translatorSelector.value;
const validLang = this.translationStore.validateLanguage(detectedLang, currentTranslator);
// 检查验证后的语言是否在选择器中
const hasDetectedOption = this.hasLanguageOption(this.sourceLangSelector, validLang);
// 设置检测到的语言为源语言
if (hasDetectedOption) {
this.sourceLangSelector.value = validLang;
}
// 存储检测到的语言代码,以便后续使用
this.detectedSourceLang = validLang;
}
// 获取翻译器显示名称
private getTranslatorDisplayName(translatorType: string): string {
switch (translatorType) {
case 'google': return 'Google';
case 'bing': return 'Bing';
case 'youdao': return 'YouDao';
case 'deepl': return 'DeepL';
default: return translatorType;
}
}
// 执行翻译
private async translate() {
const targetLang = this.targetLangSelector.value;
const translatorType = this.translatorSelector.value;
// 显示加载状态
this.loadingIndicator.style.display = 'block';
this.resultContainer.innerHTML = '';
try {
// 执行翻译 - 源语言将在store中自动检测
const result = await this.translationStore.translateText(
this.sourceText,
targetLang,
translatorType
);
// 如果检测到了语言,更新源语言选择器
if (result.sourceLang) {
this.detectedSourceLang = result.sourceLang;
this.updateLanguageSelectorsForDetectedLanguage(result.sourceLang);
}
// 显示翻译结果
this.displayTranslationResult(result);
} catch (err) {
console.error('Translation failed:', err);
this.resultContainer.innerHTML = '';
this.translatedText = '';
} finally {
// 隐藏加载状态
this.loadingIndicator.style.display = 'none';
}
}
// 显示翻译结果
private displayTranslationResult(result: any) {
// 更新结果显示
this.resultContainer.innerHTML = '';
// 创建结果容器
const resultWrapper = document.createElement('div');
resultWrapper.className = 'cm-translation-result-wrapper';
// 只显示翻译结果区域
const translatedTextElem = document.createElement('div');
translatedTextElem.className = 'cm-translation-target';
if (result.error) {
translatedTextElem.classList.add('cm-translation-error');
translatedTextElem.textContent = result.error;
this.translatedText = '';
} else {
this.translatedText = result.translatedText || '';
translatedTextElem.textContent = this.translatedText || '';
}
// 添加复制按钮
if (this.translatedText) {
const copyButton = document.createElement('button');
copyButton.className = 'cm-translation-copy-btn';
copyButton.innerHTML = `<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`;
copyButton.addEventListener('click', () => {
navigator.clipboard.writeText(this.translatedText).then(() => {
// 显示复制成功提示
const originalText = copyButton.innerHTML;
copyButton.innerHTML = `<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>`;
copyButton.classList.add('copied');
setTimeout(() => {
copyButton.innerHTML = originalText;
copyButton.classList.remove('copied');
}, 1500);
});
});
// 将复制按钮添加到结果包装器
resultWrapper.appendChild(copyButton);
}
// 添加翻译结果到包装器
resultWrapper.appendChild(translatedTextElem);
// 添加到结果容器
this.resultContainer.appendChild(resultWrapper);
}
// 更新默认配置
private updateDefaultConfig() {
const targetLang = this.targetLangSelector.value;
// 增加目标语言的使用频率
if (targetLang) {
this.translationStore.incrementLanguageUsage(targetLang);
}
this.translationStore.setDefaultConfig({
targetLang: targetLang,
translatorType: this.translatorSelector.value
});
}
// 当气泡弹窗被销毁时
destroy() {
// 保存当前配置作为默认值
this.updateDefaultConfig();
}
}
// 创建翻译气泡
export function createTranslationTooltip(view: EditorView, text: string): Tooltip {
return {
pos: view.state.selection.main.to, // 紧贴文本末尾
above: false,
strictSide: false,
arrow: true,
create: () => new TranslationTooltip(view, text)
};
}

View File

@@ -7,7 +7,7 @@ import {
searchToggleRegex,
searchToggleWholeWord,
showSearchVisibilityCommand
} from '../vscodeSearch/commands'
} from '../extensions/vscodeSearch/commands'
import {
addNewBlockAfterCurrent,
addNewBlockAfterLast,
@@ -20,14 +20,40 @@ import {
moveCurrentBlockUp,
selectNextBlock,
selectPreviousBlock
} from '../codeblock/commands'
import { selectAll } from '../codeblock/selectAll'
import { deleteLineCommand } from '../codeblock/deleteLine'
import { moveLineUp, moveLineDown } from '../codeblock/moveLines'
import { transposeChars } from '@/views/editor/extensions'
import { copyCommand, cutCommand, pasteCommand } from '../codeblock/copyPaste'
import { undo, redo, undoSelection, redoSelection, cursorSyntaxLeft, cursorSyntaxRight, selectSyntaxLeft, selectSyntaxRight, copyLineUp, copyLineDown, insertBlankLine, selectLine, selectParentSyntax, indentLess, indentMore, indentSelection, cursorMatchingBracket, toggleComment, toggleBlockComment, insertNewlineAndIndent, deleteCharBackward, deleteCharForward, deleteGroupBackward, deleteGroupForward } from '@codemirror/commands'
import { foldCode, unfoldCode, foldAll, unfoldAll } from '@codemirror/language'
} from '../extensions/codeblock/commands'
import {selectAll} from '../extensions/codeblock/selectAll'
import {deleteLineCommand} from '../extensions/codeblock/deleteLine'
import {moveLineDown, moveLineUp} from '../extensions/codeblock/moveLines'
import {transposeChars} from '../extensions/codeblock'
import {copyCommand, cutCommand, pasteCommand} from '../extensions/codeblock/copyPaste'
import {textHighlightToggleCommand} from '../extensions/textHighlight/textHighlightExtension'
import {
copyLineDown,
copyLineUp,
cursorMatchingBracket,
cursorSyntaxLeft,
cursorSyntaxRight,
deleteCharBackward,
deleteCharForward,
deleteGroupBackward,
deleteGroupForward,
indentLess,
indentMore,
indentSelection,
insertBlankLine,
insertNewlineAndIndent,
redo,
redoSelection,
selectLine,
selectParentSyntax,
selectSyntaxLeft,
selectSyntaxRight,
toggleBlockComment,
toggleComment,
undo,
undoSelection
} from '@codemirror/commands'
import {foldAll, foldCode, unfoldAll, unfoldCode} from '@codemirror/language'
import i18n from '@/i18n'
// 默认编辑器选项
@@ -259,6 +285,12 @@ export const commandRegistry = {
handler: deleteGroupForward,
descriptionKey: 'keybindings.commands.deleteGroupForward'
},
// 文本高亮扩展命令
[KeyBindingCommand.TextHighlightToggleCommand]: {
handler: textHighlightToggleCommand,
descriptionKey: 'keybindings.commands.textHighlightToggle'
},
} as const
/**

View File

@@ -0,0 +1,58 @@
import { Extension } from '@codemirror/state'
import { useKeybindingStore } from '@/stores/keybindingStore'
import { useExtensionStore } from '@/stores/extensionStore'
import { KeymapManager } from './keymapManager'
import { ExtensionID } from '@/../bindings/voidraft/internal/models/models'
/**
* 异步创建快捷键扩展
* 确保快捷键配置和扩展配置已加载
*/
export const createDynamicKeymapExtension = async (): Promise<Extension> => {
const keybindingStore = useKeybindingStore()
const extensionStore = useExtensionStore()
// 确保快捷键配置已加载
if (keybindingStore.keyBindings.length === 0) {
await keybindingStore.loadKeyBindings()
}
// 确保扩展配置已加载
if (extensionStore.extensions.length === 0) {
await extensionStore.loadExtensions()
}
// 获取启用的扩展ID列表
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id)
return KeymapManager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds)
}
/**
* 更新快捷键映射
* @param view 编辑器视图
*/
export const updateKeymapExtension = (view: any): void => {
const keybindingStore = useKeybindingStore()
const extensionStore = useExtensionStore()
// 获取启用的扩展ID列表
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id)
KeymapManager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds)
}
/**
* 获取指定扩展的快捷键
* @param extensionId 扩展ID
* @returns 该扩展的快捷键列表
*/
export const getExtensionKeyBindings = (extensionId: ExtensionID) => {
const keybindingStore = useKeybindingStore()
return keybindingStore.getKeyBindingsByExtension(extensionId)
}
// 导出相关模块
export { KeymapManager } from './keymapManager'
export { commandRegistry, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commandRegistry'
export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types'

View File

@@ -1,6 +1,6 @@
import {keymap} from '@codemirror/view'
import {Extension} from '@codemirror/state'
import {KeyBinding as KeyBindingConfig} from '@/../bindings/voidraft/internal/models/models'
import {Extension, Compartment} from '@codemirror/state'
import {KeyBinding as KeyBindingConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models'
import {KeyBinding, KeymapResult} from './types'
import {getCommandHandler, isCommandRegistered} from './commandRegistry'
@@ -9,12 +9,15 @@ import {getCommandHandler, isCommandRegistered} from './commandRegistry'
* CodeMirror快捷键扩展
*/
export class KeymapManager {
private static compartment = new Compartment()
/**
* CodeMirror快捷键绑定
* @param keyBindings
* @param enabledExtensions ID列表使
* @returns
*/
static convertToKeyBindings(keyBindings: KeyBindingConfig[]): KeymapResult {
static convertToKeyBindings(keyBindings: KeyBindingConfig[], enabledExtensions?: ExtensionID[]): KeymapResult {
const result: KeyBinding[] = []
for (const binding of keyBindings) {
@@ -23,6 +26,11 @@ export class KeymapManager {
continue
}
// 如果提供了扩展列表,则只处理启用扩展的快捷键
if (enabledExtensions && !enabledExtensions.includes(binding.extension)) {
continue
}
// 检查命令是否已注册
if (!isCommandRegistered(binding.command)) {
continue
@@ -50,13 +58,47 @@ export class KeymapManager {
/**
* CodeMirror快捷键扩展
* @param keyBindings
* @param enabledExtensions ID列表
* @returns CodeMirror扩展
*/
static createKeymapExtension(keyBindings: KeyBindingConfig[]): Extension {
static createKeymapExtension(keyBindings: KeyBindingConfig[], enabledExtensions?: ExtensionID[]): Extension {
const {keyBindings: cmKeyBindings} =
this.convertToKeyBindings(keyBindings)
this.convertToKeyBindings(keyBindings, enabledExtensions)
return keymap.of(cmKeyBindings)
return this.compartment.of(keymap.of(cmKeyBindings))
}
/**
*
* @param view
* @param keyBindings
* @param enabledExtensions ID列表
*/
static updateKeymap(view: any, keyBindings: KeyBindingConfig[], enabledExtensions: ExtensionID[]): void {
const {keyBindings: cmKeyBindings} =
this.convertToKeyBindings(keyBindings, enabledExtensions)
view.dispatch({
effects: this.compartment.reconfigure(keymap.of(cmKeyBindings))
})
}
/**
*
* @param keyBindings
* @returns
*/
static groupByExtension(keyBindings: KeyBindingConfig[]): Map<ExtensionID, KeyBindingConfig[]> {
const groups = new Map<ExtensionID, KeyBindingConfig[]>()
for (const binding of keyBindings) {
if (!groups.has(binding.extension)) {
groups.set(binding.extension, [])
}
groups.get(binding.extension)!.push(binding)
}
return groups
}
/**

View File

@@ -0,0 +1,394 @@
import {Compartment, Extension, StateEffect} from '@codemirror/state'
import {EditorView} from '@codemirror/view'
import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models'
/**
* 扩展工厂接口
* 每个扩展需要实现此接口来创建和配置扩展
*/
export interface ExtensionFactory {
/**
* 创建扩展实例
* @param config 扩展配置
* @returns CodeMirror扩展
*/
create(config: any): Extension
/**
* 获取默认配置
* @returns 默认配置对象
*/
getDefaultConfig(): any
/**
* 验证配置
* @param config 配置对象
* @returns 是否有效
*/
validateConfig?(config: any): boolean
}
/**
* 扩展状态
*/
interface ExtensionState {
id: ExtensionID
factory: ExtensionFactory
config: any
enabled: boolean
compartment: Compartment
extension: Extension
}
/**
* 视图信息
*/
interface EditorViewInfo {
view: EditorView
documentId: number
registered: boolean
}
/**
* 扩展管理器
* 负责管理所有动态扩展的注册、启用、禁用和配置更新
* 采用统一配置,多视图同步的设计模式
*/
export class ExtensionManager {
// 统一的扩展状态存储
private extensionStates = new Map<ExtensionID, ExtensionState>()
// 编辑器视图管理
private viewsMap = new Map<number, EditorViewInfo>()
private activeViewId: number | null = null
// 注册的扩展工厂
private extensionFactories = new Map<ExtensionID, ExtensionFactory>()
// 防抖处理
private debounceTimers = new Map<ExtensionID, number>()
private debounceDelay = 300 // 默认防抖时间为300毫秒
/**
* 注册扩展工厂
* @param id 扩展ID
* @param factory 扩展工厂
*/
registerExtension(id: ExtensionID, factory: ExtensionFactory): void {
this.extensionFactories.set(id, factory)
// 创建初始状态
if (!this.extensionStates.has(id)) {
const compartment = new Compartment()
const defaultConfig = factory.getDefaultConfig()
this.extensionStates.set(id, {
id,
factory,
config: defaultConfig,
enabled: false,
compartment,
extension: [] // 默认为空扩展(禁用状态)
})
}
}
/**
* 获取所有注册的扩展ID列表
*/
getRegisteredExtensions(): ExtensionID[] {
return Array.from(this.extensionFactories.keys())
}
/**
* 检查扩展是否已注册
* @param id 扩展ID
*/
isExtensionRegistered(id: ExtensionID): boolean {
return this.extensionFactories.has(id)
}
/**
* 从后端配置初始化扩展状态
* @param extensionConfigs 后端扩展配置列表
*/
initializeExtensionsFromConfig(extensionConfigs: ExtensionConfig[]): void {
for (const config of extensionConfigs) {
const factory = this.extensionFactories.get(config.id)
if (!factory) continue
// 验证配置
if (factory.validateConfig && !factory.validateConfig(config.config)) {
continue
}
try {
// 创建扩展实例
const extension = config.enabled ? factory.create(config.config) : []
// 如果状态已存在则更新,否则创建新状态
if (this.extensionStates.has(config.id)) {
const state = this.extensionStates.get(config.id)!
state.config = config.config
state.enabled = config.enabled
state.extension = extension
} else {
const compartment = new Compartment()
this.extensionStates.set(config.id, {
id: config.id,
factory,
config: config.config,
enabled: config.enabled,
compartment,
extension
})
}
} catch (error) {
console.error(`Failed to initialize extension ${config.id}:`, error)
}
}
}
/**
* 获取初始扩展配置数组(用于创建编辑器)
* @returns CodeMirror扩展数组
*/
getInitialExtensions(): Extension[] {
const extensions: Extension[] = []
// 为每个注册的扩展添加compartment
for (const state of this.extensionStates.values()) {
extensions.push(state.compartment.of(state.extension))
}
return extensions
}
/**
* 设置编辑器视图
* @param view 编辑器视图实例
* @param documentId 文档ID
*/
setView(view: EditorView, documentId: number): void {
// 保存视图信息
this.viewsMap.set(documentId, {
view,
documentId,
registered: true
})
// 设置当前活动视图
this.activeViewId = documentId
}
/**
* 获取当前活动视图
*/
private getActiveView(): EditorView | null {
if (this.activeViewId === null) return null
const viewInfo = this.viewsMap.get(this.activeViewId)
return viewInfo ? viewInfo.view : null
}
/**
* 更新单个扩展配置并应用到所有视图(带防抖功能)
* @param id 扩展ID
* @param enabled 是否启用
* @param config 扩展配置
*/
updateExtension(id: ExtensionID, enabled: boolean, config: any = {}): void {
// 清除之前的定时器
if (this.debounceTimers.has(id)) {
window.clearTimeout(this.debounceTimers.get(id))
}
// 设置新的定时器
const timerId = window.setTimeout(() => {
this.updateExtensionImmediate(id, enabled, config)
this.debounceTimers.delete(id)
}, this.debounceDelay)
this.debounceTimers.set(id, timerId)
}
/**
* 立即更新扩展(无防抖)
* @param id 扩展ID
* @param enabled 是否启用
* @param config 扩展配置
*/
updateExtensionImmediate(id: ExtensionID, enabled: boolean, config: any = {}): void {
// 获取扩展状态
const state = this.extensionStates.get(id)
if (!state) return
// 获取工厂
const factory = state.factory
// 验证配置
if (factory.validateConfig && !factory.validateConfig(config)) {
return
}
try {
// 创建新的扩展实例
const extension = enabled ? factory.create(config) : []
// 更新内部状态
state.config = config
state.enabled = enabled
state.extension = extension
// 应用到所有视图
this.applyExtensionToAllViews(id)
} catch (error) {
console.error(`Failed to update extension ${id}:`, error)
}
}
/**
* 将指定扩展的当前状态应用到所有视图
* @param id 扩展ID
*/
private applyExtensionToAllViews(id: ExtensionID): void {
const state = this.extensionStates.get(id)
if (!state) return
// 遍历所有视图并应用更改
for (const viewInfo of this.viewsMap.values()) {
try {
if (!viewInfo.registered) continue
viewInfo.view.dispatch({
effects: state.compartment.reconfigure(state.extension)
})
} catch (error) {
console.error(`Failed to apply extension ${id} to document ${viewInfo.documentId}:`, error)
}
}
}
/**
* 批量更新扩展
* @param updates 更新配置数组
*/
updateExtensions(updates: Array<{
id: ExtensionID
enabled: boolean
config: any
}>): void {
// 清除所有相关的防抖定时器
for (const update of updates) {
if (this.debounceTimers.has(update.id)) {
window.clearTimeout(this.debounceTimers.get(update.id))
this.debounceTimers.delete(update.id)
}
}
// 更新所有扩展状态
for (const update of updates) {
// 获取扩展状态
const state = this.extensionStates.get(update.id)
if (!state) continue
// 获取工厂
const factory = state.factory
// 验证配置
if (factory.validateConfig && !factory.validateConfig(update.config)) {
continue
}
try {
// 创建新的扩展实例
const extension = update.enabled ? factory.create(update.config) : []
// 更新内部状态
state.config = update.config
state.enabled = update.enabled
state.extension = extension
} catch (error) {
console.error(`Failed to update extension ${update.id}:`, error)
}
}
// 将更改应用到所有视图
for (const viewInfo of this.viewsMap.values()) {
if (!viewInfo.registered) continue
const effects: StateEffect<any>[] = []
for (const update of updates) {
const state = this.extensionStates.get(update.id)
if (!state) continue
effects.push(state.compartment.reconfigure(state.extension))
}
if (effects.length > 0) {
try {
viewInfo.view.dispatch({ effects })
} catch (error) {
console.error(`Failed to apply extensions to document ${viewInfo.documentId}:`, error)
}
}
}
}
/**
* 获取扩展当前状态
* @param id 扩展ID
*/
getExtensionState(id: ExtensionID): {
enabled: boolean
config: any
} | null {
const state = this.extensionStates.get(id)
if (!state) return null
return {
enabled: state.enabled,
config: state.config
}
}
/**
* 重置扩展到默认配置
* @param id 扩展ID
*/
resetExtensionToDefault(id: ExtensionID): void {
const state = this.extensionStates.get(id)
if (!state) return
const defaultConfig = state.factory.getDefaultConfig()
this.updateExtension(id, true, defaultConfig)
}
/**
* 从管理器中移除视图
* @param documentId 文档ID
*/
removeView(documentId: number): void {
if (this.activeViewId === documentId) {
this.activeViewId = null
}
this.viewsMap.delete(documentId)
}
/**
* 销毁管理器
*/
destroy(): void {
// 清除所有防抖定时器
for (const timerId of this.debounceTimers.values()) {
window.clearTimeout(timerId)
}
this.debounceTimers.clear()
this.viewsMap.clear()
this.activeViewId = null
this.extensionFactories.clear()
this.extensionStates.clear()
}
}

View File

@@ -0,0 +1,302 @@
import {ExtensionFactory, ExtensionManager} from './ExtensionManager'
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models'
import i18n from '@/i18n'
// 导入现有扩展的创建函数
import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension'
import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension'
import {color} from '../extensions/colorSelector'
import {hyperLink} from '../extensions/hyperlink'
import {minimap} from '../extensions/minimap'
import {vscodeSearch} from '../extensions/vscodeSearch'
import {createCheckboxExtension} from '../extensions/checkbox'
import {createTranslatorExtension} from '../extensions/translator'
import {foldingOnIndent} from '../extensions/fold/foldExtension'
/**
* 彩虹括号扩展工厂
*/
export const rainbowBracketsFactory: ExtensionFactory = {
create(config: any) {
return rainbowBracketsExtension()
},
getDefaultConfig() {
return {}
},
validateConfig(config: any) {
return typeof config === 'object'
}
}
/**
* 文本高亮扩展工厂
*/
export const textHighlightFactory: ExtensionFactory = {
create(config: any) {
return createTextHighlighter({
backgroundColor: config.backgroundColor || '#FFD700',
opacity: config.opacity || 0.3
})
},
getDefaultConfig() {
return {
backgroundColor: '#FFD700', // 金黄色
opacity: 0.3 // 透明度
}
},
validateConfig(config: any) {
return typeof config === 'object' &&
(!config.backgroundColor || typeof config.backgroundColor === 'string') &&
(!config.opacity || (typeof config.opacity === 'number' && config.opacity >= 0 && config.opacity <= 1))
}
}
/**
* 小地图扩展工厂
*/
export const minimapFactory: ExtensionFactory = {
create(config: any) {
const options = {
displayText: config.displayText || 'characters',
showOverlay: config.showOverlay || 'always',
autohide: config.autohide || false
}
return minimap(options)
},
getDefaultConfig() {
return {
displayText: 'characters',
showOverlay: 'always',
autohide: false
}
},
validateConfig(config: any) {
return typeof config === 'object' &&
(!config.displayText || typeof config.displayText === 'string') &&
(!config.showOverlay || typeof config.showOverlay === 'string') &&
(!config.autohide || typeof config.autohide === 'boolean')
}
}
/**
* 超链接扩展工厂
*/
export const hyperlinkFactory: ExtensionFactory = {
create(config: any) {
return hyperLink
},
getDefaultConfig() {
return {}
},
validateConfig(config: any) {
return typeof config === 'object'
}
}
/**
* 颜色选择器扩展工厂
*/
export const colorSelectorFactory: ExtensionFactory = {
create(config: any) {
return color
},
getDefaultConfig() {
return {}
},
validateConfig(config: any) {
return typeof config === 'object'
}
}
/**
* 搜索扩展工厂
*/
export const searchFactory: ExtensionFactory = {
create(config: any) {
return vscodeSearch
},
getDefaultConfig() {
return {}
},
validateConfig(config: any) {
return typeof config === 'object'
}
}
export const foldFactory: ExtensionFactory = {
create(config: any) {
return foldingOnIndent;
},
getDefaultConfig(): any {
return {}
},
validateConfig(config: any): boolean {
return typeof config === 'object'
}
}
/**
* 选择框扩展工厂
*/
export const checkboxFactory: ExtensionFactory = {
create(config: any) {
return createCheckboxExtension()
},
getDefaultConfig() {
return {}
},
validateConfig(config: any) {
return typeof config === 'object'
}
}
/**
* 翻译扩展工厂
*/
export const translatorFactory: ExtensionFactory = {
create(config: any) {
return createTranslatorExtension({
defaultTranslator: config.defaultTranslator || 'bing',
minSelectionLength: config.minSelectionLength || 2,
maxTranslationLength: config.maxTranslationLength || 5000,
})
},
getDefaultConfig() {
return {
defaultTranslator: 'bing',
minSelectionLength: 2,
maxTranslationLength: 5000,
}
},
validateConfig(config: any) {
return typeof config === 'object'
}
}
/**
* 所有扩展的统一配置
* 排除$zero值以避免TypeScript类型错误
*/
const EXTENSION_CONFIGS = {
// 编辑增强扩展
[ExtensionID.ExtensionRainbowBrackets]: {
factory: rainbowBracketsFactory,
displayNameKey: 'extensions.rainbowBrackets.name',
descriptionKey: 'extensions.rainbowBrackets.description'
},
[ExtensionID.ExtensionHyperlink]: {
factory: hyperlinkFactory,
displayNameKey: 'extensions.hyperlink.name',
descriptionKey: 'extensions.hyperlink.description'
},
[ExtensionID.ExtensionColorSelector]: {
factory: colorSelectorFactory,
displayNameKey: 'extensions.colorSelector.name',
descriptionKey: 'extensions.colorSelector.description'
},
[ExtensionID.ExtensionTranslator]: {
factory: translatorFactory,
displayNameKey: 'extensions.translator.name',
descriptionKey: 'extensions.translator.description'
},
// UI增强扩展
[ExtensionID.ExtensionMinimap]: {
factory: minimapFactory,
displayNameKey: 'extensions.minimap.name',
descriptionKey: 'extensions.minimap.description'
},
// 工具扩展
[ExtensionID.ExtensionSearch]: {
factory: searchFactory,
displayNameKey: 'extensions.search.name',
descriptionKey: 'extensions.search.description'
},
[ExtensionID.ExtensionFold]: {
factory: foldFactory,
displayNameKey: 'extensions.fold.name',
descriptionKey: 'extensions.fold.description'
},
[ExtensionID.ExtensionTextHighlight]: {
factory: textHighlightFactory,
displayNameKey: 'extensions.textHighlight.name',
descriptionKey: 'extensions.textHighlight.description'
},
[ExtensionID.ExtensionCheckbox]: {
factory: checkboxFactory,
displayNameKey: 'extensions.checkbox.name',
descriptionKey: 'extensions.checkbox.description'
}
}
/**
* 注册所有扩展工厂到管理器
* @param manager 扩展管理器实例
*/
export function registerAllExtensions(manager: ExtensionManager): void {
Object.entries(EXTENSION_CONFIGS).forEach(([id, config]) => {
manager.registerExtension(id as ExtensionID, config.factory)
})
}
/**
* 获取扩展工厂的显示名称
* @param id 扩展ID
* @returns 显示名称
*/
export function getExtensionDisplayName(id: ExtensionID): string {
const config = EXTENSION_CONFIGS[id as ExtensionID]
return config?.displayNameKey ? i18n.global.t(config.displayNameKey) : id
}
/**
* 获取扩展工厂的描述
* @param id 扩展ID
* @returns 描述
*/
export function getExtensionDescription(id: ExtensionID): string {
const config = EXTENSION_CONFIGS[id as ExtensionID]
return config?.descriptionKey ? i18n.global.t(config.descriptionKey) : ''
}
/**
* 获取扩展工厂实例
* @param id 扩展ID
* @returns 扩展工厂实例
*/
export function getExtensionFactory(id: ExtensionID): ExtensionFactory | undefined {
return EXTENSION_CONFIGS[id as ExtensionID]?.factory
}
/**
* 获取扩展的默认配置
* @param id 扩展ID
* @returns 默认配置对象
*/
export function getExtensionDefaultConfig(id: ExtensionID): any {
const factory = getExtensionFactory(id)
return factory?.getDefaultConfig() || {}
}
/**
* 检查扩展是否有配置项
* @param id 扩展ID
* @returns 是否有配置项
*/
export function hasExtensionConfig(id: ExtensionID): boolean {
const defaultConfig = getExtensionDefaultConfig(id)
return Object.keys(defaultConfig).length > 0
}
/**
* 获取所有可用扩展的ID列表
* @returns 扩展ID数组
*/
export function getAllExtensionIds(): ExtensionID[] {
return Object.keys(EXTENSION_CONFIGS) as ExtensionID[]
}

View File

@@ -0,0 +1,63 @@
import {Extension} from '@codemirror/state'
import {EditorView} from '@codemirror/view'
import {useExtensionStore} from '@/stores/extensionStore'
import {ExtensionManager} from './ExtensionManager'
import {registerAllExtensions} from './factories'
/**
* 全局扩展管理器实例
*/
const extensionManager = new ExtensionManager()
/**
* 异步创建动态扩展
* 确保扩展配置已加载
* @param documentId 可选的文档ID用于提前初始化视图
*/
export const createDynamicExtensions = async (documentId?: number): Promise<Extension[]> => {
const extensionStore = useExtensionStore()
// 注册所有扩展工厂
registerAllExtensions(extensionManager)
// 确保扩展配置已加载
if (extensionStore.extensions.length === 0) {
await extensionStore.loadExtensions()
}
// 初始化扩展管理器配置
extensionManager.initializeExtensionsFromConfig(extensionStore.extensions)
// 获取初始扩展配置
return extensionManager.getInitialExtensions()
}
/**
* 获取扩展管理器实例
* @returns 扩展管理器
*/
export const getExtensionManager = (): ExtensionManager => {
return extensionManager
}
/**
* 设置编辑器视图到扩展管理器
* @param view 编辑器视图
* @param documentId 文档ID
*/
export const setExtensionManagerView = (view: EditorView, documentId: number): void => {
extensionManager.setView(view, documentId)
}
/**
* 从扩展管理器移除编辑器视图
* @param documentId 文档ID
*/
export const removeExtensionManagerView = (documentId: number): void => {
extensionManager.removeView(documentId)
}
// 导出相关模块
export {ExtensionManager} from './ExtensionManager'
export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './factories'
export type {ExtensionFactory} from './ExtensionManager'

View File

@@ -2,203 +2,205 @@ import {EditorView} from '@codemirror/view';
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language';
import {tags} from '@lezer/highlight';
const colors = {
// 基础色调
background: '#252B37', // 主背景色
// backgroundAlt: '#252B37', // 交替背景色
backgroundSecondary: '#213644', // 次要背景色
surface: '#474747', // 面板背景
// 默认深色主题颜色
export const defaultDarkColors = {
// 基础色调
background: '#252B37', // 背景色
backgroundSecondary: '#213644', // 次要背景色
surface: '#474747', // 面板背景
// 文本颜色
foreground: '#9BB586', // 主文本色
foregroundSecondary: '#9c9c9c', // 次要文本色
comment: '#6272a4', // 注释色
// 文本颜色
foreground: '#9BB586', // 主文本色
foregroundSecondary: '#9c9c9c', // 次要文本色
comment: '#6272a4', // 注释色
// 语法高亮色
keyword: '#ff79c6', // 关键字
string: '#f1fa8c', // 字符串
function: '#50fa7b', // 函数名
number: '#bd93f9', // 数字
operator: '#ff79c6', // 操作符
variable: '#8fbcbb', // 变量
type: '#8be9fd', // 类型
// 语法高亮色
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)', // 活动行号
// 界面元素
cursor: '#ffffff', // 光标
selection: '#0865a9', // 选中背景
selectionBlur: '#225377', // 失焦选中背景
activeLine: '#ffffff0a', // 当前行高亮
lineNumber: '#ffffff26', // 行号
activeLineNumber: '#ffffff99', // 活动行号
// 边框和分割线
border: '#1e222a', // 边框色
borderLight: 'rgba(255,255,255, 0.1)', // 浅色边框
// 边框和分割线
borderColor: '#1e222a', // 边框色
borderLight: '#ffffff19', // 浅色边框
// 搜索和匹配
searchMatch: '#8fbcbb', // 搜索匹配
matchingBracket: 'rgba(255,255,255,0.1)', // 匹配括号
// 搜索和匹配
searchMatch: '#8fbcbb', // 搜索匹配
matchingBracket: '#ffffff19', // 匹配括号
};
const darkTheme = EditorView.theme({
// 创建深色主题
export function createDarkTheme(colors = defaultDarkColors) {
const darkTheme = EditorView.theme({
'&': {
color: colors.foreground,
backgroundColor: colors.background,
color: colors.foreground,
backgroundColor: colors.background,
},
// 确保编辑器容器背景一致
'.cm-editor': {
backgroundColor: colors.background,
backgroundColor: colors.background,
},
// 确保滚动区域背景一致
'.cm-scroller': {
backgroundColor: colors.background,
backgroundColor: colors.background,
},
// 编辑器内容
'.cm-content': {
caretColor: colors.cursor,
paddingTop: '4px',
caretColor: colors.cursor,
paddingTop: '4px',
},
// 光标
'.cm-cursor, .cm-dropCursor': {
borderLeftColor: colors.cursor,
borderLeftWidth: '2px',
paddingTop: '4px',
marginTop: '-2px',
borderLeftColor: colors.cursor,
borderLeftWidth: '2px',
paddingTop: '4px',
marginTop: '-2px',
},
// 选择
'.cm-selectionBackground': {
backgroundColor: colors.selectionBlur,
backgroundColor: colors.selectionBlur,
},
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
backgroundColor: colors.selection,
backgroundColor: colors.selection,
},
'.cm-activeLine.code-empty-block-selected': {
backgroundColor: colors.selection,
backgroundColor: colors.selection,
},
// 当前行高亮
'.cm-activeLine': {
backgroundColor: colors.activeLine
backgroundColor: colors.activeLine
},
// 行号区域
'.cm-gutters': {
backgroundColor: 'rgba(0,0,0, 0.1)',
color: colors.lineNumber,
border: 'none',
padding: '0 2px 0 4px',
userSelect: 'none',
backgroundColor: 'rgba(0,0,0, 0.1)',
color: colors.lineNumber,
border: 'none',
padding: '0 2px 0 4px',
userSelect: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: 'transparent',
color: colors.activeLineNumber,
backgroundColor: 'transparent',
color: colors.activeLineNumber,
},
// 折叠功能
'.cm-foldGutter': {
marginLeft: '0px',
marginLeft: '0px',
},
'.cm-foldGutter .cm-gutterElement': {
opacity: 0,
transition: 'opacity 400ms',
opacity: 0,
transition: 'opacity 400ms',
},
'.cm-gutters:hover .cm-gutterElement': {
opacity: 1,
opacity: 1,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
// 搜索匹配
'.cm-searchMatch': {
backgroundColor: 'transparent',
outline: `1px solid ${colors.searchMatch}`,
backgroundColor: 'transparent',
outline: `1px solid ${colors.searchMatch}`,
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: colors.foreground,
color: colors.background,
backgroundColor: colors.foreground,
color: colors.background,
},
'.cm-selectionMatch': {
backgroundColor: '#50606D',
backgroundColor: '#50606D',
},
// 括号匹配
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
outline: `0.5px solid ${colors.searchMatch}`,
outline: `0.5px solid ${colors.searchMatch}`,
},
'&.cm-focused .cm-matchingBracket': {
backgroundColor: colors.matchingBracket,
color: 'inherit',
backgroundColor: colors.matchingBracket,
color: 'inherit',
},
'&.cm-focused .cm-nonmatchingBracket': {
outline: '0.5px solid #bc8f8f',
outline: '0.5px solid #bc8f8f',
},
// 编辑器焦点
'&.cm-editor.cm-focused': {
outline: 'none',
outline: 'none',
},
// 工具提示
'.cm-tooltip': {
border: 'none',
backgroundColor: colors.surface,
border: 'none',
backgroundColor: colors.surface,
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: colors.surface,
borderBottomColor: colors.surface,
borderTopColor: colors.surface,
borderBottomColor: colors.surface,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: colors.activeLine,
color: colors.foreground,
},
'& > ul > li[aria-selected]': {
backgroundColor: colors.activeLine,
color: colors.foreground,
},
},
// 代码块层
'.code-blocks-layer': {
width: '100%',
width: '100%',
},
'.code-blocks-layer .block-even, .code-blocks-layer .block-odd': {
width: '100%',
boxSizing: 'content-box',
width: '100%',
boxSizing: 'content-box',
},
'.code-blocks-layer .block-even': {
background: colors.background,
borderTop: `1px solid ${colors.border}`,
background: colors.background,
borderTop: `1px solid ${colors.borderColor}`,
},
'.code-blocks-layer .block-even:first-child': {
borderTop: 'none',
borderTop: 'none',
},
'.code-blocks-layer .block-odd': {
background: colors.backgroundSecondary,
borderTop: `1px solid ${colors.border}`,
background: colors.backgroundSecondary,
borderTop: `1px solid ${colors.borderColor}`,
},
// 代码块开始标记
'.code-block-start': {
height: '12px',
position: 'relative',
height: '12px',
position: 'relative',
},
'.code-block-start.first': {
height: '0px',
height: '0px',
},
}, {dark: true});
}, {dark: true});
// 语法高亮样式
const darkHighlightStyle = HighlightStyle.define([
// 语法高亮样式
const darkHighlightStyle = HighlightStyle.define([
{tag: tags.keyword, color: colors.keyword},
{tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], color: colors.variable},
{tag: [tags.variableName], color: colors.variable},
@@ -229,9 +231,13 @@ const darkHighlightStyle = HighlightStyle.define([
{tag: [tags.heading1, tags.heading2], fontSize: '1.4em'},
{tag: [tags.heading3, tags.heading4], fontSize: '1.2em'},
{tag: [tags.heading5, tags.heading6], fontSize: '1.1em'},
]);
]);
export const dark = [
return [
darkTheme,
syntaxHighlighting(darkHighlightStyle),
];
];
}
// 默认深色主题
export const dark = createDarkTheme(defaultDarkColors);

View File

@@ -2,10 +2,10 @@ import { EditorView } from '@codemirror/view';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags } from '@lezer/highlight';
const colors = {
// 默认浅色主题颜色
export const defaultLightColors = {
// 基础色调
background: '#ffffff', // 主背景色
// backgroundAlt: '#f4f8f4', // 交替背景色
backgroundSecondary: '#f1faf1', // 次要背景色
surface: '#f5f5f5', // 面板背景
@@ -24,216 +24,221 @@ const colors = {
type: '#6f42c1', // 类型
// 界面元素
cursor: '#000', // 光标
selection: '#77baff8c', // 选中背景
selectionBlur: '#b2c2ca85', // 失焦选中背景
activeLine: 'rgba(0,0,0, 0.04)', // 当前行高亮
lineNumber: 'rgba(0,0,0, 0.25)', // 行号
activeLineNumber: 'rgba(0,0,0, 0.6)', // 活动行号
cursor: '#000000', // 光标
selection: '#77baff', // 选中背景
selectionBlur: '#b2c2ca', // 失焦选中背景
activeLine: '#0000000a', // 当前行高亮
lineNumber: '#00000040', // 行号
activeLineNumber: '#000000aa', // 活动行号
// 边框和分割线
border: '#dfdfdf', // 边框色
borderLight: 'rgba(0,0,0, 0.05)', // 浅色边框
borderColor: '#dfdfdf', // 边框色
borderLight: '#0000000c', // 浅色边框
// 搜索和匹配
searchMatch: '#005cc5', // 搜索匹配
matchingBracket: 'rgba(0,0,0,0.1)', // 匹配括号
matchingBracket: '#00000019', // 匹配括号
};
const lightTheme = EditorView.theme({
'&': {
color: colors.foreground,
backgroundColor: colors.background,
},
// 确保编辑器容器背景一致
'.cm-editor': {
backgroundColor: colors.background,
},
// 确保滚动区域背景一致
'.cm-scroller': {
backgroundColor: colors.background,
},
// 编辑器内容
'.cm-content': {
caretColor: colors.cursor,
paddingTop: '4px',
},
// 光标
'.cm-cursor, .cm-dropCursor': {
borderLeftColor: colors.cursor,
borderLeftWidth: '2px',
paddingTop: '4px',
marginTop: '-2px',
},
// 选择
'.cm-selectionBackground': {
backgroundColor: colors.selectionBlur,
},
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
backgroundColor: colors.selection,
},
'.cm-activeLine.code-empty-block-selected': {
backgroundColor: colors.selection,
},
// 当前行高亮
'.cm-activeLine': {
backgroundColor: colors.activeLine
},
// 行号区域
'.cm-gutters': {
backgroundColor: 'rgba(0,0,0, 0.04)',
color: colors.lineNumber,
border: 'none',
borderRight: `1px solid ${colors.borderLight}`,
padding: '0 2px 0 4px',
userSelect: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: 'transparent',
color: colors.activeLineNumber,
},
// 折叠功能
'.cm-foldGutter': {
marginLeft: '0px',
},
'.cm-foldGutter .cm-gutterElement': {
opacity: 0,
transition: 'opacity 400ms',
},
'.cm-gutters:hover .cm-gutterElement': {
opacity: 1,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: colors.comment,
},
// 搜索匹配
'.cm-searchMatch': {
backgroundColor: 'transparent',
outline: `1px solid ${colors.searchMatch}`,
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: colors.searchMatch,
color: colors.background,
},
'.cm-selectionMatch': {
backgroundColor: '#e6f3ff',
},
// 括号匹配
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
outline: `0.5px solid ${colors.searchMatch}`,
},
'&.cm-focused .cm-matchingBracket': {
backgroundColor: colors.matchingBracket,
color: 'inherit',
},
'&.cm-focused .cm-nonmatchingBracket': {
outline: '0.5px solid #d73a49',
},
// 编辑器焦点
'&.cm-editor.cm-focused': {
outline: 'none',
},
// 工具提示
'.cm-tooltip': {
border: 'none',
backgroundColor: colors.surface,
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: colors.surface,
borderBottomColor: colors.surface,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: colors.activeLine,
// 创建浅色主题
export function createLightTheme(colors = defaultLightColors) {
const lightTheme = EditorView.theme({
'&': {
color: colors.foreground,
backgroundColor: colors.background,
},
},
// 代码块层
'.code-blocks-layer': {
width: '100%',
},
'.code-blocks-layer .block-even, .code-blocks-layer .block-odd': {
width: '100%',
boxSizing: 'content-box',
},
'.code-blocks-layer .block-even': {
background: colors.background,
borderTop: `1px solid ${colors.border}`,
},
'.code-blocks-layer .block-even:first-child': {
borderTop: 'none',
},
'.code-blocks-layer .block-odd': {
background: colors.backgroundSecondary,
borderTop: `1px solid ${colors.border}`,
},
// 确保编辑器容器背景一致
'.cm-editor': {
backgroundColor: colors.background,
},
// 代码块开始标记
'.code-block-start': {
height: '12px',
},
'.code-block-start.first': {
height: '0px',
},
}, { dark: false });
// 确保滚动区域背景一致
'.cm-scroller': {
backgroundColor: colors.background,
},
// 语法高亮样式
const lightHighlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: colors.keyword },
{ tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], color: colors.variable },
{ tag: [tags.variableName], color: colors.variable },
{ tag: [tags.function(tags.variableName)], color: colors.function },
{ tag: [tags.labelName], color: colors.operator },
{ tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: colors.keyword },
{ tag: [tags.definition(tags.name), tags.separator], color: colors.function },
{ tag: [tags.brace], color: colors.variable },
{ tag: [tags.annotation], color: '#d73a49' },
{ tag: [tags.number, tags.changed, tags.annotation, tags.modifier, tags.self, tags.namespace], color: colors.number },
{ tag: [tags.typeName, tags.className], color: colors.type },
{ tag: [tags.operator, tags.operatorKeyword], color: colors.operator },
{ tag: [tags.tagName], color: colors.type },
{ tag: [tags.squareBracket], color: colors.keyword },
{ tag: [tags.angleBracket], color: colors.operator },
{ tag: [tags.attributeName], color: colors.variable },
{ tag: [tags.regexp], color: colors.string },
{ tag: [tags.quote], color: colors.comment },
{ tag: [tags.string], color: colors.string },
{ tag: tags.link, color: colors.function, textDecoration: 'underline' },
{ tag: [tags.url, tags.escape, tags.special(tags.string)], color: colors.string },
{ tag: [tags.meta], color: colors.comment },
{ tag: [tags.comment], color: colors.comment, fontStyle: 'italic' },
{ tag: tags.strong, fontWeight: 'bold' },
{ tag: tags.emphasis, fontStyle: 'italic' },
{ tag: tags.strikethrough, textDecoration: 'line-through' },
{ tag: tags.heading, fontWeight: 'bold', color: colors.keyword },
{ tag: [tags.heading1, tags.heading2], fontSize: '1.4em' },
{ tag: [tags.heading3, tags.heading4], fontSize: '1.2em' },
{ tag: [tags.heading5, tags.heading6], fontSize: '1.1em' },
]);
// 编辑器内容
'.cm-content': {
caretColor: colors.cursor,
paddingTop: '4px',
},
export const light = [
lightTheme,
syntaxHighlighting(lightHighlightStyle),
];
// 光标
'.cm-cursor, .cm-dropCursor': {
borderLeftColor: colors.cursor,
borderLeftWidth: '2px',
paddingTop: '4px',
marginTop: '-2px',
},
// 选择
'.cm-selectionBackground': {
backgroundColor: colors.selectionBlur,
},
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
backgroundColor: colors.selection,
},
'.cm-activeLine.code-empty-block-selected': {
backgroundColor: colors.selection,
},
// 当前行高亮
'.cm-activeLine': {
backgroundColor: colors.activeLine
},
// 行号区域
'.cm-gutters': {
backgroundColor: 'rgba(0,0,0, 0.04)',
color: colors.lineNumber,
border: 'none',
borderRight: `1px solid ${colors.borderLight}`,
padding: '0 2px 0 4px',
userSelect: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: 'transparent',
color: colors.activeLineNumber,
},
// 折叠功能
'.cm-foldGutter': {
marginLeft: '0px',
},
'.cm-foldGutter .cm-gutterElement': {
opacity: 0,
transition: 'opacity 400ms',
},
'.cm-gutters:hover .cm-gutterElement': {
opacity: 1,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: colors.comment,
},
// 搜索匹配
'.cm-searchMatch': {
backgroundColor: 'transparent',
outline: `1px solid ${colors.searchMatch}`,
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: colors.searchMatch,
color: colors.background,
},
'.cm-selectionMatch': {
backgroundColor: '#e6f3ff',
},
// 括号匹配
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
outline: `0.5px solid ${colors.searchMatch}`,
},
'&.cm-focused .cm-matchingBracket': {
backgroundColor: colors.matchingBracket,
color: 'inherit',
},
'&.cm-focused .cm-nonmatchingBracket': {
outline: '0.5px solid #d73a49',
},
// 编辑器焦点
'&.cm-editor.cm-focused': {
outline: 'none',
},
// 工具提示
'.cm-tooltip': {
border: 'none',
backgroundColor: colors.surface,
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: colors.surface,
borderBottomColor: colors.surface,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: colors.activeLine,
color: colors.foreground,
},
},
// 代码块层
'.code-blocks-layer': {
width: '100%',
},
'.code-blocks-layer .block-even, .code-blocks-layer .block-odd': {
width: '100%',
boxSizing: 'content-box',
},
'.code-blocks-layer .block-even': {
background: colors.background,
borderTop: `1px solid ${colors.borderColor}`,
},
'.code-blocks-layer .block-even:first-child': {
borderTop: 'none',
},
'.code-blocks-layer .block-odd': {
background: colors.backgroundSecondary,
borderTop: `1px solid ${colors.borderColor}`,
},
// 代码块开始标记
'.code-block-start': {
height: '12px',
},
'.code-block-start.first': {
height: '0px',
},
}, { dark: false });
// 语法高亮样式
const lightHighlightStyle = HighlightStyle.define([
{ tag: tags.keyword, color: colors.keyword },
{ tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], color: colors.variable },
{ tag: [tags.variableName], color: colors.variable },
{ tag: [tags.function(tags.variableName)], color: colors.function },
{ tag: [tags.labelName], color: colors.operator },
{ tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: colors.keyword },
{ tag: [tags.definition(tags.name), tags.separator], color: colors.function },
{ tag: [tags.brace], color: colors.variable },
{ tag: [tags.annotation], color: '#d73a49' },
{ tag: [tags.number, tags.changed, tags.annotation, tags.modifier, tags.self, tags.namespace], color: colors.number },
{ tag: [tags.typeName, tags.className], color: colors.type },
{ tag: [tags.operator, tags.operatorKeyword], color: colors.operator },
{ tag: [tags.tagName], color: colors.type },
{ tag: [tags.squareBracket], color: colors.keyword },
{ tag: [tags.angleBracket], color: colors.operator },
{ tag: [tags.attributeName], color: colors.variable },
{ tag: [tags.regexp], color: colors.string },
{ tag: [tags.quote], color: colors.comment },
{ tag: [tags.string], color: colors.string },
{ tag: tags.link, color: colors.function, textDecoration: 'underline' },
{ tag: [tags.url, tags.escape, tags.special(tags.string)], color: colors.string },
{ tag: [tags.meta], color: colors.comment },
{ tag: [tags.comment], color: colors.comment, fontStyle: 'italic' },
{ tag: tags.strong, fontWeight: 'bold' },
{ tag: tags.emphasis, fontStyle: 'italic' },
{ tag: tags.strikethrough, textDecoration: 'line-through' },
{ tag: tags.heading, fontWeight: 'bold', color: colors.keyword },
{ tag: [tags.heading1, tags.heading2], fontSize: '1.4em' },
{ tag: [tags.heading3, tags.heading4], fontSize: '1.2em' },
{ tag: [tags.heading5, tags.heading6], fontSize: '1.1em' },
]);
return [
lightTheme,
syntaxHighlighting(lightHighlightStyle),
];
}
// 默认浅色主题
export const light = createLightTheme(defaultLightColors);

View File

@@ -13,6 +13,7 @@ const navItems = [
{ id: 'general', icon: '⚙️', route: '/settings/general' },
{ id: 'editing', icon: '✏️', route: '/settings/editing' },
{ id: 'appearance', icon: '🎨', route: '/settings/appearance' },
{ id: 'extensions', icon: '🧩', route: '/settings/extensions' },
{ id: 'keyBindings', icon: '⌨️', route: '/settings/key-bindings' },
{ id: 'updates', icon: '🔄', route: '/settings/updates' }
];

View File

@@ -6,7 +6,12 @@ defineProps<{
<template>
<div class="setting-section">
<h2 class="section-title">{{ title }}</h2>
<div class="section-header">
<h2 class="section-title">{{ title }}</h2>
<div class="section-title-right">
<slot name="title-right"></slot>
</div>
</div>
<div class="section-content">
<slot></slot>
</div>
@@ -22,14 +27,25 @@ defineProps<{
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
border: 1px solid var(--settings-border);
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
background-color: var(--settings-hover);
border-bottom: 1px solid var(--settings-border);
}
.section-title {
font-size: 13px;
font-weight: 600;
margin: 0;
padding: 10px 14px;
background-color: var(--settings-hover);
color: var(--settings-text);
border-bottom: 1px solid var(--settings-border);
}
.section-title-right {
display: flex;
align-items: center;
}
.section-content {

View File

@@ -2,14 +2,260 @@
import { useConfigStore } from '@/stores/configStore';
import { useThemeStore } from '@/stores/themeStore';
import { useI18n } from 'vue-i18n';
import { computed, watch, onMounted, ref } from 'vue';
import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import { SystemThemeType, LanguageType } from '@/../bindings/voidraft/internal/models/models';
import { defaultDarkColors } from '@/views/editor/theme/dark';
import { defaultLightColors } from '@/views/editor/theme/light';
import PickColors from 'vue-pick-colors';
const { t } = useI18n();
const configStore = useConfigStore();
const themeStore = useThemeStore();
// 添加临时颜色状态
const tempColors = ref({
darkTheme: { ...configStore.config.appearance.customTheme?.darkTheme || defaultDarkColors },
lightTheme: { ...configStore.config.appearance.customTheme?.lightTheme || defaultLightColors }
});
// 标记是否有未保存的更改
const hasUnsavedChanges = ref(false);
// 重置按钮状态
const resetButtonState = ref({
confirming: false,
timer: null as number | null
});
// 防抖函数
const debounce = <T extends (...args: any[]) => any>(
func: T,
wait: number
): ((...args: Parameters<T>) => void) => {
let timeout: number | undefined;
return function(...args: Parameters<T>): void {
clearTimeout(timeout);
timeout = window.setTimeout(() => {
func(...args);
}, wait);
};
};
// 当前激活的主题类型(基于当前系统主题)
const activeThemeType = computed(() => {
const isDark =
themeStore.currentTheme === SystemThemeType.SystemThemeDark ||
(themeStore.currentTheme === SystemThemeType.SystemThemeAuto &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
return isDark ? 'darkTheme' : 'lightTheme';
});
// 当前主题的颜色配置 - 使用临时状态
const currentColors = computed(() => {
const themeType = activeThemeType.value;
return tempColors.value[themeType] ||
(themeType === 'darkTheme' ? defaultDarkColors : defaultLightColors);
});
// 获取当前主题模式
const currentThemeMode = computed(() => {
const isDark =
themeStore.currentTheme === SystemThemeType.SystemThemeDark ||
(themeStore.currentTheme === SystemThemeType.SystemThemeAuto &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
return isDark ? 'dark' : 'light';
});
// 监听配置变更,更新临时颜色
watch(
() => configStore.config.appearance.customTheme,
(newValue) => {
if (!hasUnsavedChanges.value) {
tempColors.value = {
darkTheme: { ...newValue.darkTheme },
lightTheme: { ...newValue.lightTheme }
};
}
},
{ deep: true, immediate: true }
);
// 初始化时加载主题颜色
onMounted(() => {
// 使用themeStore中的颜色作为初始值
tempColors.value = {
darkTheme: { ...themeStore.themeColors.darkTheme },
lightTheme: { ...themeStore.themeColors.lightTheme }
};
});
// 颜色配置分组
const colorGroups = computed(() => [
{
key: 'basic',
title: t('settings.themeColors.basic'),
colors: [
{ key: 'background', label: t('settings.themeColors.background') },
{ key: 'backgroundSecondary', label: t('settings.themeColors.backgroundSecondary') },
{ key: 'surface', label: t('settings.themeColors.surface') }
]
},
{
key: 'text',
title: t('settings.themeColors.text'),
colors: [
{ key: 'foreground', label: t('settings.themeColors.foreground') },
{ key: 'foregroundSecondary', label: t('settings.themeColors.foregroundSecondary') },
{ key: 'comment', label: t('settings.themeColors.comment') }
]
},
{
key: 'syntax',
title: t('settings.themeColors.syntax'),
colors: [
{ key: 'keyword', label: t('settings.themeColors.keyword') },
{ key: 'string', label: t('settings.themeColors.string') },
{ key: 'function', label: t('settings.themeColors.function') },
{ key: 'number', label: t('settings.themeColors.number') },
{ key: 'operator', label: t('settings.themeColors.operator') },
{ key: 'variable', label: t('settings.themeColors.variable') },
{ key: 'type', label: t('settings.themeColors.type') }
]
},
{
key: 'interface',
title: t('settings.themeColors.interface'),
colors: [
{ key: 'cursor', label: t('settings.themeColors.cursor') },
{ key: 'selection', label: t('settings.themeColors.selection') },
{ key: 'selectionBlur', label: t('settings.themeColors.selectionBlur') },
{ key: 'activeLine', label: t('settings.themeColors.activeLine') },
{ key: 'lineNumber', label: t('settings.themeColors.lineNumber') },
{ key: 'activeLineNumber', label: t('settings.themeColors.activeLineNumber') }
]
},
{
key: 'border',
title: t('settings.themeColors.border'),
colors: [
{ key: 'borderColor', label: t('settings.themeColors.borderColor') },
{ key: 'borderLight', label: t('settings.themeColors.borderLight') }
]
},
{
key: 'search',
title: t('settings.themeColors.search'),
colors: [
{ key: 'searchMatch', label: t('settings.themeColors.searchMatch') },
{ key: 'matchingBracket', label: t('settings.themeColors.matchingBracket') }
]
}
]);
// 处理重置按钮点击
const handleResetClick = () => {
if (resetButtonState.value.confirming) {
// 如果已经在确认状态,执行重置操作
resetCurrentTheme();
// 重置按钮状态
resetButtonState.value.confirming = false;
if (resetButtonState.value.timer !== null) {
clearTimeout(resetButtonState.value.timer);
resetButtonState.value.timer = null;
}
} else {
// 进入确认状态
resetButtonState.value.confirming = true;
// 设置3秒后自动恢复
resetButtonState.value.timer = window.setTimeout(() => {
resetButtonState.value.confirming = false;
resetButtonState.value.timer = null;
}, 3000);
}
};
// 重置当前主题为默认配置
const resetCurrentTheme = debounce(async () => {
// 使用themeStore的原子重置操作
const themeType = activeThemeType.value;
const success = await themeStore.resetThemeColors(themeType);
if (success) {
// 更新临时颜色状态
tempColors.value = {
darkTheme: { ...themeStore.themeColors.darkTheme },
lightTheme: { ...themeStore.themeColors.lightTheme }
};
// 标记没有未保存的更改
hasUnsavedChanges.value = false;
}
}, 300);
// 更新本地颜色配置 - 仅更新临时状态,不提交到后端
const updateLocalColor = (colorKey: string, value: string) => {
const themeType = activeThemeType.value;
// 更新临时颜色
tempColors.value = {
...tempColors.value,
[themeType]: {
...tempColors.value[themeType],
[colorKey]: value
}
};
// 标记有未保存的更改
hasUnsavedChanges.value = true;
};
// 防抖包装的颜色更新函数
const updateColor = debounce(updateLocalColor, 100);
// 应用颜色更改到系统
const applyChanges = async () => {
try {
// 获取当前主题的自定义颜色
const customTheme = {
darkTheme: tempColors.value.darkTheme,
lightTheme: tempColors.value.lightTheme
};
// 更新themeStore中的颜色
themeStore.updateThemeColors(customTheme.darkTheme, customTheme.lightTheme);
// 保存到配置
await themeStore.saveThemeColors();
// 刷新编辑器主题
themeStore.refreshEditorTheme();
// 清除未保存标记
hasUnsavedChanges.value = false;
} catch (error) {
console.error('Failed to apply theme change:', error);
}
};
// 取消颜色更改
const cancelChanges = () => {
// 恢复到themeStore中的颜色
tempColors.value = {
darkTheme: { ...themeStore.themeColors.darkTheme },
lightTheme: { ...themeStore.themeColors.lightTheme }
};
// 清除未保存标记
hasUnsavedChanges.value = false;
};
// 语言选项
const languageOptions = [
{ value: LanguageType.LangZhCN, label: t('languages.zh-CN') },
@@ -38,6 +284,24 @@ const updateSystemTheme = async (event: Event) => {
await themeStore.setTheme(selectedSystemTheme);
};
// 控制颜色选择器显示状态
const showPickerMap = ref<Record<string, boolean>>({});
// 切换颜色选择器显示状态
const toggleColorPicker = (colorKey: string) => {
showPickerMap.value[colorKey] = !showPickerMap.value[colorKey];
};
// 颜色变更处理
const handleColorChange = (colorKey: string, value: string) => {
updateColor(colorKey, value);
};
// 颜色选择器关闭处理
const handlePickerClose = () => {
// 可以在此添加额外的逻辑
};
</script>
<template>
@@ -61,6 +325,70 @@ const updateSystemTheme = async (event: Event) => {
</select>
</SettingItem>
</SettingSection>
<!-- 自定义主题颜色配置 -->
<SettingSection :title="t('settings.customThemeColors')">
<template #title-right>
<div class="theme-controls">
<button
v-if="!hasUnsavedChanges"
:class="['reset-button', resetButtonState.confirming ? 'reset-button-confirming' : '']"
@click="handleResetClick"
>
{{ resetButtonState.confirming ? t('settings.confirmReset') : t('settings.resetToDefault') }}
</button>
<template v-else>
<button class="apply-button" @click="applyChanges">
{{ t('settings.apply') }}
</button>
<button class="cancel-button" @click="cancelChanges">
{{ t('settings.cancel') }}
</button>
</template>
</div>
</template>
<div class="color-groups">
<div v-for="group in colorGroups" :key="group.key" class="color-group">
<h4 class="group-title">{{ group.title }}</h4>
<div class="color-items">
<SettingItem
v-for="color in group.colors"
:key="color.key"
:title="color.label"
class="color-setting-item"
>
<div class="color-input-wrapper">
<div class="color-picker-wrapper">
<PickColors
v-model:value="currentColors[color.key]"
v-model:show-picker="showPickerMap[color.key]"
:size="28"
show-alpha
:theme="currentThemeMode"
:colors="[]"
format="hex"
:format-options="['rgb', 'hex', 'hsl', 'hsv']"
placement="bottom"
position="absolute"
:z-index="1000"
@change="(val) => handleColorChange(color.key, val)"
@close-picker="handlePickerClose"
/>
</div>
<input
type="text"
:value="currentColors[color.key] || ''"
@input="updateColor(color.key, ($event.target as HTMLInputElement).value)"
class="color-text-input"
:placeholder="t('settings.colorValue')"
/>
</div>
</SettingItem>
</div>
</div>
</div>
</SettingSection>
</div>
</template>
@@ -95,4 +423,146 @@ const updateSystemTheme = async (event: Event) => {
color: var(--settings-text);
}
}
// 主题控制区域
.theme-controls {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
// 主题颜色配置样式
.reset-button, .apply-button, .cancel-button {
padding: 6px 12px;
font-size: 12px;
border: 1px solid var(--settings-input-border);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
border-color: #4a9eff;
}
&:active {
transform: translateY(1px);
}
}
.reset-button {
background-color: var(--settings-button-bg);
color: var(--settings-button-text);
&:hover {
background-color: var(--settings-button-hover-bg);
}
&.reset-button-confirming {
background-color: #e74c3c;
color: white;
border-color: #c0392b;
&:hover {
background-color: #c0392b;
}
}
}
.apply-button {
background-color: #4a9eff;
color: white;
font-weight: 500;
&:hover {
background-color: #3a8eef;
}
}
.cancel-button {
background-color: var(--settings-button-bg);
color: var(--settings-button-text);
&:hover {
background-color: var(--settings-button-hover-bg);
}
}
.color-groups {
display: flex;
flex-direction: column;
gap: 24px;
}
.color-group {
.group-title {
font-size: 14px;
font-weight: 600;
color: var(--settings-text);
margin: 0 0 12px 0;
padding-bottom: 6px;
border-bottom: 1px solid var(--settings-input-border);
}
.color-items {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 8px;
}
}
.color-setting-item {
:deep(.setting-item-content) {
align-items: center;
}
:deep(.setting-item-title) {
font-size: 12px;
min-width: 120px;
}
}
.color-input-wrapper {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
}
.color-picker-wrapper {
display: flex;
align-items: center;
height: 28px;
cursor: pointer;
:deep(.pick-colors-trigger) {
border: 1px solid var(--settings-input-border);
border-radius: 4px;
overflow: hidden;
}
}
.color-text-input {
flex: 1;
min-width: 160px;
padding: 4px 8px;
border: 1px solid var(--settings-input-border);
border-radius: 4px;
background-color: var(--settings-input-bg);
color: var(--settings-text);
font-size: 11px;
font-family: 'Courier New', monospace;
transition: border-color 0.2s ease;
height: 28px;
box-sizing: border-box;
&:focus {
outline: none;
border-color: #4a9eff;
}
&::placeholder {
color: var(--settings-text-secondary);
}
}
</style>

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import { useConfigStore } from '@/stores/configStore';
import { FONT_OPTIONS } from '@/stores/configStore';
import { useI18n } from 'vue-i18n';
import {computed, onMounted } from 'vue';
import SettingSection from '../components/SettingSection.vue';
@@ -19,7 +18,7 @@ onMounted(async () => {
});
// 字体选择选项
const fontFamilyOptions = FONT_OPTIONS;
const fontFamilyOptions = computed(() => configStore.localizedFontOptions);
const currentFontFamily = computed(() => configStore.config.editing.fontFamily);
// 字体选择
@@ -33,15 +32,15 @@ const handleFontFamilyChange = async (event: Event) => {
// 字体粗细选项
const fontWeightOptions = [
{ value: '100', label: '极细 (100)' },
{ value: '200', label: '超细 (200)' },
{ value: '300', label: '细 (300)' },
{ value: 'normal', label: '正常 (400)' },
{ value: '500', label: '中等 (500)' },
{ value: '600', label: '半粗 (600)' },
{ value: 'bold', label: '粗体 (700)' },
{ value: '800', label: '超粗 (800)' },
{ value: '900', label: '极粗 (900)' }
{ value: '100', label: t('settings.fontWeights.100') },
{ value: '200', label: t('settings.fontWeights.200') },
{ value: '300', label: t('settings.fontWeights.300') },
{ value: 'normal', label: t('settings.fontWeights.normal') },
{ value: '500', label: t('settings.fontWeights.500') },
{ value: '600', label: t('settings.fontWeights.600') },
{ value: 'bold', label: t('settings.fontWeights.bold') },
{ value: '800', label: t('settings.fontWeights.800') },
{ value: '900', label: t('settings.fontWeights.900') }
];
// 字体粗细选择
@@ -213,7 +212,7 @@ const handleAutoSaveDelayChange = async (event: Event) => {
</SettingSection>
<SettingSection :title="t('settings.saveOptions')">
<SettingItem :title="t('settings.autoSaveDelay')" :description="'定时保存间隔,每隔指定时间自动保存(仅在有变更时)'">
<SettingItem :title="t('settings.autoSaveDelay')">
<input
type="number"
class="number-input"

View File

@@ -0,0 +1,398 @@
<script setup lang="ts">
import {computed, ref} from 'vue'
import {useI18n} from 'vue-i18n'
import {useEditorStore} from '@/stores/editorStore'
import {useExtensionStore} from '@/stores/extensionStore'
import {ExtensionService} from '@/../bindings/voidraft/internal/services'
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models'
import {getExtensionManager} from '@/views/editor/manager'
import {
getAllExtensionIds,
getExtensionDefaultConfig,
getExtensionDescription,
getExtensionDisplayName,
hasExtensionConfig
} from '@/views/editor/manager/factories'
import SettingSection from '../components/SettingSection.vue'
import SettingItem from '../components/SettingItem.vue'
import ToggleSwitch from '../components/ToggleSwitch.vue'
const {t} = useI18n()
const editorStore = useEditorStore()
const extensionStore = useExtensionStore()
// 展开状态管理
const expandedExtensions = ref<Set<ExtensionID>>(new Set())
// 获取所有可用的扩展
const availableExtensions = computed(() => {
return getAllExtensionIds().map(id => {
const extension = extensionStore.extensions.find(ext => ext.id === id)
return {
id,
displayName: getExtensionDisplayName(id),
description: getExtensionDescription(id),
enabled: extension?.enabled || false,
isDefault: extension?.isDefault || false,
hasConfig: hasExtensionConfig(id),
config: extension?.config || {},
defaultConfig: getExtensionDefaultConfig(id)
}
})
})
// 切换展开状态
const toggleExpanded = (extensionId: ExtensionID) => {
if (expandedExtensions.value.has(extensionId)) {
expandedExtensions.value.delete(extensionId)
} else {
expandedExtensions.value.add(extensionId)
}
}
// 更新扩展状态
const updateExtension = async (extensionId: ExtensionID, enabled: boolean) => {
try {
await editorStore.updateExtension(extensionId, enabled)
} catch (error) {
console.error('Failed to update extension:', error)
}
}
// 更新扩展配置
const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string, value: any) => {
try {
// 获取当前扩展状态
const extension = extensionStore.extensions.find(ext => ext.id === extensionId)
if (!extension) return
// 更新配置
const updatedConfig = {...extension.config, [configKey]: value}
console.log(`[ExtensionsPage] 更新扩展 ${extensionId} 配置, ${configKey}=${value}`)
// 使用editorStore的updateExtension方法更新确保应用到所有编辑器实例
await editorStore.updateExtension(extensionId, extension.enabled, updatedConfig)
} catch (error) {
console.error('Failed to update extension config:', error)
}
}
// 重置扩展到默认配置
const resetExtension = async (extensionId: ExtensionID) => {
try {
// 重置到默认配置(后端)
await ExtensionService.ResetExtensionToDefault(extensionId)
// 重新加载扩展状态以获取最新配置
await extensionStore.loadExtensions()
// 获取重置后的状态,立即应用到所有编辑器视图
const extension = extensionStore.extensions.find(ext => ext.id === extensionId)
if (extension) {
// 通过editorStore更新确保所有视图都能同步
await editorStore.updateExtension(extensionId, extension.enabled, extension.config)
console.log(`[ExtensionsPage] 重置扩展 ${extensionId} 配置,同步应用到所有编辑器实例`)
}
} catch (error) {
console.error('Failed to reset extension:', error)
}
}
// 配置项类型定义
type ConfigItemType = 'toggle' | 'number' | 'text' | 'select'
interface SelectOption {
value: any
label: string
}
interface ConfigItemMeta {
type: ConfigItemType
options?: SelectOption[]
}
// 只保留 select 类型的配置项元数据
const extensionConfigMeta: Partial<Record<ExtensionID, Record<string, ConfigItemMeta>>> = {
[ExtensionID.ExtensionMinimap]: {
displayText: {
type: 'select',
options: [
{value: 'characters', label: 'Characters'},
{value: 'blocks', label: 'Blocks'}
]
},
showOverlay: {
type: 'select',
options: [
{value: 'always', label: 'Always'},
{value: 'mouse-over', label: 'Mouse Over'}
]
}
}
}
// 获取配置项类型
const getConfigItemType = (extensionId: ExtensionID, configKey: string, defaultValue: any): string => {
const meta = extensionConfigMeta[extensionId]?.[configKey]
if (meta?.type) {
return meta.type
}
// 根据默认值类型自动推断
if (typeof defaultValue === 'boolean') return 'toggle'
if (typeof defaultValue === 'number') return 'number'
return 'text'
}
// 获取选择框的选项列表
const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOption[] => {
return extensionConfigMeta[extensionId]?.[configKey]?.options || []
}
</script>
<template>
<div class="settings-page">
<SettingSection :title="t('settings.extensions')">
<div
v-for="extension in availableExtensions"
:key="extension.id"
class="extension-item"
>
<!-- 扩展主项 -->
<SettingItem
:title="extension.displayName"
:description="extension.description"
>
<div class="extension-controls">
<button
v-if="extension.hasConfig"
class="config-button"
@click="toggleExpanded(extension.id)"
:class="{ expanded: expandedExtensions.has(extension.id) }"
:title="t('settings.extensionsPage.configuration')"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path
d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</button>
<div v-else class="config-placeholder"></div>
<ToggleSwitch
:model-value="extension.enabled"
@update:model-value="updateExtension(extension.id, $event)"
/>
</div>
</SettingItem>
<!-- 可展开的配置区域 -->
<div
v-if="extension.hasConfig && expandedExtensions.has(extension.id)"
class="extension-config"
>
<!-- 配置项标题和重置按钮 -->
<div class="config-header">
<h4 class="config-title">{{ t('settings.extensionsPage.configuration') }}</h4>
<button
class="reset-button"
@click="resetExtension(extension.id)"
:title="t('settings.extensionsPage.resetToDefault')"
>
{{ t('settings.reset') }}
</button>
</div>
<div
v-for="[configKey, configValue] in Object.entries(extension.defaultConfig)"
:key="configKey"
class="config-item"
>
<SettingItem
:title="configKey"
>
<!-- 布尔值切换开关 -->
<ToggleSwitch
v-if="getConfigItemType(extension.id, configKey, configValue) === 'toggle'"
:model-value="extension.config[configKey] ?? configValue"
@update:model-value="updateExtensionConfig(extension.id, configKey, $event)"
/>
<!-- 数字输入框 -->
<input
v-else-if="getConfigItemType(extension.id, configKey, configValue) === 'number'"
type="number"
class="config-input"
:value="extension.config[configKey] ?? configValue"
:min="configKey === 'opacity' ? 0 : undefined"
:max="configKey === 'opacity' ? 1 : undefined"
:step="configKey === 'opacity' ? 0.1 : 1"
@input="updateExtensionConfig(extension.id, configKey, parseFloat(($event.target as HTMLInputElement).value))"
/>
<!-- 选择框 -->
<select
v-else-if="getConfigItemType(extension.id, configKey, configValue) === 'select'"
class="config-select"
:value="extension.config[configKey] ?? configValue"
@change="updateExtensionConfig(extension.id, configKey, ($event.target as HTMLSelectElement).value)"
>
<option
v-for="option in getSelectOptions(extension.id, configKey)"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
<!-- 文本输入框 -->
<input
v-else
type="text"
class="config-input"
:value="extension.config[configKey] ?? configValue"
@input="updateExtensionConfig(extension.id, configKey, ($event.target as HTMLInputElement).value)"
/>
</SettingItem>
</div>
</div>
</div>
</SettingSection>
</div>
</template>
<style scoped lang="scss">
.settings-page {
max-width: 1000px;
}
.extension-item {
border-bottom: 1px solid var(--settings-input-border);
&:last-child {
border-bottom: none;
}
}
.extension-controls {
display: flex;
align-items: center;
gap: 12px;
min-width: 140px;
justify-content: flex-end;
}
.config-button {
padding: 4px;
border: none;
background: none;
color: var(--settings-text-secondary);
cursor: pointer;
transition: all 0.2s ease;
border-radius: 4px;
flex-shrink: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background-color: var(--settings-hover);
color: var(--settings-text);
}
&.expanded {
color: var(--settings-accent);
background-color: var(--settings-hover);
}
svg {
transition: all 0.2s ease;
}
}
.config-placeholder {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.extension-config {
background-color: var(--settings-input-bg);
border-left: 3px solid var(--settings-accent);
margin: 8px 0 16px 0;
padding: 12px;
border-radius: 6px;
font-size: 13px;
}
.config-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.config-title {
font-size: 13px;
font-weight: 600;
color: var(--settings-text);
margin: 0;
}
.reset-button {
padding: 6px 12px;
font-size: 11px;
border: 1px solid var(--settings-input-border);
border-radius: 4px;
background-color: var(--settings-input-bg);
color: var(--settings-text-secondary);
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
&:hover {
background-color: var(--settings-hover);
color: var(--settings-text);
border-color: var(--settings-accent);
}
}
.config-item {
&:not(:last-child) {
margin-bottom: 12px;
}
/* 配置项标题和描述字体大小 */
:deep(.setting-item-title) {
font-size: 12px;
}
:deep(.setting-item-description) {
font-size: 11px;
}
}
.config-input, .config-select {
min-width: 120px;
padding: 4px 8px;
border: 1px solid var(--settings-input-border);
border-radius: 3px;
background-color: var(--settings-input-bg);
color: var(--settings-text);
font-size: 11px;
&:focus {
outline: none;
border-color: var(--settings-accent);
}
}
.config-select {
cursor: pointer;
}
</style>

View File

@@ -321,8 +321,8 @@ onUnmounted(() => {
</div>
<div class="hotkey-preview">
<span class="preview-label">预览</span>
<span class="preview-hotkey">{{ hotkeyPreview || '无' }}</span>
<span class="preview-label">{{ t('settings.hotkeyPreview') }}</span>
<span class="preview-hotkey">{{ hotkeyPreview || t('settings.none') }}</span>
</div>
</div>
</SettingSection>

View File

@@ -3,23 +3,33 @@ import { useI18n } from 'vue-i18n';
import { onMounted, computed } from 'vue';
import SettingSection from '../components/SettingSection.vue';
import { useKeybindingStore } from '@/stores/keybindingStore';
import { useExtensionStore } from '@/stores/extensionStore';
import { useSystemStore } from '@/stores/systemStore';
import { getCommandDescription } from '@/views/editor/extensions/keymap/commandRegistry';
import { getCommandDescription } from '@/views/editor/keymap/commandRegistry';
import {KeyBindingCommand} from "@/../bindings/voidraft/internal/models";
const { t } = useI18n();
const keybindingStore = useKeybindingStore();
const extensionStore = useExtensionStore();
const systemStore = useSystemStore();
// 加载数据
onMounted(async () => {
await keybindingStore.loadKeyBindings();
await extensionStore.loadExtensions();
});
// 从store中获取快捷键数据并转换为显示格式
const keyBindings = computed(() => {
// 只显示启用扩展的快捷键
const enabledExtensionIds = new Set(extensionStore.enabledExtensionIds);
return keybindingStore.keyBindings
.filter(kb => kb.enabled)
.filter(kb => kb.enabled && enabledExtensionIds.has(kb.extension))
.map(kb => ({
id: kb.command,
keys: parseKeyBinding(kb.key, kb.command),
category: kb.category,
category: kb.extension,
description: getCommandDescription(kb.command) || kb.command
}));
});
@@ -186,11 +196,6 @@ const parseKeyBinding = (keyStr: string, command?: string): string[] => {
return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase();
}).filter(part => part.length > 0);
};
// 组件挂载时加载快捷键数据
onMounted(async () => {
await keybindingStore.loadKeyBindings();
});
</script>
<template>

View File

@@ -1,87 +1,141 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import {useI18n} from 'vue-i18n';
import {computed, onMounted, ref} from 'vue';
import {useConfigStore} from '@/stores/configStore';
import {useUpdateStore} from '@/stores/updateStore';
import SettingSection from '../components/SettingSection.vue';
import SettingItem from '../components/SettingItem.vue';
import ToggleSwitch from '../components/ToggleSwitch.vue';
import { Remarkable } from 'remarkable';
const { t } = useI18n();
const {t} = useI18n();
const configStore = useConfigStore();
const updateStore = useUpdateStore();
// 模拟版本数据
const currentVersion = ref('1.0.0');
const isCheckingForUpdates = ref(false);
const updateAvailable = ref(false);
const latestVersion = ref('1.1.0');
const updateNotes = ref([
'优化编辑器性能',
'新增自动保存功能',
'修复多个界面显示问题',
'添加更多编辑器主题'
]);
// 初始化Remarkable实例并配置
const md = new Remarkable({
html: true, // 允许HTML
xhtmlOut: false, // 不使用'/'闭合单标签
breaks: true, // 将'\n'转换为<br>
typographer: true // 启用排版增强
});
// 自动检查更新选项
const autoCheckUpdates = ref(true);
// 计算属性
const autoCheckUpdates = computed({
get: () => configStore.config.updates.autoUpdate,
set: async (value: boolean) => {
await configStore.setAutoUpdate(value);
}
});
// 模拟检查更新
const checkForUpdates = () => {
isCheckingForUpdates.value = true;
// 模拟网络请求延迟
setTimeout(() => {
isCheckingForUpdates.value = false;
updateAvailable.value = true;
}, 1500);
// 使用Remarkable解析Markdown
const parseMarkdown = (markdown: string) => {
if (!markdown) return '';
return md.render(markdown);
};
// 模拟下载更新
const downloadUpdate = () => {
// 在实际应用中这里会调用后端API下载更新
alert('开始下载更新...');
// 处理更新按钮点击
const handleUpdateButtonClick = async () => {
if (updateStore.updateSuccess) {
// 如果更新成功,点击按钮重启应用
await updateStore.restartApplication();
} else if (updateStore.hasUpdate) {
// 如果有更新,点击按钮应用更新
await updateStore.applyUpdate();
} else {
// 否则检查更新
await updateStore.checkForUpdates();
}
};
// 当前版本号
const currentVersion = computed(() => {
return updateStore.updateResult?.currentVersion || configStore.config.updates.version;
});
</script>
<template>
<div class="settings-page">
<SettingSection :title="t('settings.updates')">
<div class="update-info">
<div class="version-info">
<div class="current-version">
<span class="label">当前版本:</span>
<span class="version">{{ currentVersion }}</span>
</div>
<!-- 自动更新设置 -->
<SettingSection :title="t('settings.updateSettings')">
<SettingItem
:title="t('settings.autoCheckUpdates')"
:description="t('settings.autoCheckUpdatesDescription')"
>
<ToggleSwitch v-model="autoCheckUpdates"/>
</SettingItem>
</SettingSection>
<button
<!-- 手动检查更新 -->
<SettingSection :title="t('settings.manualCheck')">
<SettingItem
:title="`${t('settings.currentVersion')}: ${currentVersion}`"
>
<button
class="check-button"
@click="checkForUpdates"
:disabled="isCheckingForUpdates"
>
<span v-if="isCheckingForUpdates" class="loading-spinner"></span>
{{ isCheckingForUpdates ? '检查中...' : '检查更新' }}
</button>
:class="{
'update-available-button': updateStore.hasUpdate && !updateStore.updateSuccess,
'update-success-button': updateStore.updateSuccess
}"
@click="handleUpdateButtonClick"
:disabled="updateStore.isChecking || updateStore.isUpdating"
>
<span v-if="updateStore.isChecking || updateStore.isUpdating" class="loading-spinner"></span>
{{ updateStore.isChecking
? t('settings.checking')
: (updateStore.isUpdating
? t('settings.updating')
: (updateStore.updateSuccess
? t('settings.restartNow')
: (updateStore.hasUpdate ? t('settings.updateNow') : t('settings.checkForUpdates'))))
}}
</button>
</SettingItem>
<!-- 检查结果 -->
<div class="check-results" v-if="updateStore.updateResult || updateStore.errorMessage">
<!-- 错误信息 -->
<div v-if="updateStore.errorMessage" class="result-item error-result">
<div class="result-text">
<span class="result-icon"></span>
<div class="result-message">{{ updateStore.errorMessage }}</div>
</div>
</div>
<div v-if="updateAvailable" class="update-available">
<div class="update-header">
<div class="update-title">发现新版本: {{ latestVersion }}</div>
<button class="download-button" @click="downloadUpdate">
下载更新
</button>
<!-- 更新成功 -->
<div v-else-if="updateStore.updateSuccess" class="result-item update-success">
<div class="result-text">
<span class="result-icon"></span>
<span class="result-message">
{{ t('settings.updateSuccessRestartRequired') }}
</span>
</div>
</div>
<!-- 有新版本 -->
<div v-else-if="updateStore.hasUpdate" class="result-item update-result">
<div class="result-text">
<span class="result-icon">🎉</span>
<span class="result-message">
{{ t('settings.newVersionAvailable') }}: {{ updateStore.updateResult?.latestVersion }}
</span>
</div>
<div class="update-notes">
<div class="notes-title">更新内容:</div>
<ul class="notes-list">
<li v-for="(note, index) in updateNotes" :key="index">
{{ note }}
</li>
</ul>
<div v-if="updateStore.updateResult?.releaseNotes" class="release-notes">
<div class="notes-title">{{ t('settings.releaseNotes') }}:</div>
<div class="markdown-content" v-html="parseMarkdown(updateStore.updateResult.releaseNotes)"></div>
</div>
</div>
<!-- 已是最新版本 -->
<div v-else-if="updateStore.updateResult && !updateStore.hasUpdate && !updateStore.errorMessage"
class="result-item latest-version">
<div class="result-text">
<span class="result-icon"></span>
<span class="result-message">{{ t('settings.upToDate') }}</span>
</div>
</div>
</div>
<SettingItem title="自动检查更新" description="启动应用时自动检查更新">
<ToggleSwitch v-model="autoCheckUpdates" />
</SettingItem>
</SettingSection>
</div>
</template>
@@ -89,135 +143,229 @@ const downloadUpdate = () => {
<style scoped lang="scss">
.settings-page {
max-width: 800px;
width: 100%; // 确保在小屏幕上也能占满可用空间
}
.update-info {
padding: 15px 16px;
margin-bottom: 20px;
.check-button {
padding: 8px 16px;
background-color: var(--settings-input-bg);
border: 1px solid var(--settings-input-border);
border-radius: 4px;
color: var(--settings-text);
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
min-width: 120px;
justify-content: center;
.version-info {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
&:hover:not(:disabled) {
background-color: var(--settings-hover);
border-color: var(--settings-border);
}
.current-version {
font-size: 13px;
&:active:not(:disabled) {
transform: translateY(1px);
}
.label {
color: var(--text-muted);
margin-right: 5px;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.version {
color: var(--settings-text);
font-weight: 500;
}
}
.loading-spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--settings-text);
animation: spin 1s linear infinite;
}
.check-button {
padding: 8px 16px;
background-color: var(--settings-input-bg);
border: 1px solid var(--settings-input-border);
border-radius: 4px;
color: var(--settings-text);
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
&.update-available-button {
background-color: #2196f3;
border-color: #2196f3;
color: white;
&:hover:not(:disabled) {
background-color: var(--settings-hover);
border-color: var(--settings-border);
}
&:active:not(:disabled) {
transform: translateY(1px);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading-spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--settings-text);
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
&:hover {
background-color: #1976d2;
border-color: #1976d2;
}
}
.update-available {
background-color: var(--settings-card-bg);
border: 1px solid var(--settings-border);
border-radius: 6px;
padding: 16px;
&.update-success-button {
background-color: #4caf50;
border-color: #4caf50;
color: white;
.update-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
&:hover {
background-color: #43a047;
border-color: #43a047;
}
}
.update-title {
@keyframes spin {
to {
transform: rotate(360deg);
}
}
}
.check-results {
margin-top: 16px;
width: 100%;
// 为错误消息添加特殊样式
.error-result {
padding: 12px;
background-color: rgba(255, 82, 82, 0.05);
border-radius: 4px;
border-left: 3px solid var(--error-text, #ff5252);
margin-bottom: 8px;
.result-message {
color: var(--error-text, #ff5252);
max-width: 100%;
overflow: visible;
padding-right: 8px; // 添加右侧内边距,防止文本贴近容器边缘
}
}
}
.result-item {
padding: 12px;
border-radius: 4px;
margin-bottom: 8px;
.result-text {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
font-size: 13px;
line-height: 1.5; // 增加行高,提高可读性
}
.result-icon {
font-size: 16px;
flex-shrink: 0;
margin-top: 1px;
}
.result-message {
flex: 1;
word-break: break-word;
white-space: normal;
overflow-wrap: break-word;
}
.release-notes {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--settings-border, rgba(0,0,0,0.1));
.notes-title {
font-size: 12px;
font-weight: 500;
color: var(--settings-text);
margin-bottom: 8px;
}
.markdown-content {
font-size: 12px;
color: var(--settings-text);
line-height: 1.4;
/* Markdown内容样式 */
:deep(p) {
margin: 0 0 6px 0;
}
:deep(ul), :deep(ol) {
margin: 6px 0;
padding-left: 16px;
}
:deep(li) {
margin-bottom: 4px;
}
:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
margin: 10px 0 6px 0;
font-size: 13px;
font-weight: 500;
color: #4a9eff;
}
.download-button {
padding: 8px 16px;
background-color: #2c5a9e;
border: none;
border-radius: 4px;
color: #ffffff;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
&:hover {
background-color: #3867a9;
}
&:active {
transform: translateY(1px);
}
}
}
}
}
.update-notes {
.notes-title {
font-size: 12px;
color: var(--settings-text-secondary);
margin-bottom: 8px;
}
.error-result {
background-color: rgba(244, 67, 54, 0.03);
.notes-list {
margin: 0;
padding-left: 20px;
.result-icon {
color: #f44336;
}
li {
font-size: 12px;
color: var(--settings-text-secondary);
margin-bottom: 6px;
.result-message {
color: var(--error-text, #ff5252);
}
}
&:last-child {
margin-bottom: 0;
}
}
}
.update-result {
background-color: rgba(33, 150, 243, 0.03);
.result-icon {
color: #2196f3;
}
.result-message {
color: #2196f3;
font-weight: 500;
}
}
.update-success {
background-color: rgba(76, 175, 80, 0.03);
.result-icon {
color: #4caf50;
}
.result-message {
color: var(--settings-text);
}
}
.latest-version {
background-color: transparent;
border-left: 3px solid #9e9e9e;
padding-left: 10px;
.result-icon {
color: #9e9e9e;
}
.result-message {
color: var(--settings-text-secondary, #757575);
font-weight: normal;
}
}
// 响应式布局调整
@media (max-width: 600px) {
.result-item {
padding: 10px;
.result-text {
font-size: 12px; // 小屏幕上稍微减小字体
}
}
.check-button {
min-width: 100px;
padding: 6px 12px;
}
}
</style>

42
go.mod
View File

@@ -1,31 +1,38 @@
module voidraft
go 1.23.0
toolchain go1.24.2
go 1.24.4
require (
github.com/Masterminds/semver/v3 v3.3.1
github.com/Masterminds/semver/v3 v3.4.0
github.com/creativeprojects/go-selfupdate v1.5.0
github.com/knadh/koanf/parsers/json v1.0.0
github.com/knadh/koanf/providers/file v1.2.0
github.com/knadh/koanf/providers/structs v1.0.0
github.com/knadh/koanf/v2 v2.2.1
github.com/wailsapp/wails/v3 v3.0.0-alpha.9
github.com/robertkrimen/otto v0.5.1
github.com/wailsapp/wails/v3 v3.0.0-alpha.10
golang.org/x/net v0.41.0
golang.org/x/sys v0.33.0
golang.org/x/text v0.26.0
)
require (
code.gitea.io/sdk/gitea v0.21.0 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/42wim/httpsig v1.2.3 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.16.2 // indirect
@@ -33,7 +40,12 @@ require (
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/go-github/v30 v30.1.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
@@ -41,24 +53,34 @@ require (
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/lmittmann/tint v1.1.2 // indirect
github.com/matryer/is v1.4.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pjbgf/sha1cd v0.4.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.51.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/wailsapp/go-webview2 v1.0.21 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/go-gitlab v0.115.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/time v0.12.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.66.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.38.0 // indirect
)

108
go.sum
View File

@@ -1,7 +1,11 @@
code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
@@ -17,23 +21,33 @@ github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/creativeprojects/go-selfupdate v1.5.0 h1:4zuFafc/qGpymx7umexxth2y2lJXoBR49c3uI0Hr+zU=
github.com/creativeprojects/go-selfupdate v1.5.0/go.mod h1:Pewm8hY7Xe1ne7P8irVBAFnXjTkRuxbbkMlBeTdumNQ=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
@@ -50,10 +64,27 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
@@ -94,19 +125,25 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=
github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
@@ -121,23 +158,42 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA=
github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v3 v3.0.0-alpha.9 h1:b8CfRrhPno8Fra0xFp4Ifyj+ogmXBc35rsQWvcrHtsI=
github.com/wailsapp/wails/v3 v3.0.0-alpha.9/go.mod h1:dSv6s722nSWaUyUiapAM1DHc5HKggNGY1a79shO85/g=
github.com/wailsapp/wails/v3 v3.0.0-alpha.10 h1:SrxwhkBcdtaSxQ/zujJuifJN5q8hxyba5UKv5oaM/X4=
github.com/wailsapp/wails/v3 v3.0.0-alpha.10/go.mod h1:4LCCW7s9e4PuSmu7l9OTvfWIGMO8TaSiftSeR5NpBIc=
github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -152,19 +208,55 @@ golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

View File

@@ -0,0 +1,483 @@
// Package translator 提供文本翻译功能
package translator
import (
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"net/http/cookiejar"
"net/url"
"regexp"
"strings"
"time"
"golang.org/x/text/language"
)
// BingTranslator Bing翻译器结构体
type BingTranslator struct {
httpClient *http.Client // HTTP客户端
Timeout time.Duration // 请求超时时间
session *BingSession // Bing会话
languages map[string]LanguageInfo // 支持的语言列表
}
// BingSession 保持Bing翻译会话状态
type BingSession struct {
Cookie map[string]string // 会话Cookie
Headers map[string]string // 会话请求头
Token string // 翻译Token
Key string // 翻译Key
IG string // IG参数
}
// 常量定义
const (
bingDefaultTimeout = 30 * time.Second
bingTranslatorURL = "https://cn.bing.com/translator"
bingTranslateAPIURL = "https://cn.bing.com/ttranslatev3"
)
// 错误定义
var (
ErrBingNetworkError = errors.New("bing translator network error")
ErrBingParseError = errors.New("bing translator parse error")
ErrBingTokenError = errors.New("failed to get bing translator token")
ErrBingEmptyResponse = errors.New("empty response from bing translator")
ErrBingRateLimit = errors.New("bing translator rate limit reached")
)
// 用户代理列表
var userAgents = []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
}
// NewBingTranslator 创建一个新的Bing翻译器实例
func NewBingTranslator() *BingTranslator {
// 初始化随机数种子
rand.New(rand.NewSource(time.Now().UnixNano()))
// 创建带Cookie存储的HTTP客户端
jar, _ := cookiejar.New(nil)
translator := &BingTranslator{
httpClient: &http.Client{
Timeout: bingDefaultTimeout,
// 启用Cookie存储
Jar: jar,
},
Timeout: bingDefaultTimeout,
session: &BingSession{
Headers: make(map[string]string),
Cookie: make(map[string]string),
},
languages: initBingLanguages(),
}
// 初始化会话
translator.refreshSession()
return translator
}
// initBingLanguages 初始化Bing翻译器支持的语言列表
func initBingLanguages() map[string]LanguageInfo {
// 创建语言映射表
languages := make(map[string]LanguageInfo)
// 添加所有支持的语言
// 基于 Microsoft Translator 支持的语言列表
// 参考: https://learn.microsoft.com/en-us/azure/ai-services/translator/language-support
// 常用语言
languages["en"] = LanguageInfo{Code: "en", Name: "English"}
languages["zh-Hans"] = LanguageInfo{Code: "zh-Hans", Name: "Chinese Simplified"}
languages["zh-Hant"] = LanguageInfo{Code: "zh-Hant", Name: "Chinese Traditional"}
languages["ja"] = LanguageInfo{Code: "ja", Name: "Japanese"}
languages["ko"] = LanguageInfo{Code: "ko", Name: "Korean"}
languages["fr"] = LanguageInfo{Code: "fr", Name: "French"}
languages["fr-ca"] = LanguageInfo{Code: "fr-ca", Name: "French (Canada)"}
languages["de"] = LanguageInfo{Code: "de", Name: "German"}
languages["es"] = LanguageInfo{Code: "es", Name: "Spanish"}
languages["ru"] = LanguageInfo{Code: "ru", Name: "Russian"}
languages["pt"] = LanguageInfo{Code: "pt", Name: "Portuguese (Brazil)"}
languages["pt-br"] = LanguageInfo{Code: "pt-br", Name: "Portuguese (Brazil)"}
languages["pt-pt"] = LanguageInfo{Code: "pt-pt", Name: "Portuguese (Portugal)"}
languages["it"] = LanguageInfo{Code: "it", Name: "Italian"}
languages["ar"] = LanguageInfo{Code: "ar", Name: "Arabic"}
// 特殊语言
languages["yue"] = LanguageInfo{Code: "yue", Name: "Cantonese (Traditional)"}
languages["lzh"] = LanguageInfo{Code: "lzh", Name: "Chinese (Literary)"}
// 其他语言
languages["af"] = LanguageInfo{Code: "af", Name: "Afrikaans"}
languages["am"] = LanguageInfo{Code: "am", Name: "Amharic"}
languages["as"] = LanguageInfo{Code: "as", Name: "Assamese"}
languages["az"] = LanguageInfo{Code: "az", Name: "Azerbaijani (Latin)"}
languages["ba"] = LanguageInfo{Code: "ba", Name: "Bashkir"}
languages["bg"] = LanguageInfo{Code: "bg", Name: "Bulgarian"}
languages["bn"] = LanguageInfo{Code: "bn", Name: "Bangla"}
languages["bo"] = LanguageInfo{Code: "bo", Name: "Tibetan"}
languages["bs"] = LanguageInfo{Code: "bs", Name: "Bosnian (Latin)"}
languages["ca"] = LanguageInfo{Code: "ca", Name: "Catalan"}
languages["cs"] = LanguageInfo{Code: "cs", Name: "Czech"}
languages["cy"] = LanguageInfo{Code: "cy", Name: "Welsh"}
languages["da"] = LanguageInfo{Code: "da", Name: "Danish"}
languages["dv"] = LanguageInfo{Code: "dv", Name: "Divehi"}
languages["el"] = LanguageInfo{Code: "el", Name: "Greek"}
languages["et"] = LanguageInfo{Code: "et", Name: "Estonian"}
languages["eu"] = LanguageInfo{Code: "eu", Name: "Basque"}
languages["fa"] = LanguageInfo{Code: "fa", Name: "Persian"}
languages["fi"] = LanguageInfo{Code: "fi", Name: "Finnish"}
languages["fil"] = LanguageInfo{Code: "fil", Name: "Filipino"}
languages["fj"] = LanguageInfo{Code: "fj", Name: "Fijian"}
languages["fo"] = LanguageInfo{Code: "fo", Name: "Faroese"}
languages["ga"] = LanguageInfo{Code: "ga", Name: "Irish"}
languages["gl"] = LanguageInfo{Code: "gl", Name: "Galician"}
languages["gu"] = LanguageInfo{Code: "gu", Name: "Gujarati"}
languages["ha"] = LanguageInfo{Code: "ha", Name: "Hausa"}
languages["he"] = LanguageInfo{Code: "he", Name: "Hebrew"}
languages["hi"] = LanguageInfo{Code: "hi", Name: "Hindi"}
languages["hr"] = LanguageInfo{Code: "hr", Name: "Croatian"}
languages["ht"] = LanguageInfo{Code: "ht", Name: "Haitian Creole"}
languages["hu"] = LanguageInfo{Code: "hu", Name: "Hungarian"}
languages["hy"] = LanguageInfo{Code: "hy", Name: "Armenian"}
languages["id"] = LanguageInfo{Code: "id", Name: "Indonesian"}
languages["ig"] = LanguageInfo{Code: "ig", Name: "Igbo"}
languages["is"] = LanguageInfo{Code: "is", Name: "Icelandic"}
languages["ka"] = LanguageInfo{Code: "ka", Name: "Georgian"}
languages["kk"] = LanguageInfo{Code: "kk", Name: "Kazakh"}
languages["km"] = LanguageInfo{Code: "km", Name: "Khmer"}
languages["kn"] = LanguageInfo{Code: "kn", Name: "Kannada"}
languages["ku"] = LanguageInfo{Code: "ku", Name: "Kurdish (Arabic) (Central)"}
languages["ky"] = LanguageInfo{Code: "ky", Name: "Kyrgyz (Cyrillic)"}
languages["lo"] = LanguageInfo{Code: "lo", Name: "Lao"}
languages["lt"] = LanguageInfo{Code: "lt", Name: "Lithuanian"}
languages["lv"] = LanguageInfo{Code: "lv", Name: "Latvian"}
languages["mg"] = LanguageInfo{Code: "mg", Name: "Malagasy"}
languages["mi"] = LanguageInfo{Code: "mi", Name: "Maori"}
languages["mk"] = LanguageInfo{Code: "mk", Name: "Macedonian"}
languages["ml"] = LanguageInfo{Code: "ml", Name: "Malayalam"}
languages["mn-Cyrl"] = LanguageInfo{Code: "mn-Cyrl", Name: "Mongolian (Cyrillic)"}
languages["mr"] = LanguageInfo{Code: "mr", Name: "Marathi"}
languages["ms"] = LanguageInfo{Code: "ms", Name: "Malay (Latin)"}
languages["mt"] = LanguageInfo{Code: "mt", Name: "Maltese"}
languages["mww"] = LanguageInfo{Code: "mww", Name: "Hmong Daw (Latin)"}
languages["my"] = LanguageInfo{Code: "my", Name: "Myanmar (Burmese)"}
languages["nb"] = LanguageInfo{Code: "nb", Name: "Norwegian Bokmål"}
languages["ne"] = LanguageInfo{Code: "ne", Name: "Nepali"}
languages["nl"] = LanguageInfo{Code: "nl", Name: "Dutch"}
languages["or"] = LanguageInfo{Code: "or", Name: "Odia"}
languages["otq"] = LanguageInfo{Code: "otq", Name: "Queretaro Otomi"}
languages["pa"] = LanguageInfo{Code: "pa", Name: "Punjabi"}
languages["pl"] = LanguageInfo{Code: "pl", Name: "Polish"}
languages["prs"] = LanguageInfo{Code: "prs", Name: "Dari"}
languages["ps"] = LanguageInfo{Code: "ps", Name: "Pashto"}
languages["ro"] = LanguageInfo{Code: "ro", Name: "Romanian"}
languages["rw"] = LanguageInfo{Code: "rw", Name: "Kinyarwanda"}
languages["sk"] = LanguageInfo{Code: "sk", Name: "Slovak"}
languages["sl"] = LanguageInfo{Code: "sl", Name: "Slovenian"}
languages["sm"] = LanguageInfo{Code: "sm", Name: "Samoan (Latin)"}
languages["sn"] = LanguageInfo{Code: "sn", Name: "chiShona"}
languages["so"] = LanguageInfo{Code: "so", Name: "Somali"}
languages["sq"] = LanguageInfo{Code: "sq", Name: "Albanian"}
languages["sr-Cyrl"] = LanguageInfo{Code: "sr-Cyrl", Name: "Serbian (Cyrillic)"}
languages["sr"] = LanguageInfo{Code: "sr", Name: "Serbian (Latin)"}
languages["sr-latn"] = LanguageInfo{Code: "sr-latn", Name: "Serbian (Latin)"}
languages["sv"] = LanguageInfo{Code: "sv", Name: "Swedish"}
languages["sw"] = LanguageInfo{Code: "sw", Name: "Swahili (Latin)"}
languages["ta"] = LanguageInfo{Code: "ta", Name: "Tamil"}
languages["te"] = LanguageInfo{Code: "te", Name: "Telugu"}
languages["th"] = LanguageInfo{Code: "th", Name: "Thai"}
languages["ti"] = LanguageInfo{Code: "ti", Name: "Tigrinya"}
languages["tk"] = LanguageInfo{Code: "tk", Name: "Turkmen (Latin)"}
languages["tlh-Latn"] = LanguageInfo{Code: "tlh-Latn", Name: "Klingon"}
languages["tlh-Piqd"] = LanguageInfo{Code: "tlh-Piqd", Name: "Klingon (plqaD)"}
languages["to"] = LanguageInfo{Code: "to", Name: "Tongan"}
languages["tr"] = LanguageInfo{Code: "tr", Name: "Turkish"}
languages["tt"] = LanguageInfo{Code: "tt", Name: "Tatar (Latin)"}
languages["ty"] = LanguageInfo{Code: "ty", Name: "Tahitian"}
languages["ug"] = LanguageInfo{Code: "ug", Name: "Uyghur (Arabic)"}
languages["uk"] = LanguageInfo{Code: "uk", Name: "Ukrainian"}
languages["ur"] = LanguageInfo{Code: "ur", Name: "Urdu"}
languages["uz"] = LanguageInfo{Code: "uz", Name: "Uzbek (Latin)"}
languages["vi"] = LanguageInfo{Code: "vi", Name: "Vietnamese"}
languages["yua"] = LanguageInfo{Code: "yua", Name: "Yucatec Maya"}
languages["zu"] = LanguageInfo{Code: "zu", Name: "Zulu"}
// 添加一些特殊情况的映射
languages["zh"] = LanguageInfo{Code: "zh-Hans", Name: "Chinese Simplified"} // 将zh映射到zh-Hans
return languages
}
// SetTimeout 设置请求超时时间
func (t *BingTranslator) SetTimeout(timeout time.Duration) {
t.Timeout = timeout
t.httpClient.Timeout = timeout
}
// getRandomUserAgent 获取随机用户代理
func getRandomUserAgent() string {
return userAgents[rand.Intn(len(userAgents))]
}
// refreshSession 刷新翻译会话
func (t *BingTranslator) refreshSession() error {
// 设置随机用户代理
userAgent := getRandomUserAgent()
t.session.Headers["User-Agent"] = userAgent
t.session.Headers["Referer"] = bingTranslatorURL
t.session.Headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
t.session.Headers["Accept-Language"] = "en-US,en;q=0.5"
t.session.Headers["Connection"] = "keep-alive"
t.session.Headers["Upgrade-Insecure-Requests"] = "1"
t.session.Headers["Cache-Control"] = "max-age=0"
// 创建请求
req, err := http.NewRequest("GET", bingTranslatorURL, nil)
if err != nil {
return fmt.Errorf("the creation request failed: %w", err)
}
// 设置请求头
for k, v := range t.session.Headers {
req.Header.Set(k, v)
}
// 发送请求
resp, err := t.httpClient.Do(req)
if err != nil {
return fmt.Errorf("%w: %v", ErrBingNetworkError, err)
}
defer resp.Body.Close()
// 保存Cookie
for _, cookie := range resp.Cookies() {
t.session.Cookie[cookie.Name] = cookie.Value
}
// 读取响应内容
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response failed: %w", err)
}
content := string(body)
// 提取参数
// 1. 提取key和token
paramsPattern := regexp.MustCompile(`params_AbusePreventionHelper\s*=\s*(\[.*?\]);`)
paramsMatch := paramsPattern.FindStringSubmatch(content)
if paramsMatch == nil || len(paramsMatch) < 2 {
return fmt.Errorf("%w: params_AbusePreventionHelper could not be extracted", ErrBingTokenError)
}
// 解析参数数组
paramsStr := paramsMatch[1]
paramsStr = strings.ReplaceAll(paramsStr, "[", "")
paramsStr = strings.ReplaceAll(paramsStr, "]", "")
paramsParts := strings.Split(paramsStr, ",")
if len(paramsParts) < 2 {
return fmt.Errorf("%w: params_AbusePreventionHelper format is incorrect", ErrBingTokenError)
}
// 提取key和token
t.session.Key = strings.Trim(paramsParts[0], `"' `)
t.session.Token = strings.Trim(paramsParts[1], `"' `)
// 2. 提取IG值
igPattern := regexp.MustCompile(`IG:"(\w+)"`)
igMatch := igPattern.FindStringSubmatch(content)
if igMatch == nil || len(igMatch) < 2 {
return fmt.Errorf("%w: Unable to extract IG values", ErrBingTokenError)
}
t.session.IG = igMatch[1]
// 更新会话头部
t.session.Headers["IG"] = t.session.IG
t.session.Headers["key"] = t.session.Key
t.session.Headers["token"] = t.session.Token
return nil
}
// Translate 使用标准语言标签进行文本翻译
func (t *BingTranslator) Translate(text string, from language.Tag, to language.Tag) (string, error) {
return t.translate(text, from.String(), to.String())
}
// TranslateWithParams 使用简单字符串参数进行文本翻译
func (t *BingTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) {
// 设置超时时间(如果有指定)
if params.Timeout > 0 {
t.SetTimeout(params.Timeout)
}
return t.translate(text, params.From, params.To)
}
// translate 执行实际翻译操作
func (t *BingTranslator) translate(text, from, to string) (string, error) {
// 如果没有会话或关键参数缺失,刷新会话
if t.session == nil || t.session.Token == "" || t.session.Key == "" || t.session.IG == "" {
if err := t.refreshSession(); err != nil {
return "", fmt.Errorf("the refresh session failed: %w", err)
}
}
// 生成随机IID
randNum := rand.Intn(10) // 0-9的随机数
iid := fmt.Sprintf("translator.5019.%d", 1+randNum%3) // 生成随机IID
// 构建URL - 确保使用双&符号
reqURL := fmt.Sprintf("%s?isVertical=1&&IG=%s&IID=%s",
bingTranslateAPIURL, t.session.IG, iid)
// 标准化语言代码
fromLang := t.GetStandardLanguageCode(from)
toLang := t.GetStandardLanguageCode(to)
// 构建表单数据
formData := url.Values{}
formData.Set("fromLang", fromLang)
formData.Set("text", text)
formData.Set("to", toLang)
formData.Set("token", t.session.Token)
formData.Set("key", t.session.Key)
formDataStr := formData.Encode()
// 创建请求
req, err := http.NewRequest("POST", reqURL, strings.NewReader(formDataStr))
if err != nil {
return "", fmt.Errorf("The creation request failed: %w", err)
}
// 设置请求头
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", t.session.Headers["User-Agent"])
req.Header.Set("Referer", bingTranslatorURL)
req.Header.Set("Accept", "*/*")
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
req.Header.Set("Origin", "https://cn.bing.com")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("X-Requested-With", "XMLHttpRequest")
// 添加Cookie
for name, value := range t.session.Cookie {
req.AddCookie(&http.Cookie{
Name: name,
Value: value,
})
}
// 发送请求
resp, err := t.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrBingNetworkError, err)
}
// 读取响应
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return "", fmt.Errorf("read response failed: %w", err)
}
if len(body) == 0 {
return "", ErrBingEmptyResponse
}
// 尝试解析响应
var result interface{}
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("%w: %v", ErrBingParseError, err)
}
// 检查是否是字典类型
if resultDict, ok := result.(map[string]interface{}); ok {
// 检查是否需要验证码
if _, hasCaptcha := resultDict["ShowCaptcha"]; hasCaptcha {
return "", ErrBingRateLimit
}
// 检查状态码
if statusCode, hasStatus := resultDict["statusCode"]; hasStatus {
if statusCode.(float64) == 400 {
// 检查是否有错误消息
if errorMsg, hasError := resultDict["errorMessage"]; hasError && errorMsg.(string) != "" {
return "", fmt.Errorf("translation failed: %s", errorMsg)
}
// 如果没有明确的错误消息可能是API变更或其他问题
return "", fmt.Errorf("translation request failed (status code: 400)")
} else if statusCode.(float64) == 429 {
return "", ErrBingRateLimit
}
}
// 尝试从错误响应中提取详细信息
if message, hasMessage := resultDict["message"]; hasMessage {
return "", fmt.Errorf("translation failed: %v", message)
}
// 尝试从响应中获取翻译结果
if translations, hasTranslations := resultDict["translations"]; hasTranslations {
if translationsArray, ok := translations.([]interface{}); ok && len(translationsArray) > 0 {
if translation, ok := translationsArray[0].(map[string]interface{}); ok {
if text, ok := translation["text"].(string); ok {
return text, nil
}
}
}
}
// 其他错误
return "", fmt.Errorf("translation failed: %v", resultDict)
}
// 应该是数组类型
if resultArray, ok := result.([]interface{}); ok && len(resultArray) > 0 {
firstItem := resultArray[0]
if itemDict, ok := firstItem.(map[string]interface{}); ok {
if translations, ok := itemDict["translations"].([]interface{}); ok && len(translations) > 0 {
if translation, ok := translations[0].(map[string]interface{}); ok {
if text, ok := translation["text"].(string); ok {
return text, nil
}
}
}
}
}
return "", fmt.Errorf("%w: The response format is not as expected", ErrBingParseError)
}
// GetSupportedLanguages 获取翻译器支持的语言列表
func (t *BingTranslator) GetSupportedLanguages() map[string]LanguageInfo {
return t.languages
}
// IsLanguageSupported 检查指定的语言代码是否受支持
func (t *BingTranslator) IsLanguageSupported(languageCode string) bool {
_, exists := t.languages[languageCode]
return exists
}
// GetStandardLanguageCode 获取标准化的语言代码
func (t *BingTranslator) GetStandardLanguageCode(languageCode string) string {
if info, exists := t.languages[languageCode]; exists {
return info.Code
}
return languageCode // 如果没有找到映射,返回原始代码
}

View File

@@ -0,0 +1,327 @@
// Package translator 提供文本翻译功能
package translator
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"strings"
"time"
"golang.org/x/text/language"
)
// DeeplTranslator DeepL翻译器结构体
type DeeplTranslator struct {
DeeplHost string // DeepL服务主机
httpClient *http.Client // HTTP客户端
Timeout time.Duration // 请求超时时间
languages map[string]LanguageInfo // 支持的语言列表
}
// 常量定义
const (
deeplDefaultTimeout = 30 * time.Second
defaultDeeplHost = "www2.deepl.com" // 默认DeepL API主机
deeplJsonRpcUrl = "https://www2.deepl.com/jsonrpc" // DeepL JSON-RPC API
)
// 错误定义
var (
ErrDeeplNetworkError = errors.New("deepl translator network error")
ErrDeeplUnsupportedLang = errors.New("deepl translator unsupported language")
ErrDeeplResponseError = errors.New("deepl translator response error")
)
// DeeplRequest DeepL请求结构体
type DeeplRequest struct {
Jsonrpc string `json:"jsonrpc"`
Method string `json:"method"`
ID int64 `json:"id"`
Params DeeplReqParams `json:"params"`
}
// DeeplReqParams DeepL请求参数结构体
type DeeplReqParams struct {
Texts []DeeplText `json:"texts"`
Splitting string `json:"splitting"`
Lang DeeplLang `json:"lang"`
Timestamp int64 `json:"timestamp"`
}
// DeeplText DeepL文本结构体
type DeeplText struct {
Text string `json:"text"`
RequestAlternatives int `json:"requestAlternatives"`
}
// DeeplLang DeepL语言结构体
type DeeplLang struct {
SourceLangUserSelected string `json:"source_lang_user_selected"`
TargetLang string `json:"target_lang"`
}
// DeeplResponse DeepL响应结构体
type DeeplResponse struct {
Jsonrpc string `json:"jsonrpc"`
ID int64 `json:"id"`
Result DeeplResult `json:"result"`
}
// DeeplResult DeepL结果结构体
type DeeplResult struct {
Texts []DeeplResultText `json:"texts"`
Lang string `json:"lang"`
LangIsConfident bool `json:"lang_is_confident"`
DetectedLanguages map[string]float64 `json:"detectedLanguages"`
}
// DeeplResultText DeepL结果文本结构体
type DeeplResultText struct {
Text string `json:"text"`
Alternatives []DeeplAlternative `json:"alternatives,omitempty"`
}
// DeeplAlternative DeepL替代翻译结构体
type DeeplAlternative struct {
Text string `json:"text"`
}
// NewDeeplTranslator 创建一个新的DeepL翻译器实例
func NewDeeplTranslator() *DeeplTranslator {
translator := &DeeplTranslator{
DeeplHost: defaultDeeplHost,
Timeout: deeplDefaultTimeout,
httpClient: &http.Client{Timeout: deeplDefaultTimeout},
languages: initDeeplLanguages(),
}
return translator
}
// initDeeplLanguages 初始化DeepL翻译器支持的语言列表
func initDeeplLanguages() map[string]LanguageInfo {
// 创建语言映射表
languages := make(map[string]LanguageInfo)
// 添加所有支持的语言
// 基于 DeepL API 支持的语言列表
// 参考: https://developers.deepl.com/docs/resources/supported-languages
// 源语言和目标语言
languages["ar"] = LanguageInfo{Code: "AR", Name: "Arabic"}
languages["bg"] = LanguageInfo{Code: "BG", Name: "Bulgarian"}
languages["cs"] = LanguageInfo{Code: "CS", Name: "Czech"}
languages["da"] = LanguageInfo{Code: "DA", Name: "Danish"}
languages["de"] = LanguageInfo{Code: "DE", Name: "German"}
languages["el"] = LanguageInfo{Code: "EL", Name: "Greek"}
languages["en"] = LanguageInfo{Code: "EN", Name: "English"}
languages["en-gb"] = LanguageInfo{Code: "EN-GB", Name: "English (British)"}
languages["en-us"] = LanguageInfo{Code: "EN-US", Name: "English (American)"}
languages["es"] = LanguageInfo{Code: "ES", Name: "Spanish"}
languages["et"] = LanguageInfo{Code: "ET", Name: "Estonian"}
languages["fi"] = LanguageInfo{Code: "FI", Name: "Finnish"}
languages["fr"] = LanguageInfo{Code: "FR", Name: "French"}
languages["hu"] = LanguageInfo{Code: "HU", Name: "Hungarian"}
languages["id"] = LanguageInfo{Code: "ID", Name: "Indonesian"}
languages["it"] = LanguageInfo{Code: "IT", Name: "Italian"}
languages["ja"] = LanguageInfo{Code: "JA", Name: "Japanese"}
languages["ko"] = LanguageInfo{Code: "KO", Name: "Korean"}
languages["lt"] = LanguageInfo{Code: "LT", Name: "Lithuanian"}
languages["lv"] = LanguageInfo{Code: "LV", Name: "Latvian"}
languages["nb"] = LanguageInfo{Code: "NB", Name: "Norwegian Bokmål"}
languages["nl"] = LanguageInfo{Code: "NL", Name: "Dutch"}
languages["pl"] = LanguageInfo{Code: "PL", Name: "Polish"}
languages["pt"] = LanguageInfo{Code: "PT", Name: "Portuguese"}
languages["pt-br"] = LanguageInfo{Code: "PT-BR", Name: "Portuguese (Brazilian)"}
languages["pt-pt"] = LanguageInfo{Code: "PT-PT", Name: "Portuguese (Portugal)"}
languages["ro"] = LanguageInfo{Code: "RO", Name: "Romanian"}
languages["ru"] = LanguageInfo{Code: "RU", Name: "Russian"}
languages["sk"] = LanguageInfo{Code: "SK", Name: "Slovak"}
languages["sl"] = LanguageInfo{Code: "SL", Name: "Slovenian"}
languages["sv"] = LanguageInfo{Code: "SV", Name: "Swedish"}
languages["tr"] = LanguageInfo{Code: "TR", Name: "Turkish"}
languages["uk"] = LanguageInfo{Code: "UK", Name: "Ukrainian"}
languages["zh"] = LanguageInfo{Code: "ZH", Name: "Chinese"}
return languages
}
// SetTimeout 设置请求超时时间
func (t *DeeplTranslator) SetTimeout(timeout time.Duration) {
t.Timeout = timeout
t.httpClient.Timeout = timeout
}
// SetDeeplHost 设置DeepL主机
func (t *DeeplTranslator) SetDeeplHost(host string) {
t.DeeplHost = host
}
// Translate 使用标准语言标签进行文本翻译
func (t *DeeplTranslator) Translate(text string, from language.Tag, to language.Tag) (string, error) {
return t.translate(text, from.String(), to.String())
}
// TranslateWithParams 使用简单字符串参数进行文本翻译
func (t *DeeplTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) {
// 设置超时时间(如果有指定)
if params.Timeout > 0 {
t.SetTimeout(params.Timeout)
}
// 直接执行一次翻译
return t.translate(text, params.From, params.To)
}
// translate 执行实际翻译操作
func (t *DeeplTranslator) translate(text, from, to string) (string, error) {
// 转换语言代码为DeepL格式
fromLower := strings.ToLower(from)
toLower := strings.ToLower(to)
var sourceLang string
if fromLower == "auto" {
sourceLang = "auto"
} else if fromLangInfo, ok := t.languages[fromLower]; ok {
sourceLang = fromLangInfo.Code
} else {
sourceLang = "auto"
}
var targetLang string
if toLangInfo, ok := t.languages[toLower]; ok {
targetLang = toLangInfo.Code
} else {
return "", fmt.Errorf("%w: language '%s' not supported by DeepL", ErrDeeplUnsupportedLang, to)
}
// 准备请求数据
id := getRandomNumber()
iCount := getICount(text)
timestamp := getTimeStamp(iCount)
// 构建请求体
reqParams := DeeplReqParams{
Texts: []DeeplText{
{
Text: text,
RequestAlternatives: 3,
},
},
Splitting: "newlines",
Lang: DeeplLang{
SourceLangUserSelected: sourceLang,
TargetLang: targetLang,
},
Timestamp: timestamp,
}
request := DeeplRequest{
Jsonrpc: "2.0",
Method: "LMT_handle_texts",
ID: id,
Params: reqParams,
}
// 序列化请求
jsonData, err := json.Marshal(request)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %w", err)
}
// 特殊处理method字段格式
postStr := string(jsonData)
if (id+5)%29 == 0 || (id+3)%13 == 0 {
postStr = strings.Replace(postStr, `"method":"`, `"method" : "`, 1)
} else {
postStr = strings.Replace(postStr, `"method":"`, `"method": "`, 1)
}
// 发送请求
req, err := http.NewRequest("POST", deeplJsonRpcUrl, bytes.NewBuffer([]byte(postStr)))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "*/*")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Origin", "https://www.deepl.com")
req.Header.Set("Referer", "https://www.deepl.com/translator")
// 执行请求
resp, err := t.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrDeeplNetworkError, err)
}
defer resp.Body.Close()
// 检查响应状态
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("API error: status code %d", resp.StatusCode)
}
// 读取响应内容
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
// 解析响应
var response DeeplResponse
if err := json.Unmarshal(body, &response); err != nil {
return "", fmt.Errorf("%w: %v", ErrDeeplResponseError, err)
}
// 检查结果
if len(response.Result.Texts) == 0 {
return "", fmt.Errorf("%w: empty translation result", ErrDeeplResponseError)
}
// 返回翻译结果
return response.Result.Texts[0].Text, nil
}
// getICount 获取文本中'i'字符的数量
func getICount(text string) int {
return strings.Count(text, "i")
}
// getRandomNumber 生成随机数
func getRandomNumber() int64 {
return int64(rand.Intn(99999)+100000) * 1000
}
// getTimeStamp 获取时间戳
func getTimeStamp(iCount int) int64 {
ts := time.Now().UnixMilli()
if iCount != 0 {
iCount++
return ts - (ts % int64(iCount)) + int64(iCount)
}
return ts
}
// GetSupportedLanguages 获取翻译器支持的语言列表
func (t *DeeplTranslator) GetSupportedLanguages() map[string]LanguageInfo {
return t.languages
}
// IsLanguageSupported 检查指定的语言代码是否受支持
func (t *DeeplTranslator) IsLanguageSupported(languageCode string) bool {
_, ok := t.languages[strings.ToLower(languageCode)]
return ok
}
// GetStandardLanguageCode 获取标准化的语言代码
func (t *DeeplTranslator) GetStandardLanguageCode(languageCode string) string {
// 简单返回小写版本作为标准代码
return strings.ToLower(languageCode)
}

View File

@@ -0,0 +1,443 @@
// Package translator 提供文本翻译功能
package translator
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"math"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"github.com/robertkrimen/otto"
"golang.org/x/text/language"
)
// 错误定义
var (
ErrBadNetwork = errors.New("bad network, please check your internet connection")
)
// GoogleTranslator Google翻译器结构体统一管理翻译功能
type GoogleTranslator struct {
GoogleHost string // Google服务主机
vm *otto.Otto // JavaScript虚拟机
ttk otto.Value // 翻译token缓存
httpClient *http.Client // HTTP客户端
Timeout time.Duration // 请求超时时间
languages map[string]LanguageInfo // 支持的语言列表
}
// NewGoogleTranslator 创建一个新的Google翻译器实例
func NewGoogleTranslator() *GoogleTranslator {
translator := &GoogleTranslator{
GoogleHost: "google.com",
vm: otto.New(),
Timeout: defaultTimeout,
httpClient: &http.Client{Timeout: defaultTimeout},
languages: initGoogleLanguages(),
}
// 初始化ttk
translator.ttk, _ = otto.ToValue("0")
return translator
}
// initGoogleLanguages 初始化Google翻译器支持的语言列表
func initGoogleLanguages() map[string]LanguageInfo {
// 创建语言映射表
languages := make(map[string]LanguageInfo)
// 添加所有支持的语言
// 参考: https://cloud.google.com/translate/docs/languages
// 添加自动检测
languages["auto"] = LanguageInfo{Code: "auto", Name: "Auto Detect"}
// 主要语言
languages["en"] = LanguageInfo{Code: "en", Name: "English"}
languages["zh-cn"] = LanguageInfo{Code: "zh-CN", Name: "Chinese (Simplified)"}
languages["zh-tw"] = LanguageInfo{Code: "zh-TW", Name: "Chinese (Traditional)"}
languages["ja"] = LanguageInfo{Code: "ja", Name: "Japanese"}
languages["ko"] = LanguageInfo{Code: "ko", Name: "Korean"}
languages["fr"] = LanguageInfo{Code: "fr", Name: "French"}
languages["de"] = LanguageInfo{Code: "de", Name: "German"}
languages["es"] = LanguageInfo{Code: "es", Name: "Spanish"}
languages["ru"] = LanguageInfo{Code: "ru", Name: "Russian"}
languages["it"] = LanguageInfo{Code: "it", Name: "Italian"}
languages["pt"] = LanguageInfo{Code: "pt", Name: "Portuguese"}
// 其他语言
languages["af"] = LanguageInfo{Code: "af", Name: "Afrikaans"}
languages["sq"] = LanguageInfo{Code: "sq", Name: "Albanian"}
languages["am"] = LanguageInfo{Code: "am", Name: "Amharic"}
languages["ar"] = LanguageInfo{Code: "ar", Name: "Arabic"}
languages["hy"] = LanguageInfo{Code: "hy", Name: "Armenian"}
languages["az"] = LanguageInfo{Code: "az", Name: "Azerbaijani"}
languages["eu"] = LanguageInfo{Code: "eu", Name: "Basque"}
languages["be"] = LanguageInfo{Code: "be", Name: "Belarusian"}
languages["bn"] = LanguageInfo{Code: "bn", Name: "Bengali"}
languages["bs"] = LanguageInfo{Code: "bs", Name: "Bosnian"}
languages["bg"] = LanguageInfo{Code: "bg", Name: "Bulgarian"}
languages["ca"] = LanguageInfo{Code: "ca", Name: "Catalan"}
languages["ceb"] = LanguageInfo{Code: "ceb", Name: "Cebuano"}
languages["zh"] = LanguageInfo{Code: "zh", Name: "Chinese"}
languages["co"] = LanguageInfo{Code: "co", Name: "Corsican"}
languages["hr"] = LanguageInfo{Code: "hr", Name: "Croatian"}
languages["cs"] = LanguageInfo{Code: "cs", Name: "Czech"}
languages["da"] = LanguageInfo{Code: "da", Name: "Danish"}
languages["nl"] = LanguageInfo{Code: "nl", Name: "Dutch"}
languages["eo"] = LanguageInfo{Code: "eo", Name: "Esperanto"}
languages["et"] = LanguageInfo{Code: "et", Name: "Estonian"}
languages["fi"] = LanguageInfo{Code: "fi", Name: "Finnish"}
languages["fy"] = LanguageInfo{Code: "fy", Name: "Frisian"}
languages["gl"] = LanguageInfo{Code: "gl", Name: "Galician"}
languages["ka"] = LanguageInfo{Code: "ka", Name: "Georgian"}
languages["el"] = LanguageInfo{Code: "el", Name: "Greek"}
languages["gu"] = LanguageInfo{Code: "gu", Name: "Gujarati"}
languages["ht"] = LanguageInfo{Code: "ht", Name: "Haitian Creole"}
languages["ha"] = LanguageInfo{Code: "ha", Name: "Hausa"}
languages["haw"] = LanguageInfo{Code: "haw", Name: "Hawaiian"}
languages["he"] = LanguageInfo{Code: "he", Name: "Hebrew"}
languages["hi"] = LanguageInfo{Code: "hi", Name: "Hindi"}
languages["hmn"] = LanguageInfo{Code: "hmn", Name: "Hmong"}
languages["hu"] = LanguageInfo{Code: "hu", Name: "Hungarian"}
languages["is"] = LanguageInfo{Code: "is", Name: "Icelandic"}
languages["ig"] = LanguageInfo{Code: "ig", Name: "Igbo"}
languages["id"] = LanguageInfo{Code: "id", Name: "Indonesian"}
languages["ga"] = LanguageInfo{Code: "ga", Name: "Irish"}
languages["jw"] = LanguageInfo{Code: "jw", Name: "Javanese"}
languages["kn"] = LanguageInfo{Code: "kn", Name: "Kannada"}
languages["kk"] = LanguageInfo{Code: "kk", Name: "Kazakh"}
languages["km"] = LanguageInfo{Code: "km", Name: "Khmer"}
languages["ku"] = LanguageInfo{Code: "ku", Name: "Kurdish"}
languages["ky"] = LanguageInfo{Code: "ky", Name: "Kyrgyz"}
languages["lo"] = LanguageInfo{Code: "lo", Name: "Lao"}
languages["la"] = LanguageInfo{Code: "la", Name: "Latin"}
languages["lv"] = LanguageInfo{Code: "lv", Name: "Latvian"}
languages["lt"] = LanguageInfo{Code: "lt", Name: "Lithuanian"}
languages["lb"] = LanguageInfo{Code: "lb", Name: "Luxembourgish"}
languages["mk"] = LanguageInfo{Code: "mk", Name: "Macedonian"}
languages["mg"] = LanguageInfo{Code: "mg", Name: "Malagasy"}
languages["ms"] = LanguageInfo{Code: "ms", Name: "Malay"}
languages["ml"] = LanguageInfo{Code: "ml", Name: "Malayalam"}
languages["mt"] = LanguageInfo{Code: "mt", Name: "Maltese"}
languages["mi"] = LanguageInfo{Code: "mi", Name: "Maori"}
languages["mr"] = LanguageInfo{Code: "mr", Name: "Marathi"}
languages["mn"] = LanguageInfo{Code: "mn", Name: "Mongolian"}
languages["my"] = LanguageInfo{Code: "my", Name: "Myanmar (Burmese)"}
languages["ne"] = LanguageInfo{Code: "ne", Name: "Nepali"}
languages["no"] = LanguageInfo{Code: "no", Name: "Norwegian"}
languages["ny"] = LanguageInfo{Code: "ny", Name: "Nyanja (Chichewa)"}
languages["ps"] = LanguageInfo{Code: "ps", Name: "Pashto"}
languages["fa"] = LanguageInfo{Code: "fa", Name: "Persian"}
languages["pl"] = LanguageInfo{Code: "pl", Name: "Polish"}
languages["pt-br"] = LanguageInfo{Code: "pt-BR", Name: "Portuguese (Brazil)"}
languages["pt-pt"] = LanguageInfo{Code: "pt-PT", Name: "Portuguese (Portugal)"}
languages["pa"] = LanguageInfo{Code: "pa", Name: "Punjabi"}
languages["ro"] = LanguageInfo{Code: "ro", Name: "Romanian"}
languages["sm"] = LanguageInfo{Code: "sm", Name: "Samoan"}
languages["gd"] = LanguageInfo{Code: "gd", Name: "Scots Gaelic"}
languages["sr"] = LanguageInfo{Code: "sr", Name: "Serbian"}
languages["st"] = LanguageInfo{Code: "st", Name: "Sesotho"}
languages["sn"] = LanguageInfo{Code: "sn", Name: "Shona"}
languages["sd"] = LanguageInfo{Code: "sd", Name: "Sindhi"}
languages["si"] = LanguageInfo{Code: "si", Name: "Sinhala (Sinhalese)"}
languages["sk"] = LanguageInfo{Code: "sk", Name: "Slovak"}
languages["sl"] = LanguageInfo{Code: "sl", Name: "Slovenian"}
languages["so"] = LanguageInfo{Code: "so", Name: "Somali"}
languages["su"] = LanguageInfo{Code: "su", Name: "Sundanese"}
languages["sw"] = LanguageInfo{Code: "sw", Name: "Swahili"}
languages["sv"] = LanguageInfo{Code: "sv", Name: "Swedish"}
languages["tl"] = LanguageInfo{Code: "tl", Name: "Tagalog (Filipino)"}
languages["tg"] = LanguageInfo{Code: "tg", Name: "Tajik"}
languages["ta"] = LanguageInfo{Code: "ta", Name: "Tamil"}
languages["te"] = LanguageInfo{Code: "te", Name: "Telugu"}
languages["th"] = LanguageInfo{Code: "th", Name: "Thai"}
languages["tr"] = LanguageInfo{Code: "tr", Name: "Turkish"}
languages["uk"] = LanguageInfo{Code: "uk", Name: "Ukrainian"}
languages["ur"] = LanguageInfo{Code: "ur", Name: "Urdu"}
languages["uz"] = LanguageInfo{Code: "uz", Name: "Uzbek"}
languages["vi"] = LanguageInfo{Code: "vi", Name: "Vietnamese"}
languages["cy"] = LanguageInfo{Code: "cy", Name: "Welsh"}
languages["xh"] = LanguageInfo{Code: "xh", Name: "Xhosa"}
languages["yi"] = LanguageInfo{Code: "yi", Name: "Yiddish"}
languages["yo"] = LanguageInfo{Code: "yo", Name: "Yoruba"}
languages["zu"] = LanguageInfo{Code: "zu", Name: "Zulu"}
return languages
}
// SetTimeout 设置请求超时时间
func (t *GoogleTranslator) SetTimeout(timeout time.Duration) {
t.Timeout = timeout
t.httpClient.Timeout = timeout
}
// SetGoogleHost 设置Google主机
func (t *GoogleTranslator) SetGoogleHost(host string) {
t.GoogleHost = host
}
// Translate 使用Go语言提供的标准语言标签进行文本翻译
func (t *GoogleTranslator) Translate(text string, from language.Tag, to language.Tag) (string, error) {
return t.translate(text, from.String(), to.String(), false)
}
// TranslateWithParams 使用简单字符串参数进行文本翻译
func (t *GoogleTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) {
// 设置超时时间(如果有指定)
if params.Timeout > 0 {
t.SetTimeout(params.Timeout)
}
return t.translate(text, params.From, params.To, true)
}
// translate 执行实际翻译操作
func (t *GoogleTranslator) translate(text, from, to string, withVerification bool) (string, error) {
if withVerification {
if _, err := language.Parse(from); err != nil && from != "auto" {
log.Println("[WARNING], '" + from + "' is a invalid language, switching to 'auto'")
from = "auto"
}
if _, err := language.Parse(to); err != nil {
log.Println("[WARNING], '" + to + "' is a invalid language, switching to 'en'")
to = "en"
}
}
textValue, _ := otto.ToValue(text)
urlStr := fmt.Sprintf("https://translate.%s/translate_a/single", t.GoogleHost)
token := t.getToken(textValue)
data := map[string]string{
"client": "gtx",
"sl": from,
"tl": to,
"hl": to,
"ie": "UTF-8",
"oe": "UTF-8",
"otf": "1",
"ssel": "0",
"tsel": "0",
"kc": "7",
"q": text,
}
u, err := url.Parse(urlStr)
if err != nil {
return "", err
}
parameters := url.Values{}
for k, v := range data {
parameters.Add(k, v)
}
for _, v := range []string{"at", "bd", "ex", "ld", "md", "qca", "rw", "rm", "ss", "t"} {
parameters.Add("dt", v)
}
parameters.Add("tk", token)
u.RawQuery = parameters.Encode()
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return "", err
}
r, err := t.httpClient.Do(req)
if err != nil {
if errors.Is(err, http.ErrHandlerTimeout) {
return "", ErrBadNetwork
}
return "", err
}
if r.StatusCode != http.StatusOK {
return "", fmt.Errorf("API error: status code %d", r.StatusCode)
}
raw, err := io.ReadAll(r.Body)
if err != nil {
return "", err
}
defer r.Body.Close()
var resp []interface{}
err = json.Unmarshal(raw, &resp)
if err != nil {
return "", err
}
responseText := ""
for _, obj := range resp[0].([]interface{}) {
if len(obj.([]interface{})) == 0 {
break
}
t, ok := obj.([]interface{})[0].(string)
if ok {
responseText += t
}
}
return responseText, nil
}
// getToken 获取翻译API所需的token
func (t *GoogleTranslator) getToken(text otto.Value) string {
ttk, err := t.updateTTK()
if err != nil {
return ""
}
tk, err := t.generateToken(text, ttk)
if err != nil {
return ""
}
return strings.Replace(tk.String(), "&tk=", "", -1)
}
// updateTTK 更新TTK值
func (t *GoogleTranslator) updateTTK() (otto.Value, error) {
timestamp := time.Now().UnixNano() / 3600000
now := math.Floor(float64(timestamp))
ttk, err := strconv.ParseFloat(t.ttk.String(), 64)
if err != nil {
return otto.UndefinedValue(), err
}
if ttk == now {
return t.ttk, nil
}
req, err := http.NewRequest("GET", fmt.Sprintf("https://translate.%s", t.GoogleHost), nil)
if err != nil {
return otto.UndefinedValue(), err
}
resp, err := t.httpClient.Do(req)
if err != nil {
return otto.UndefinedValue(), err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return otto.UndefinedValue(), err
}
matches := regexp.MustCompile(`tkk:\s?'(.+?)'`).FindStringSubmatch(string(body))
if len(matches) > 0 {
v, err := otto.ToValue(matches[0])
if err != nil {
return otto.UndefinedValue(), err
}
t.ttk = v
return v, nil
}
return t.ttk, nil
}
// generateToken 生成翻译API所需的token
func (t *GoogleTranslator) generateToken(a otto.Value, TTK otto.Value) (otto.Value, error) {
err := t.vm.Set("x", a)
if err != nil {
return otto.UndefinedValue(), err
}
_ = t.vm.Set("internalTTK", TTK)
result, err := t.vm.Run(`
function sM(a) {
var b;
if (null !== yr)
b = yr;
else {
b = wr(String.fromCharCode(84));
var c = wr(String.fromCharCode(75));
b = [b(), b()];
b[1] = c();
b = (yr = window[b.join(c())] || "") || ""
}
var d = wr(String.fromCharCode(116))
, c = wr(String.fromCharCode(107))
, d = [d(), d()];
d[1] = c();
c = "&" + d.join("") + "=";
d = b.split(".");
b = Number(d[0]) || 0;
for (var e = [], f = 0, g = 0; g < a.length; g++) {
var l = a.charCodeAt(g);
128 > l ? e[f++] = l : (2048 > l ? e[f++] = l >> 6 | 192 : (55296 == (l & 64512) && g + 1 < a.length && 56320 == (a.charCodeAt(g + 1) & 64512) ? (l = 65536 + ((l & 1023) << 10) + (a.charCodeAt(++g) & 1023),
e[f++] = l >> 18 | 240,
e[f++] = l >> 12 & 63 | 128) : e[f++] = l >> 12 | 224,
e[f++] = l >> 6 & 63 | 128),
e[f++] = l & 63 | 128)
}
a = b;
for (f = 0; f < e.length; f++)
a += e[f],
a = xr(a, "+-a^+6");
a = xr(a, "+-3^+b+-f");
a ^= Number(d[1]) || 0;
0 > a && (a = (a & 2147483647) + 2147483648);
a %= 1E6;
return c + (a.toString() + "." + (a ^ b))
}
var yr = null;
var wr = function(a) {
return function() {
return a
}
}
, xr = function(a, b) {
for (var c = 0; c < b.length - 2; c += 3) {
var d = b.charAt(c + 2)
, d = "a" <= d ? d.charCodeAt(0) - 87 : Number(d)
, d = "+" == b.charAt(c + 1) ? a >>> d : a << d;
a = "+" == b.charAt(c) ? a + d & 4294967295 : a ^ d
}
return a
};
var window = {
TKK: internalTTK
};
sM(x)
`)
if err != nil {
return otto.UndefinedValue(), err
}
return result, nil
}
// GetSupportedLanguages 获取翻译器支持的语言列表
func (t *GoogleTranslator) GetSupportedLanguages() map[string]LanguageInfo {
return t.languages
}
// IsLanguageSupported 检查指定的语言代码是否受支持
func (t *GoogleTranslator) IsLanguageSupported(languageCode string) bool {
_, ok := t.languages[strings.ToLower(languageCode)]
return ok
}
// GetStandardLanguageCode 获取标准化的语言代码
func (t *GoogleTranslator) GetStandardLanguageCode(languageCode string) string {
// 简单返回小写版本作为标准代码
return strings.ToLower(languageCode)
}

View File

@@ -0,0 +1,86 @@
// Package translator 提供文本翻译功能
package translator
import (
"fmt"
"time"
"golang.org/x/text/language"
)
// TranslationParams 用于指定翻译参数
type TranslationParams struct {
From string // 源语言
To string // 目标语言
Timeout time.Duration // 超时时间
}
// 常量定义
const (
defaultTimeout = 30 * time.Second
)
// TranslatorType 翻译器类型
type TranslatorType string
const (
// GoogleTranslatorType 谷歌翻译器
GoogleTranslatorType TranslatorType = "google"
// BingTranslatorType 必应翻译器
BingTranslatorType TranslatorType = "bing"
// YoudaoTranslatorType 有道翻译器
YoudaoTranslatorType TranslatorType = "youdao"
// DeeplTranslatorType DeepL翻译器
DeeplTranslatorType TranslatorType = "deepl"
)
// LanguageInfo 语言信息结构体
type LanguageInfo struct {
Code string // 语言代码
Name string // 语言名称
}
// Translator 翻译器接口,定义所有翻译器必须实现的方法
type Translator interface {
// Translate 使用Go语言提供的标准语言标签进行文本翻译
Translate(text string, from language.Tag, to language.Tag) (string, error)
// TranslateWithParams 使用简单字符串参数进行文本翻译
TranslateWithParams(text string, params TranslationParams) (string, error)
// SetTimeout 设置请求超时时间
SetTimeout(timeout time.Duration)
// GetSupportedLanguages 获取翻译器支持的语言列表
GetSupportedLanguages() map[string]LanguageInfo
// IsLanguageSupported 检查指定的语言代码是否受支持
IsLanguageSupported(languageCode string) bool
// GetStandardLanguageCode 获取标准化的语言代码
GetStandardLanguageCode(languageCode string) string
}
// TranslatorFactory 翻译器工厂,用于创建不同类型的翻译器
type TranslatorFactory struct{}
// NewTranslatorFactory 创建一个新的翻译器工厂
func NewTranslatorFactory() *TranslatorFactory {
return &TranslatorFactory{}
}
// Create 根据类型创建翻译器
func (f *TranslatorFactory) Create(translatorType TranslatorType) (Translator, error) {
switch translatorType {
case GoogleTranslatorType:
return NewGoogleTranslator(), nil
case BingTranslatorType:
return NewBingTranslator(), nil
case YoudaoTranslatorType:
return NewYoudaoTranslator(), nil
case DeeplTranslatorType:
return NewDeeplTranslator(), nil
default:
return nil, fmt.Errorf("unsupported translator type: %s", translatorType)
}
}

View File

@@ -0,0 +1,221 @@
// Package translator 提供文本翻译功能
package translator
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"golang.org/x/net/html"
"golang.org/x/text/language"
)
// YoudaoTranslator 有道翻译器结构体
type YoudaoTranslator struct {
httpClient *http.Client // HTTP客户端
Timeout time.Duration // 请求超时时间
languages map[string]LanguageInfo // 支持的语言列表
}
// 常量定义
const (
youdaoDefaultTimeout = 30 * time.Second
youdaoTranslateURL = "https://m.youdao.com/translate"
)
// 错误定义
var (
ErrYoudaoNetworkError = errors.New("youdao translator network error")
ErrYoudaoParseError = errors.New("youdao translator parse error")
)
// NewYoudaoTranslator 创建一个新的有道翻译器实例
func NewYoudaoTranslator() *YoudaoTranslator {
translator := &YoudaoTranslator{
Timeout: youdaoDefaultTimeout,
httpClient: &http.Client{Timeout: youdaoDefaultTimeout},
languages: initYoudaoLanguages(),
}
return translator
}
// initYoudaoLanguages 初始化有道翻译器支持的语言列表
func initYoudaoLanguages() map[string]LanguageInfo {
// 创建语言映射表
languages := make(map[string]LanguageInfo)
// 自动检测
languages["auto"] = LanguageInfo{Code: "auto", Name: "Auto"}
return languages
}
// SetTimeout 设置请求超时时间
func (t *YoudaoTranslator) SetTimeout(timeout time.Duration) {
t.Timeout = timeout
t.httpClient.Timeout = timeout
}
// Translate 使用标准语言标签进行文本翻译
func (t *YoudaoTranslator) Translate(text string, from language.Tag, to language.Tag) (string, error) {
// 有道翻译不需要指定源语言和目标语言,它会自动检测
return t.translate(text)
}
// TranslateWithParams 使用简单字符串参数进行文本翻译
func (t *YoudaoTranslator) TranslateWithParams(text string, params TranslationParams) (string, error) {
// 设置超时时间(如果有指定)
if params.Timeout > 0 {
t.SetTimeout(params.Timeout)
}
// 有道翻译不需要指定源语言和目标语言,它会自动检测
return t.translate(text)
}
// translate 执行实际翻译操作
func (t *YoudaoTranslator) translate(text string) (string, error) {
// 构建表单数据
form := url.Values{}
form.Add("inputtext", text)
form.Add("type", "AUTO")
// 创建请求
req, err := http.NewRequest("POST", youdaoTranslateURL, strings.NewReader(form.Encode()))
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
// 设置请求头
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
// 发送请求
resp, err := t.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrYoudaoNetworkError, err)
}
defer resp.Body.Close()
// 判断请求是否成功
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("API error: status code %d", resp.StatusCode)
}
// 读取响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
// 解析HTML响应
result, err := t.extractTranslationResult(string(body))
if err != nil {
return "", err
}
return result, nil
}
// extractTranslationResult 从HTML响应中提取翻译结果
func (t *YoudaoTranslator) extractTranslationResult(htmlContent string) (string, error) {
// 方法1使用正则表达式提取翻译结果
pattern := regexp.MustCompile(`<ul id="translateResult"[^>]*>.*?<li[^>]*>(.*?)</li>`)
matches := pattern.FindStringSubmatch(htmlContent)
if len(matches) >= 2 {
// 清理HTML标签
result := matches[1]
result = strings.ReplaceAll(result, "<br>", "\n")
result = t.stripHTMLTags(result)
return result, nil
}
// 方法2使用HTML解析器提取翻译结果
doc, err := html.Parse(strings.NewReader(htmlContent))
if err != nil {
return "", fmt.Errorf("%w: failed to parse HTML", ErrYoudaoParseError)
}
// 查找翻译结果元素
result := t.findTranslateResult(doc)
if result != "" {
return result, nil
}
return "", fmt.Errorf("%w: could not find translation result", ErrYoudaoParseError)
}
// stripHTMLTags 移除HTML标签
func (t *YoudaoTranslator) stripHTMLTags(input string) string {
// 简单的HTML标签移除
re := regexp.MustCompile("<[^>]*>")
return re.ReplaceAllString(input, "")
}
// findTranslateResult 在HTML文档中查找翻译结果
func (t *YoudaoTranslator) findTranslateResult(n *html.Node) string {
if n.Type == html.ElementNode && n.Data == "ul" {
for _, attr := range n.Attr {
if attr.Key == "id" && attr.Val == "translateResult" {
// 找到了translateResult元素提取其中的文本
var result string
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.ElementNode && c.Data == "li" {
return t.extractText(c)
}
}
return result
}
}
}
// 递归查找子节点
for c := n.FirstChild; c != nil; c = c.NextSibling {
result := t.findTranslateResult(c)
if result != "" {
return result
}
}
return ""
}
// extractText 提取节点中的文本内容
func (t *YoudaoTranslator) extractText(n *html.Node) string {
if n.Type == html.TextNode {
return n.Data
}
var result string
for c := n.FirstChild; c != nil; c = c.NextSibling {
result += t.extractText(c)
}
return result
}
// GetSupportedLanguages 获取翻译器支持的语言列表
func (t *YoudaoTranslator) GetSupportedLanguages() map[string]LanguageInfo {
return t.languages
}
// IsLanguageSupported 检查指定的语言代码是否受支持
func (t *YoudaoTranslator) IsLanguageSupported(languageCode string) bool {
_, ok := t.languages[strings.ToLower(languageCode)]
return ok
}
// GetStandardLanguageCode 获取标准化的语言代码
func (t *YoudaoTranslator) GetStandardLanguageCode(languageCode string) string {
// 简单返回小写版本作为标准代码
return strings.ToLower(languageCode)
}

View File

@@ -33,13 +33,13 @@ func RegisterTrayEvents(app *application.App, systray *application.SystemTray, m
// RegisterTrayMenuEvents 注册系统托盘菜单事件
func RegisterTrayMenuEvents(app *application.App, menu *application.Menu, mainWindow *application.WebviewWindow) {
menu.Add("主窗口").OnClick(func(data *application.Context) {
menu.Add("Main window").OnClick(func(data *application.Context) {
mainWindow.Show()
})
menu.AddSeparator()
menu.Add("退出").OnClick(func(data *application.Context) {
menu.Add("Quit").OnClick(func(data *application.Context) {
app.Quit()
})
}

View File

@@ -38,6 +38,29 @@ const (
SystemThemeAuto SystemThemeType = "auto"
)
// UpdateSourceType 更新源类型
type UpdateSourceType string
const (
// UpdateSourceGithub GitHub更新源
UpdateSourceGithub UpdateSourceType = "github"
// UpdateSourceGitea Gitea更新源
UpdateSourceGitea UpdateSourceType = "gitea"
)
// GithubConfig GitHub配置
type GithubConfig struct {
Owner string `json:"owner"` // 仓库所有者
Repo string `json:"repo"` // 仓库名称
}
// GiteaConfig Gitea配置
type GiteaConfig struct {
BaseURL string `json:"baseURL"` // Gitea服务器URL
Owner string `json:"owner"` // 仓库所有者
Repo string `json:"repo"` // 仓库名称
}
// GeneralConfig 通用设置配置
type GeneralConfig struct {
AlwaysOnTop bool `json:"alwaysOnTop"` // 窗口是否置顶
@@ -78,15 +101,21 @@ type EditingConfig struct {
// AppearanceConfig 外观设置配置
type AppearanceConfig struct {
Language LanguageType `json:"language"` // 界面语言
SystemTheme SystemThemeType `json:"systemTheme"` // 系统界面主题
Language LanguageType `json:"language"` // 界面语言
SystemTheme SystemThemeType `json:"systemTheme"` // 系统界面主题
CustomTheme CustomThemeConfig `json:"customTheme"` // 自定义主题配置
}
// UpdatesConfig 更新设置配置
type UpdatesConfig struct {
Version string `json:"version"` // 当前版本号
AutoUpdate bool `json:"autoUpdate"` // 是否自动更新
BetaChannel bool `json:"betaChannel"` // 是否启用测试版
Version string `json:"version"` // 当前版本号
AutoUpdate bool `json:"autoUpdate"` // 是否自动更新
PrimarySource UpdateSourceType `json:"primarySource"` // 主要更新源
BackupSource UpdateSourceType `json:"backupSource"` // 备用更新源
BackupBeforeUpdate bool `json:"backupBeforeUpdate"` // 更新前是否备份
UpdateTimeout int `json:"updateTimeout"` // 更新超时时间(秒)
Github GithubConfig `json:"github"` // GitHub配置
Gitea GiteaConfig `json:"gitea"` // Gitea配置
}
// AppConfig 应用配置 - 按照前端设置页面分类组织
@@ -107,7 +136,7 @@ type ConfigMetadata struct {
// NewDefaultAppConfig 创建默认应用配置
func NewDefaultAppConfig() *AppConfig {
currentDir, _ := os.UserConfigDir()
currentDir, _ := os.UserHomeDir()
dataDir := filepath.Join(currentDir, ".voidraft", "data")
return &AppConfig{
@@ -134,18 +163,31 @@ func NewDefaultAppConfig() *AppConfig {
// Tab设置
EnableTabIndent: true,
TabSize: 4,
TabType: TabTypeSpaces,
TabType: TabTypeTab,
// 保存选项
AutoSaveDelay: 5000, // 5秒后自动保存
AutoSaveDelay: 2000,
},
Appearance: AppearanceConfig{
Language: LangZhCN,
SystemTheme: SystemThemeAuto, // 默认使用深色系统主题
Language: LangEnUS,
SystemTheme: SystemThemeAuto,
CustomTheme: *NewDefaultCustomThemeConfig(),
},
Updates: UpdatesConfig{
Version: "1.0.0",
AutoUpdate: true,
BetaChannel: false,
Version: "1.0.0",
AutoUpdate: true,
PrimarySource: UpdateSourceGithub,
BackupSource: UpdateSourceGitea,
BackupBeforeUpdate: true,
UpdateTimeout: 30,
Github: GithubConfig{
Owner: "landaiqing",
Repo: "voidraft",
},
Gitea: GiteaConfig{
BaseURL: "https://git.landaiqing.cn",
Owner: "landaiqing",
Repo: "voidraft",
},
},
Metadata: ConfigMetadata{
LastUpdated: time.Now().Format(time.RFC3339),

View File

@@ -4,38 +4,29 @@ import (
"time"
)
// DocumentMeta 文档元数据
type DocumentMeta struct {
ID string `json:"id"` // 文档唯一标识
Title string `json:"title"` // 文档标题
LastUpdated time.Time `json:"lastUpdated"` // 最后更新时间
CreatedAt time.Time `json:"createdAt"` // 创建时间
}
// Document 表示一个文档
// Document represents a document in the system
type Document struct {
Meta DocumentMeta `json:"meta"` // 元数据
Content string `json:"content"` // 文档内容
ID int64 `json:"id" db:"id"`
Title string `json:"title" db:"title"`
Content string `json:"content" db:"content"`
CreatedAt time.Time `json:"createdAt" db:"created_at"`
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
IsDeleted bool `json:"is_deleted"`
}
// DocumentInfo 文档信息(不包含内容,用于列表展示
type DocumentInfo struct {
ID string `json:"id"` // 文档ID
Title string `json:"title"` // 文档标题
LastUpdated time.Time `json:"lastUpdated"` // 最后更新时间
Path string `json:"path"` // 文档路径
// NewDocument 创建新文档不需要传ID由数据库自增
func NewDocument(title, content string) *Document {
now := time.Now()
return &Document{
Title: title,
Content: content,
CreatedAt: now,
UpdatedAt: now,
IsDeleted: false,
}
}
// NewDefaultDocument 创建默认文档
func NewDefaultDocument() *Document {
now := time.Now()
return &Document{
Meta: DocumentMeta{
ID: "default",
Title: "默认文档",
LastUpdated: now,
CreatedAt: now,
},
Content: "∞∞∞text-a\n",
}
return NewDocument("default", "∞∞∞text-a\n")
}

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