Compare commits
89 Commits
ab0255a775
...
v1.2.0
Author | SHA1 | Date | |
---|---|---|---|
a720a4cfb8 | |||
b5510d605c | |||
4d62da912a | |||
b52e067d50 | |||
8dce06c30e | |||
b404434b5b | |||
685897e828 | |||
4f1d70135e | |||
4dc424781b | |||
7fcfc5e992 | |||
7c2318a13f | |||
a2a332e735 | |||
ebee33ea7c | |||
81eb2c94ac | |||
25e1a98932 | |||
1ccee779ae | |||
3e45e6aa9b | |||
1604564e63 | |||
70d88dabba | |||
6f8775472d | |||
a9b967aba4 | |||
69957a16cf | |||
650884cb85 | |||
8e91e3cf7c | |||
f3bcb87828 | |||
ea025e3f5d | |||
4f8272e290 | |||
d6dd34db87 | |||
eb9b037f8e | |||
35c89e086e | |||
77287bccfa | |||
a92e5486b2 | |||
1153c0a652 | |||
145b868a44 | |||
6acab678d0 | |||
cb3d369aef | |||
85544ba1e4 | |||
13072a00a1 | |||
25858cb42b | |||
f2894b2a89 | |||
a62ea251cd | |||
3c880199ae | |||
9204315c7b | |||
cce9cf7e92 | |||
87fe9d48b1 | |||
1d6cf7cf68 | |||
0927b921c3 | |||
4844ccdf58 | |||
c79cba48c2 | |||
ceb177114d | |||
5203784b63 | |||
aff08f4d3d | |||
7f97b4a937 | |||
8522a47b5f | |||
d5a0b07f2a | |||
61f293ce6f | |||
31addd5a20 | |||
3e20f47b8e | |||
d9745ac7d1 | |||
5381e4ba17 | |||
5d7f6ec6a1 | |||
81868e8d37 | |||
77bd15bed7 | |||
a516b8973e | |||
44f7baad10 | |||
5f102edcf7 | |||
72a222f932 | |||
7f25dc942e | |||
4e291b889b | |||
1246166231 | |||
bd0bbc9674 | |||
33bd4940e9 | |||
7e6e7e4e73 | |||
c0db3a68cd | |||
e0b9a376cb | |||
0710f0c9b0 | |||
c9aa8aebcb | |||
198ba44ceb | |||
d7a848e7ad | |||
0d3df25a94 | |||
618bae3afe | |||
3ab209f899 | |||
946075f25d | |||
e87d5ec929 | |||
b3ecc08468 | |||
3483ac4cf6 | |||
8673b36f98 | |||
dcf07c9106 | |||
f41a13c217 |
83
LICENSE
83
LICENSE
@@ -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.
|
180
README.md
180
README.md
@@ -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
|
||||
|
||||
```
|
||||
### Developer-Friendly
|
||||
|
||||
- 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
|
||||
|
||||
### Modern Interface
|
||||
|
||||
- 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
|
||||
|
||||
### Extension System
|
||||
|
||||
- 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
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Download 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
|
||||
|
||||
### Development Environment
|
||||
|
||||
```bash
|
||||
# Clone the project
|
||||
git clone https://github.com/landaiqing/voidraft
|
||||
cd voidraft
|
||||
|
||||
# Install frontend dependencies
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
cd ..
|
||||
|
||||
# Start development server
|
||||
wails3 dev
|
||||
```
|
||||
|
||||
This will start your application and enable hot-reloading for both frontend and backend changes.
|
||||
### Production Build
|
||||
|
||||
3. To build your application for production, use:
|
||||
|
||||
```
|
||||
wails3 build
|
||||
```bash
|
||||
# Build application
|
||||
wails3 package
|
||||
```
|
||||
|
||||
This will create a production-ready executable in the `build` directory.
|
||||
After building, the executable will be generated in the `bin` directory.
|
||||
|
||||
## Exploring Wails3 Features
|
||||
## Technical Architecture
|
||||
|
||||
Now that you have your project set up, it's time to explore the features that Wails3 offers:
|
||||
### Core Technologies
|
||||
|
||||
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.
|
||||
|
||||
2. **Run an example**: To run any of the examples, navigate to the example's directory and use:
|
||||
|
||||
```
|
||||
go run .
|
||||
```
|
||||
|
||||
Note: Some examples may be under development during the alpha phase.
|
||||
|
||||
3. **Explore the documentation**: Visit the [Wails3 documentation](https://v3alpha.wails.io/) for in-depth guides and API references.
|
||||
|
||||
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).
|
||||
| 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.
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/yourusername/Voidraft)
|
||||
[](https://github.com/yourusername/Voidraft)
|
||||
|
||||
*Made with ❤️ by landaiqing*
|
165
README_ZH.md
Normal file
165
README_ZH.md
Normal 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 和贡献代码。
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/yourusername/Voidraft)
|
||||
[](https://github.com/yourusername/Voidraft)
|
||||
|
||||
*Made with ❤️ by landaiqing*
|
@@ -5,13 +5,13 @@ version: '3'
|
||||
|
||||
# This information is used to generate the build assets.
|
||||
info:
|
||||
companyName: "My Company" # The name of the company
|
||||
productName: "My Product" # The name of the application
|
||||
productIdentifier: "com.mycompany.myproduct" # The unique product identifier
|
||||
description: "A program that does X" # The application description
|
||||
copyright: "(c) 2024, My Company" # Copyright text
|
||||
comments: "Some Product Comments" # Comments
|
||||
version: "v0.0.1" # The application version
|
||||
companyName: "Voidraft" # The name of the company
|
||||
productName: "Voidraft" # The name of the application
|
||||
productIdentifier: "landaiqing" # The unique product identifier
|
||||
description: "Voidraft" # The application description
|
||||
copyright: "© 2025 Voidraft. All rights reserved." # Copyright text
|
||||
comments: "Voidraft" # Comments
|
||||
version: "0.0.1.0" # The application version
|
||||
|
||||
# Dev mode configuration
|
||||
dev_mode:
|
||||
|
@@ -4,17 +4,17 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>My Product</string>
|
||||
<string>Voidraft</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>voidraft.exe</string>
|
||||
<string>Voidraft</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.wails.voidraft</string>
|
||||
<string>landaiqing</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1.0</string>
|
||||
<string>0.0.1.0</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>This is a comment</string>
|
||||
<string>Voidraft</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<string>0.0.1.0</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icons</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
@@ -22,7 +22,7 @@
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© now, My Company</string>
|
||||
<string>© 2025 Voidraft. All rights reserved.</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
|
@@ -4,17 +4,17 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>My Product</string>
|
||||
<string>Voidraft</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>voidraft.exe</string>
|
||||
<string>Voidraft</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.wails.voidraft</string>
|
||||
<string>landaiqing</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1.0</string>
|
||||
<string>0.0.1.0</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>This is a comment</string>
|
||||
<string>Voidraft</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<string>0.0.1.0</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icons</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
@@ -22,6 +22,6 @@
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© now, My Company</string>
|
||||
<string>© 2025 Voidraft. All rights reserved.</string>
|
||||
</dict>
|
||||
</plist>
|
BIN
build/linux/appimage/appicon.png
Normal file
BIN
build/linux/appimage/appicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
@@ -3,26 +3,26 @@
|
||||
#
|
||||
# The lines below are called `modelines`. See `:help modeline`
|
||||
|
||||
name: "voidraft.exe"
|
||||
name: "Voidraft"
|
||||
arch: ${GOARCH}
|
||||
platform: "linux"
|
||||
version: "0.1.0"
|
||||
version: "0.0.1.0"
|
||||
section: "default"
|
||||
priority: "extra"
|
||||
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
|
||||
description: "My Product Description"
|
||||
vendor: "My Company"
|
||||
description: "Voidraft"
|
||||
vendor: "Voidraft"
|
||||
homepage: "https://wails.io"
|
||||
license: "MIT"
|
||||
release: "1"
|
||||
|
||||
contents:
|
||||
- src: "./bin/voidraft.exe"
|
||||
dst: "/usr/local/bin/voidraft.exe"
|
||||
- src: "./bin/Voidraft"
|
||||
dst: "/usr/local/bin/Voidraft"
|
||||
- src: "./build/appicon.png"
|
||||
dst: "/usr/share/icons/hicolor/128x128/apps/voidraft.exe.png"
|
||||
- src: "./build/linux/voidraft.exe.desktop"
|
||||
dst: "/usr/share/applications/voidraft.exe.desktop"
|
||||
dst: "/usr/share/icons/hicolor/128x128/apps/Voidraft.png"
|
||||
- src: "./build/linux/Voidraft.desktop"
|
||||
dst: "/usr/share/applications/Voidraft.desktop"
|
||||
|
||||
depends:
|
||||
- gtk3
|
||||
|
10
build/linux/voidraft.desktop
Normal file
10
build/linux/voidraft.desktop
Normal file
@@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=voidraft
|
||||
Exec=voidraft
|
||||
Icon=appicon
|
||||
Categories=Development;
|
||||
Terminal=false
|
||||
Keywords=wails
|
||||
Version=1.0
|
||||
StartupNotify=false
|
@@ -23,7 +23,7 @@ tasks:
|
||||
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}'
|
||||
env:
|
||||
GOOS: windows
|
||||
CGO_ENABLED: 0
|
||||
CGO_ENABLED: 1
|
||||
GOARCH: '{{.ARCH | default ARCH}}'
|
||||
PRODUCTION: '{{.PRODUCTION | default "false"}}'
|
||||
|
||||
|
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"fixed": {
|
||||
"file_version": "0.1.0"
|
||||
"file_version": "0.0.1.0"
|
||||
},
|
||||
"info": {
|
||||
"0000": {
|
||||
"ProductVersion": "0.1.0",
|
||||
"CompanyName": "My Company",
|
||||
"FileDescription": "My Product Description",
|
||||
"LegalCopyright": "© now, My Company",
|
||||
"ProductName": "My Product",
|
||||
"Comments": "This is a comment"
|
||||
"ProductVersion": "0.0.1.0",
|
||||
"CompanyName": "Voidraft",
|
||||
"FileDescription": "Voidraft",
|
||||
"LegalCopyright": "© 2025 Voidraft. All rights reserved.",
|
||||
"ProductName": "Voidraft",
|
||||
"Comments": "Voidraft"
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,19 +5,19 @@
|
||||
!include "FileFunc.nsh"
|
||||
|
||||
!ifndef INFO_PROJECTNAME
|
||||
!define INFO_PROJECTNAME "voidraft"
|
||||
!define INFO_PROJECTNAME "Voidraft"
|
||||
!endif
|
||||
!ifndef INFO_COMPANYNAME
|
||||
!define INFO_COMPANYNAME "My Company"
|
||||
!define INFO_COMPANYNAME "Voidraft"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTNAME
|
||||
!define INFO_PRODUCTNAME "My Product"
|
||||
!define INFO_PRODUCTNAME "Voidraft"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTVERSION
|
||||
!define INFO_PRODUCTVERSION "0.1.0"
|
||||
!define INFO_PRODUCTVERSION "0.0.1.0"
|
||||
!endif
|
||||
!ifndef INFO_COPYRIGHT
|
||||
!define INFO_COPYRIGHT "© now, My Company"
|
||||
!define INFO_COPYRIGHT "© 2025 Voidraft. All rights reserved."
|
||||
!endif
|
||||
!ifndef PRODUCT_EXECUTABLE
|
||||
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity type="win32" name="com.wails.voidraft" version="0.1.0" processorArchitecture="*"/>
|
||||
<assemblyIdentity type="win32" name="landaiqing" version="0.0.1.0" processorArchitecture="*"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
|
@@ -0,0 +1,4 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export * from "./models.js";
|
@@ -0,0 +1,443 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as slog$0 from "../../../../../../log/slog/models.js";
|
||||
|
||||
export class App {
|
||||
/**
|
||||
* Manager pattern for organized API
|
||||
*/
|
||||
"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 (!("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;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new App instance from a string or object.
|
||||
*/
|
||||
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 ("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"] = $$createField11_0($$parsedSource["Logger"]);
|
||||
}
|
||||
return new App($$parsedSource as Partial<App>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BrowserManager manages browser-related operations
|
||||
*/
|
||||
export class BrowserManager {
|
||||
|
||||
/** Creates a new BrowserManager instance. */
|
||||
constructor($$source: Partial<BrowserManager> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new BrowserManager instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): BrowserManager {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
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>);
|
||||
}
|
||||
}
|
||||
|
||||
export class WebviewWindow {
|
||||
|
||||
/** Creates a new WebviewWindow instance. */
|
||||
constructor($$source: Partial<WebviewWindow> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new WebviewWindow instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): WebviewWindow {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new WebviewWindow($$parsedSource as Partial<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 = WindowManager.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
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);
|
@@ -1,7 +1,9 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as GreetService from "./greetservice.js";
|
||||
import * as Service from "./service.js";
|
||||
export {
|
||||
GreetService
|
||||
Service
|
||||
};
|
||||
|
||||
export * from "./models.js";
|
@@ -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;
|
@@ -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);
|
@@ -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);
|
||||
}
|
||||
}
|
4
frontend/bindings/log/slog/index.ts
Normal file
4
frontend/bindings/log/slog/index.ts
Normal 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";
|
31
frontend/bindings/log/slog/models.ts
Normal file
31
frontend/bindings/log/slog/models.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// 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";
|
||||
|
||||
/**
|
||||
* A Logger records structured information about each call to its
|
||||
* Log, Debug, Info, Warn, and Error methods.
|
||||
* For each call, it creates a [Record] and passes it to a [Handler].
|
||||
*
|
||||
* To create a new Logger, call [New] or a Logger method
|
||||
* that begins "With".
|
||||
*/
|
||||
export class Logger {
|
||||
|
||||
/** Creates a new Logger instance. */
|
||||
constructor($$source: Partial<Logger> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Logger instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Logger {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new Logger($$parsedSource as Partial<Logger>);
|
||||
}
|
||||
}
|
4
frontend/bindings/time/index.ts
Normal file
4
frontend/bindings/time/index.ts
Normal 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";
|
51
frontend/bindings/time/models.ts
Normal file
51
frontend/bindings/time/models.ts
Normal 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";
|
||||
|
||||
/**
|
||||
* A Time represents an instant in time with nanosecond precision.
|
||||
*
|
||||
* Programs using times should typically store and pass them as values,
|
||||
* not pointers. That is, time variables and struct fields should be of
|
||||
* type [time.Time], not *time.Time.
|
||||
*
|
||||
* A Time value can be used by multiple goroutines simultaneously except
|
||||
* that the methods [Time.GobDecode], [Time.UnmarshalBinary], [Time.UnmarshalJSON] and
|
||||
* [Time.UnmarshalText] are not concurrency-safe.
|
||||
*
|
||||
* Time instants can be compared using the [Time.Before], [Time.After], and [Time.Equal] methods.
|
||||
* The [Time.Sub] method subtracts two instants, producing a [Duration].
|
||||
* The [Time.Add] method adds a Time and a Duration, producing a Time.
|
||||
*
|
||||
* The zero value of type Time is January 1, year 1, 00:00:00.000000000 UTC.
|
||||
* As this time is unlikely to come up in practice, the [Time.IsZero] method gives
|
||||
* a simple way of detecting a time that has not been initialized explicitly.
|
||||
*
|
||||
* Each time has an associated [Location]. The methods [Time.Local], [Time.UTC], and Time.In return a
|
||||
* Time with a specific Location. Changing the Location of a Time value with
|
||||
* these methods does not change the actual instant it represents, only the time
|
||||
* zone in which to interpret it.
|
||||
*
|
||||
* Representations of a Time value saved by the [Time.GobEncode], [Time.MarshalBinary], [Time.AppendBinary],
|
||||
* [Time.MarshalJSON], [Time.MarshalText] and [Time.AppendText] methods store the [Time.Location]'s offset,
|
||||
* but not the location name. They therefore lose information about Daylight Saving Time.
|
||||
*
|
||||
* In addition to the required “wall clock” reading, a Time may contain an optional
|
||||
* reading of the current process's monotonic clock, to provide additional precision
|
||||
* for comparison or subtraction.
|
||||
* See the “Monotonic Clocks” section in the package documentation for details.
|
||||
*
|
||||
* Note that the Go == operator compares not just the time instant but also the
|
||||
* Location and the monotonic clock reading. Therefore, Time values should not
|
||||
* be used as map or database keys without first guaranteeing that the
|
||||
* identical Location has been set for all values, which can be achieved
|
||||
* through use of the UTC or Local method, and that the monotonic clock reading
|
||||
* has been stripped by setting t = t.Round(0). In general, prefer t.Equal(u)
|
||||
* to t == u, since t.Equal uses the most accurate comparison available and
|
||||
* correctly handles the case when only one of its arguments has a monotonic
|
||||
* clock reading.
|
||||
*/
|
||||
export type Time = any;
|
@@ -0,0 +1,4 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export * from "./models.js";
|
@@ -0,0 +1,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",
|
||||
};
|
4
frontend/bindings/voidraft/internal/models/index.ts
Normal file
4
frontend/bindings/voidraft/internal/models/index.ts
Normal 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";
|
1422
frontend/bindings/voidraft/internal/models/models.ts
Normal file
1422
frontend/bindings/voidraft/internal/models/models.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* ConfigService 应用配置服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
/**
|
||||
* Get 获取配置项
|
||||
*/
|
||||
export function Get(key: string): Promise<any> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(807201772, key) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetConfig 获取完整应用配置
|
||||
*/
|
||||
export function GetConfig(): Promise<models$0.AppConfig | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1013336538) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetConfig 强制重置所有配置为默认值
|
||||
*/
|
||||
export function ResetConfig(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3593047389) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown 关闭服务
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3963562361) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set 设置配置项
|
||||
*/
|
||||
export function Set(key: string, value: any): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2921955968, key, value) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetDataPathChangeCallback 设置数据路径配置变更回调
|
||||
*/
|
||||
export function SetDataPathChangeCallback(callback: any): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(393017412, callback) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetHotkeyChangeCallback 设置热键配置变更回调
|
||||
*/
|
||||
export function SetHotkeyChangeCallback(callback: any): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(283872321, callback) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.AppConfig.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
@@ -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;
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* DialogService 对话框服务,处理文件选择等对话框操作
|
||||
* @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";
|
||||
|
||||
/**
|
||||
* SelectDirectory 打开目录选择对话框
|
||||
*/
|
||||
export function SelectDirectory(): Promise<string> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2249533621) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetWindow 设置绑定的窗口
|
||||
*/
|
||||
export function SetWindow(window: application$0.WebviewWindow | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(968177170, window) as any;
|
||||
return $resultPromise;
|
||||
}
|
119
frontend/bindings/voidraft/internal/services/documentservice.ts
Normal file
119
frontend/bindings/voidraft/internal/services/documentservice.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* DocumentService provides document management 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";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
/**
|
||||
* CreateDocument creates a new document and returns the created document with ID
|
||||
*/
|
||||
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;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* DeleteDocument marks a document as deleted (default document with ID=1 cannot be deleted)
|
||||
*/
|
||||
export function DeleteDocument(id: number): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(412287269, id) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetDocumentByID gets a document by ID
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* ListAllDocumentsMeta lists all active (non-deleted) document metadata
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup initializes the service when the application starts
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1474135487, options) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateDocumentContent updates the content of a document
|
||||
*/
|
||||
export function UpdateDocumentContent(id: number, content: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3251897116, id, content) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateDocumentTitle updates the title of a document
|
||||
*/
|
||||
export function UpdateDocumentTitle(id: number, title: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2045530459, id, title) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.Document.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Array($$createType1);
|
@@ -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);
|
@@ -0,0 +1,82 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* HotkeyService Windows全局热键服务
|
||||
* @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";
|
||||
|
||||
/**
|
||||
* GetCurrentHotkey 获取当前热键
|
||||
*/
|
||||
export function GetCurrentHotkey(): Promise<models$0.HotkeyCombo | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2572811187) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize 初始化热键服务
|
||||
*/
|
||||
export function Initialize(app: application$0.App | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3671360458, app) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* IsRegistered 检查是否已注册
|
||||
*/
|
||||
export function IsRegistered(): Promise<boolean> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(106954156) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* RegisterHotkey 注册全局热键
|
||||
*/
|
||||
export function RegisterHotkey(hotkey: models$0.HotkeyCombo | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1103945691, hotkey) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown 关闭服务
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(157291181) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UnregisterHotkey 取消注册全局热键
|
||||
*/
|
||||
export function UnregisterHotkey(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3544283732) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateHotkey 更新热键配置
|
||||
*/
|
||||
export function UpdateHotkey(enable: boolean, hotkey: models$0.HotkeyCombo | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(823285555, enable, hotkey) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.HotkeyCombo.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
35
frontend/bindings/voidraft/internal/services/index.ts
Normal file
35
frontend/bindings/voidraft/internal/services/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// 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,
|
||||
TranslationService,
|
||||
TrayService,
|
||||
WindowService
|
||||
};
|
||||
|
||||
export * from "./models.js";
|
@@ -0,0 +1,42 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* KeyBindingService 快捷键管理服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
/**
|
||||
* GetAllKeyBindings 获取所有快捷键配置
|
||||
*/
|
||||
export function GetAllKeyBindings(): Promise<models$0.KeyBinding[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1633502882) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup 启动时调用
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2057121990, options) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.KeyBinding.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
@@ -0,0 +1,54 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* MigrationService 迁移服务
|
||||
* @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";
|
||||
|
||||
/**
|
||||
* CancelMigration 取消迁移
|
||||
*/
|
||||
export function CancelMigration(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1813274502) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetProgress 获取当前进度
|
||||
*/
|
||||
export function GetProgress(): Promise<$models.MigrationProgress> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3413264131) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* MigrateDirectory 迁移目录
|
||||
*/
|
||||
export function MigrateDirectory(srcPath: string, dstPath: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(311970580, srcPath, dstPath) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown 服务关闭
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3472042605) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.MigrationProgress.createFrom;
|
243
frontend/bindings/voidraft/internal/services/models.ts
Normal file
243
frontend/bindings/voidraft/internal/services/models.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
|
||||
/**
|
||||
* MemoryStats 内存统计信息
|
||||
*/
|
||||
export class MemoryStats {
|
||||
/**
|
||||
* 当前堆内存使用量(字节)
|
||||
*/
|
||||
"heapInUse": number;
|
||||
|
||||
/**
|
||||
* 堆内存分配总量(字节)
|
||||
*/
|
||||
"heapAlloc": number;
|
||||
|
||||
/**
|
||||
* 系统内存使用量(字节)
|
||||
*/
|
||||
"sys": number;
|
||||
|
||||
/**
|
||||
* GC 次数
|
||||
*/
|
||||
"numGC": number;
|
||||
|
||||
/**
|
||||
* GC 暂停时间(纳秒)
|
||||
*/
|
||||
"pauseTotalNs": number;
|
||||
|
||||
/**
|
||||
* Goroutine 数量
|
||||
*/
|
||||
"numGoroutine": number;
|
||||
|
||||
/** Creates a new MemoryStats instance. */
|
||||
constructor($$source: Partial<MemoryStats> = {}) {
|
||||
if (!("heapInUse" in $$source)) {
|
||||
this["heapInUse"] = 0;
|
||||
}
|
||||
if (!("heapAlloc" in $$source)) {
|
||||
this["heapAlloc"] = 0;
|
||||
}
|
||||
if (!("sys" in $$source)) {
|
||||
this["sys"] = 0;
|
||||
}
|
||||
if (!("numGC" in $$source)) {
|
||||
this["numGC"] = 0;
|
||||
}
|
||||
if (!("pauseTotalNs" in $$source)) {
|
||||
this["pauseTotalNs"] = 0;
|
||||
}
|
||||
if (!("numGoroutine" in $$source)) {
|
||||
this["numGoroutine"] = 0;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new MemoryStats instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): MemoryStats {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new MemoryStats($$parsedSource as Partial<MemoryStats>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MigrationProgress 迁移进度信息
|
||||
*/
|
||||
export class MigrationProgress {
|
||||
"status": MigrationStatus;
|
||||
"progress": number;
|
||||
"error"?: string;
|
||||
|
||||
/** Creates a new MigrationProgress instance. */
|
||||
constructor($$source: Partial<MigrationProgress> = {}) {
|
||||
if (!("status" in $$source)) {
|
||||
this["status"] = ("" as MigrationStatus);
|
||||
}
|
||||
if (!("progress" in $$source)) {
|
||||
this["progress"] = 0;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new MigrationProgress instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): MigrationProgress {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new MigrationProgress($$parsedSource as Partial<MigrationProgress>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MigrationStatus 迁移状态
|
||||
*/
|
||||
export enum MigrationStatus {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
MigrationStatusMigrating = "migrating",
|
||||
MigrationStatusCompleted = "completed",
|
||||
MigrationStatusFailed = "failed",
|
||||
};
|
||||
|
||||
/**
|
||||
* SelfUpdateResult 自我更新结果
|
||||
*/
|
||||
export class SelfUpdateResult {
|
||||
/**
|
||||
* 是否有更新
|
||||
*/
|
||||
"hasUpdate": boolean;
|
||||
|
||||
/**
|
||||
* 当前版本
|
||||
*/
|
||||
"currentVersion": string;
|
||||
|
||||
/**
|
||||
* 最新版本
|
||||
*/
|
||||
"latestVersion": string;
|
||||
|
||||
/**
|
||||
* 是否已应用更新
|
||||
*/
|
||||
"updateApplied": boolean;
|
||||
|
||||
/**
|
||||
* 下载链接
|
||||
*/
|
||||
"assetURL": string;
|
||||
|
||||
/**
|
||||
* 发布说明
|
||||
*/
|
||||
"releaseNotes": string;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
"error": string;
|
||||
|
||||
/**
|
||||
* 更新源(github/gitea)
|
||||
*/
|
||||
"source": string;
|
||||
|
||||
/** Creates a new SelfUpdateResult instance. */
|
||||
constructor($$source: Partial<SelfUpdateResult> = {}) {
|
||||
if (!("hasUpdate" in $$source)) {
|
||||
this["hasUpdate"] = false;
|
||||
}
|
||||
if (!("currentVersion" in $$source)) {
|
||||
this["currentVersion"] = "";
|
||||
}
|
||||
if (!("latestVersion" in $$source)) {
|
||||
this["latestVersion"] = "";
|
||||
}
|
||||
if (!("updateApplied" in $$source)) {
|
||||
this["updateApplied"] = false;
|
||||
}
|
||||
if (!("assetURL" in $$source)) {
|
||||
this["assetURL"] = "";
|
||||
}
|
||||
if (!("releaseNotes" in $$source)) {
|
||||
this["releaseNotes"] = "";
|
||||
}
|
||||
if (!("error" in $$source)) {
|
||||
this["error"] = "";
|
||||
}
|
||||
if (!("source" in $$source)) {
|
||||
this["source"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new SelfUpdateResult instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): SelfUpdateResult {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new SelfUpdateResult($$parsedSource as Partial<SelfUpdateResult>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WindowInfo 窗口信息
|
||||
*/
|
||||
export class WindowInfo {
|
||||
"Window": application$0.WebviewWindow | null;
|
||||
"DocumentID": number;
|
||||
"Title": string;
|
||||
|
||||
/** Creates a new WindowInfo instance. */
|
||||
constructor($$source: Partial<WindowInfo> = {}) {
|
||||
if (!("Window" in $$source)) {
|
||||
this["Window"] = null;
|
||||
}
|
||||
if (!("DocumentID" in $$source)) {
|
||||
this["DocumentID"] = 0;
|
||||
}
|
||||
if (!("Title" in $$source)) {
|
||||
this["Title"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new WindowInfo instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): WindowInfo {
|
||||
const $$createField0_0 = $$createType1;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("Window" in $$parsedSource) {
|
||||
$$parsedSource["Window"] = $$createField0_0($$parsedSource["Window"]);
|
||||
}
|
||||
return new WindowInfo($$parsedSource as Partial<WindowInfo>);
|
||||
}
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = application$0.WebviewWindow.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
@@ -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);
|
@@ -1,11 +1,19 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* StartupService 开机启动服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
export function Greet(name: string): Promise<string> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1411160069, name) as any;
|
||||
/**
|
||||
* SetEnabled 设置开机启动状态
|
||||
*/
|
||||
export function SetEnabled(enabled: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2911601468, enabled) as any;
|
||||
return $resultPromise;
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* SystemService 系统监控服务
|
||||
* @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";
|
||||
|
||||
/**
|
||||
* FormatBytes 格式化字节数为人类可读的格式
|
||||
*/
|
||||
export function FormatBytes(bytes: number): Promise<string> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1368998019, bytes) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetMemoryStats 获取当前内存统计信息
|
||||
*/
|
||||
export function GetMemoryStats(): Promise<$models.MemoryStats> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1678201009) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* TriggerGC 手动触发垃圾回收
|
||||
*/
|
||||
export function TriggerGC(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(741882899) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.MemoryStats.createFrom;
|
@@ -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);
|
63
frontend/bindings/voidraft/internal/services/trayservice.ts
Normal file
63
frontend/bindings/voidraft/internal/services/trayservice.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* TrayService 系统托盘服务
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// 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";
|
||||
|
||||
/**
|
||||
* HandleWindowClose 处理窗口关闭事件
|
||||
*/
|
||||
export function HandleWindowClose(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1824247204) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* HandleWindowMinimize 处理窗口最小化事件
|
||||
*/
|
||||
export function HandleWindowMinimize(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(178686624) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* MinimizeButtonClicked 处理标题栏最小化按钮点击
|
||||
*/
|
||||
export function MinimizeButtonClicked(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2477618539) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetAppReferences 设置应用引用
|
||||
*/
|
||||
export function SetAppReferences(app: application$0.App | null, mainWindow: application$0.WebviewWindow | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3544515719, app, mainWindow) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ShouldMinimizeToTray 检查是否应该最小化到托盘
|
||||
*/
|
||||
export function ShouldMinimizeToTray(): Promise<boolean> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3403884012) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ShowWindow 显示主窗口
|
||||
*/
|
||||
export function ShowWindow(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1315913255) as any;
|
||||
return $resultPromise;
|
||||
}
|
@@ -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);
|
10
frontend/components.d.ts
vendored
10
frontend/components.d.ts
vendored
@@ -8,8 +8,16 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
BlockLanguageSelector: typeof import('./src/components/toolbar/BlockLanguageSelector.vue')['default']
|
||||
DocumentSelector: typeof import('./src/components/toolbar/DocumentSelector.vue')['default']
|
||||
LinuxTitleBar: typeof import('./src/components/titlebar/LinuxTitleBar.vue')['default']
|
||||
LoadingScreen: typeof import('./src/components/loading/LoadingScreen.vue')['default']
|
||||
MacOSTitleBar: typeof import('./src/components/titlebar/MacOSTitleBar.vue')['default']
|
||||
MemoryMonitor: typeof import('./src/components/monitor/MemoryMonitor.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Toolbar: typeof import('./src/components/toolbar/index.vue')['default']
|
||||
Toolbar: typeof import('./src/components/toolbar/Toolbar.vue')['default']
|
||||
WindowsTitleBar: typeof import('./src/components/titlebar/WindowsTitleBar.vue')['default']
|
||||
WindowTitleBar: typeof import('./src/components/titlebar/WindowTitleBar.vue')['default']
|
||||
}
|
||||
}
|
||||
|
1455
frontend/package-lock.json
generated
1455
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,58 +14,69 @@
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.18.6",
|
||||
"@codemirror/commands": "^6.8.1",
|
||||
"@codemirror/lang-angular": "^0.1.3",
|
||||
"@codemirror/lang-cpp": "^6.0.2",
|
||||
"@codemirror/lang-angular": "^0.1.4",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-go": "^6.0.1",
|
||||
"@codemirror/lang-html": "^6.4.9",
|
||||
"@codemirror/lang-java": "^6.0.1",
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-java": "^6.0.2",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-less": "^6.0.2",
|
||||
"@codemirror/lang-lezer": "^6.0.2",
|
||||
"@codemirror/lang-liquid": "^6.2.3",
|
||||
"@codemirror/lang-markdown": "^6.3.2",
|
||||
"@codemirror/lang-php": "^6.0.1",
|
||||
"@codemirror/lang-python": "^6.1.7",
|
||||
"@codemirror/lang-rust": "^6.0.1",
|
||||
"@codemirror/lang-markdown": "^6.3.3",
|
||||
"@codemirror/lang-php": "^6.0.2",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/lang-rust": "^6.0.2",
|
||||
"@codemirror/lang-sass": "^6.0.2",
|
||||
"@codemirror/lang-sql": "^6.8.0",
|
||||
"@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.0",
|
||||
"@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.10",
|
||||
"@codemirror/search": "^6.5.11",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.36.5",
|
||||
"@codemirror/view": "^6.38.0",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@primeuix/themes": "^1.0.3",
|
||||
"codemirror": "^6.0.1",
|
||||
"pinia": "^3.0.2",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"primevue": "^4.3.3",
|
||||
"sass": "^1.87.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"vue-router": "^4.5.0"
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"codemirror": "^6.0.2",
|
||||
"codemirror-lang-elixir": "^4.0.0",
|
||||
"colors-named": "^1.0.2",
|
||||
"colors-named-hex": "^1.0.2",
|
||||
"franc-min": "^6.2.0",
|
||||
"hsl-matcher": "^1.2.4",
|
||||
"lezer": "^0.13.5",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.4.1",
|
||||
"prettier": "^3.6.2",
|
||||
"remarkable": "^2.0.1",
|
||||
"sass": "^1.89.2",
|
||||
"vue": "^3.5.17",
|
||||
"vue-i18n": "^11.1.9",
|
||||
"vue-pick-colors": "^1.8.0",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.1",
|
||||
"@primevue/auto-import-resolver": "^4.3.3",
|
||||
"@types/node": "^22.14.1",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@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.25.1",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint-plugin-vue": "^10.3.0",
|
||||
"globals": "^16.3.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.31.0",
|
||||
"unplugin-vue-components": "^28.5.0",
|
||||
"vite": "^6.3.2",
|
||||
"vue-eslint-parser": "^10.1.3",
|
||||
"vue-tsc": "^2.2.8"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
28
frontend/public/guesslang.min.js
vendored
Normal file
28
frontend/public/guesslang.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
48
frontend/public/langdetect-worker.js
Normal file
48
frontend/public/langdetect-worker.js
Normal file
@@ -0,0 +1,48 @@
|
||||
importScripts("guesslang.min.js")
|
||||
|
||||
const LANGUAGES = ["json", "py", "html", "sql", "md", "java", "php", "css", "xml", "cpp", "rs", "cs", "rb", "sh", "yaml", "toml", "go", "clj", "ex", "erl", "js", "ts", "swift", "kt", "groovy", "ps1", "dart", "scala"]
|
||||
|
||||
const guessLang = new self.GuessLang()
|
||||
|
||||
function sendResult(language, confidence, idx) {
|
||||
postMessage({language, confidence, idx})
|
||||
}
|
||||
|
||||
onmessage = (event) => {
|
||||
const {content, idx} = event.data
|
||||
|
||||
// JSON 快速检测
|
||||
const trimmed = content.trim()
|
||||
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
||||
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
||||
try {
|
||||
if (typeof JSON.parse(trimmed) === "object") {
|
||||
sendResult("json", 1.0, idx)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
guessLang.runModel(content).then((result) => {
|
||||
if (result.length > 0) {
|
||||
const lang = result[0]
|
||||
if (LANGUAGES.includes(lang.languageId) && lang.confidence > 0.15) {
|
||||
sendResult(lang.languageId, lang.confidence, idx)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for (let lang of result) {
|
||||
if (LANGUAGES.includes(lang.languageId) && lang.confidence > 0.5) {
|
||||
sendResult(lang.languageId, lang.confidence, idx)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sendResult("text", 0.0, idx)
|
||||
}).catch(() => {
|
||||
sendResult("text", 0.0, idx)
|
||||
})
|
||||
}
|
@@ -1,14 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import Editor from '@/editor/index.vue';
|
||||
import Toolbar from '@/components/toolbar/index.vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { useConfigStore } from '@/stores/configStore';
|
||||
import { useSystemStore } from '@/stores/systemStore';
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore';
|
||||
import { useThemeStore } from '@/stores/themeStore';
|
||||
import { useUpdateStore } from '@/stores/updateStore';
|
||||
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const systemStore = useSystemStore();
|
||||
const keybindingStore = useKeybindingStore();
|
||||
const themeStore = useThemeStore();
|
||||
const updateStore = useUpdateStore();
|
||||
|
||||
// 应用启动时加载配置和初始化系统信息
|
||||
onMounted(async () => {
|
||||
// 并行初始化配置、系统信息和快捷键配置
|
||||
await Promise.all([
|
||||
configStore.initConfig(),
|
||||
systemStore.initializeSystemInfo(),
|
||||
keybindingStore.loadKeyBindings(),
|
||||
]);
|
||||
|
||||
// 初始化语言和主题
|
||||
await configStore.initializeLanguage();
|
||||
themeStore.initializeTheme();
|
||||
|
||||
// 启动时检查更新
|
||||
await updateStore.checkOnStartup();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="editor-wrapper">
|
||||
<Editor />
|
||||
<WindowTitleBar />
|
||||
<div class="app-content">
|
||||
<router-view/>
|
||||
</div>
|
||||
<Toolbar />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -20,10 +48,11 @@ import Toolbar from '@/components/toolbar/index.vue';
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.editor-wrapper {
|
||||
.app-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
|
146
frontend/src/assets/styles/fonts.css
Normal file
146
frontend/src/assets/styles/fonts.css
Normal file
@@ -0,0 +1,146 @@
|
||||
/* HarmonyOS Sans 字体定义 */
|
||||
|
||||
/* HarmonyOS Sans Regular */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans Light */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans Medium */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans Semibold */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Semibold.ttf') format('truetype');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans Bold */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans Black */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Black.ttf') format('truetype');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans Thin */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_Sans/HarmonyOS_Sans_Thin.ttf') format('truetype');
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans SC 简体中文字体 */
|
||||
|
||||
/* HarmonyOS Sans SC Regular */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans SC Light */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans SC Medium */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans SC Semibold */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Semibold.ttf') format('truetype');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans SC Bold */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans SC Black */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Black.ttf') format('truetype');
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* HarmonyOS Sans SC Thin */
|
||||
@font-face {
|
||||
font-family: 'HarmonyOS Sans SC';
|
||||
src: url('../fonts/HarmonyOS Sans/HarmonyOS_SansSC/HarmonyOS_SansSC_Thin.ttf') format('truetype');
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* 字体加载优化 */
|
||||
.font-loading {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.font-loaded {
|
||||
font-family: 'HarmonyOS Sans SC', 'HarmonyOS Sans', 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* CodeMirror 专用字体类 */
|
||||
.cm-harmonyos-font {
|
||||
font-family: 'HarmonyOS Sans SC', 'HarmonyOS Sans', 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif !important;
|
||||
font-feature-settings: 'liga' 1, 'calt' 1;
|
||||
font-variant-ligatures: contextual;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
@@ -1,3 +1,5 @@
|
||||
/* 导入所有CSS文件 */
|
||||
@import 'normalize.css';
|
||||
@import 'variables.css';
|
||||
@import "fonts.css";
|
||||
@import 'scrollbar.css';
|
108
frontend/src/assets/styles/scrollbar.css
Normal file
108
frontend/src/assets/styles/scrollbar.css
Normal file
@@ -0,0 +1,108 @@
|
||||
/* 滚动条样式 - 支持主题切换 */
|
||||
|
||||
/* Webkit 浏览器滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-track);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 6px;
|
||||
border: 2px solid var(--scrollbar-track);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background: var(--scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: var(--scrollbar-track);
|
||||
}
|
||||
|
||||
/* 细滚动条变体(用于特定区域) */
|
||||
.thin-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.thin-scrollbar::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-track);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.thin-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--scrollbar-track);
|
||||
}
|
||||
|
||||
.thin-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
/* Firefox 滚动条样式 */
|
||||
* {
|
||||
scrollbar-width: auto;
|
||||
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
||||
}
|
||||
|
||||
/* 细滚动条的Firefox样式 */
|
||||
.thin-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
||||
}
|
||||
|
||||
/* 隐藏滚动条但保持功能的工具类 */
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 自定义悬浮显示滚动条 */
|
||||
.hover-scrollbar {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.hover-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hover-scrollbar:hover {
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
|
||||
.hover-scrollbar:hover::-webkit-scrollbar {
|
||||
display: block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.hover-scrollbar:hover::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-track);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.hover-scrollbar:hover::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.hover-scrollbar:hover::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scrollbar-thumb-hover);
|
||||
}
|
@@ -1,12 +1,248 @@
|
||||
:root {
|
||||
/* 主题颜色 */
|
||||
--bg-secondary: #0E1217; /* 工具栏背景 */
|
||||
|
||||
/* 文本颜色 */
|
||||
/* 编辑器区域 */
|
||||
--text-primary: #9BB586; /* 内容区域字体颜色 */
|
||||
--text-secondary: #a0aec0;
|
||||
--text-muted: #666;
|
||||
|
||||
/* 边框颜色 */
|
||||
--border-color: #2d3748;
|
||||
/* 深色主题颜色变量 */
|
||||
--dark-toolbar-bg: #2d2d2d;
|
||||
--dark-toolbar-border: #404040;
|
||||
--dark-toolbar-text: #ffffff;
|
||||
--dark-toolbar-text-secondary: #cccccc;
|
||||
--dark-toolbar-button-hover: #404040;
|
||||
--dark-bg-secondary: #0E1217;
|
||||
--dark-text-secondary: #a0aec0;
|
||||
--dark-text-muted: #666;
|
||||
--dark-border-color: #2d3748;
|
||||
--dark-settings-bg: #2a2a2a;
|
||||
--dark-settings-card-bg: #333333;
|
||||
--dark-settings-text: #ffffff;
|
||||
--dark-settings-text-secondary: #cccccc;
|
||||
--dark-settings-border: #444444;
|
||||
--dark-settings-input-bg: #3a3a3a;
|
||||
--dark-settings-input-border: #555555;
|
||||
--dark-settings-hover: #404040;
|
||||
--dark-scrollbar-track: #2a2a2a;
|
||||
--dark-scrollbar-thumb: #555555;
|
||||
--dark-scrollbar-thumb-hover: #666666;
|
||||
--dark-selection-bg: rgba(181, 206, 168, 0.1);
|
||||
--dark-selection-text: #b5cea8;
|
||||
--dark-danger-color: #ff6b6b;
|
||||
--dark-bg-primary: #1a1a1a;
|
||||
--dark-bg-hover: #2a2a2a;
|
||||
--dark-loading-bg-gradient: radial-gradient(#222922, #000500);
|
||||
--dark-loading-color: #fff;
|
||||
--dark-loading-glow: 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5);
|
||||
--dark-loading-done-color: #6f6;
|
||||
--dark-loading-overlay: linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%);
|
||||
|
||||
/* 浅色主题颜色变量 */
|
||||
--light-toolbar-bg: #f8f9fa;
|
||||
--light-toolbar-border: #e9ecef;
|
||||
--light-toolbar-text: #212529;
|
||||
--light-toolbar-text-secondary: #495057;
|
||||
--light-toolbar-button-hover: #e9ecef;
|
||||
--light-bg-secondary: #f7fef7;
|
||||
--light-text-secondary: #374151;
|
||||
--light-text-muted: #6b7280;
|
||||
--light-border-color: #e5e7eb;
|
||||
--light-settings-bg: #ffffff;
|
||||
--light-settings-card-bg: #f8f9fa;
|
||||
--light-settings-text: #212529;
|
||||
--light-settings-text-secondary: #6c757d;
|
||||
--light-settings-border: #dee2e6;
|
||||
--light-settings-input-bg: #ffffff;
|
||||
--light-settings-input-border: #ced4da;
|
||||
--light-settings-hover: #e9ecef;
|
||||
--light-scrollbar-track: #f1f3f4;
|
||||
--light-scrollbar-thumb: #c1c1c1;
|
||||
--light-scrollbar-thumb-hover: #a8a8a8;
|
||||
--light-selection-bg: rgba(59, 130, 246, 0.15);
|
||||
--light-selection-text: #2563eb;
|
||||
--light-danger-color: #dc3545;
|
||||
--light-bg-primary: #ffffff;
|
||||
--light-bg-hover: #f1f3f4;
|
||||
--light-loading-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
|
||||
--light-loading-color: #1a3c1a;
|
||||
--light-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
|
||||
--light-loading-done-color: #008800;
|
||||
--light-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
|
||||
|
||||
/* 默认使用深色主题 */
|
||||
--toolbar-bg: var(--dark-toolbar-bg);
|
||||
--toolbar-border: var(--dark-toolbar-border);
|
||||
--toolbar-text: var(--dark-toolbar-text);
|
||||
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
|
||||
--toolbar-button-hover: var(--dark-toolbar-button-hover);
|
||||
--toolbar-separator: var(--dark-toolbar-button-hover);
|
||||
--bg-secondary: var(--dark-bg-secondary);
|
||||
--text-secondary: var(--dark-text-secondary);
|
||||
--text-muted: var(--dark-text-muted);
|
||||
--border-color: var(--dark-border-color);
|
||||
--settings-bg: var(--dark-settings-bg);
|
||||
--settings-card-bg: var(--dark-settings-card-bg);
|
||||
--settings-text: var(--dark-settings-text);
|
||||
--settings-text-secondary: var(--dark-settings-text-secondary);
|
||||
--settings-border: var(--dark-settings-border);
|
||||
--settings-input-bg: var(--dark-settings-input-bg);
|
||||
--settings-input-border: var(--dark-settings-input-border);
|
||||
--settings-hover: var(--dark-settings-hover);
|
||||
--scrollbar-track: var(--dark-scrollbar-track);
|
||||
--scrollbar-thumb: var(--dark-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--dark-selection-bg);
|
||||
--selection-text: var(--dark-selection-text);
|
||||
--text-danger: var(--dark-danger-color);
|
||||
--bg-primary: var(--dark-bg-primary);
|
||||
--bg-hover: var(--dark-bg-hover);
|
||||
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
|
||||
--voidraft-loading-color: var(--dark-loading-color);
|
||||
--voidraft-loading-glow: var(--dark-loading-glow);
|
||||
--voidraft-loading-done-color: var(--dark-loading-done-color);
|
||||
--voidraft-loading-overlay: var(--dark-loading-overlay);
|
||||
--voidraft-mono-font: "HarmonyOS Sans Mono", monospace;
|
||||
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
/* 监听系统深色主题 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-theme="auto"] {
|
||||
--toolbar-bg: var(--dark-toolbar-bg);
|
||||
--toolbar-border: var(--dark-toolbar-border);
|
||||
--toolbar-text: var(--dark-toolbar-text);
|
||||
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
|
||||
--toolbar-button-hover: var(--dark-toolbar-button-hover);
|
||||
--toolbar-separator: var(--dark-toolbar-button-hover);
|
||||
--bg-secondary: var(--dark-bg-secondary);
|
||||
--text-secondary: var(--dark-text-secondary);
|
||||
--text-muted: var(--dark-text-muted);
|
||||
--border-color: var(--dark-border-color);
|
||||
--settings-bg: var(--dark-settings-bg);
|
||||
--settings-card-bg: var(--dark-settings-card-bg);
|
||||
--settings-text: var(--dark-settings-text);
|
||||
--settings-text-secondary: var(--dark-settings-text-secondary);
|
||||
--settings-border: var(--dark-settings-border);
|
||||
--settings-input-bg: var(--dark-settings-input-bg);
|
||||
--settings-input-border: var(--dark-settings-input-border);
|
||||
--settings-hover: var(--dark-settings-hover);
|
||||
--scrollbar-track: var(--dark-scrollbar-track);
|
||||
--scrollbar-thumb: var(--dark-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--dark-selection-bg);
|
||||
--selection-text: var(--dark-selection-text);
|
||||
--text-danger: var(--dark-danger-color);
|
||||
--bg-primary: var(--dark-bg-primary);
|
||||
--bg-hover: var(--dark-bg-hover);
|
||||
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
|
||||
--voidraft-loading-color: var(--dark-loading-color);
|
||||
--voidraft-loading-glow: var(--dark-loading-glow);
|
||||
--voidraft-loading-done-color: var(--dark-loading-done-color);
|
||||
--voidraft-loading-overlay: var(--dark-loading-overlay);
|
||||
}
|
||||
}
|
||||
|
||||
/* 监听系统浅色主题 */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root[data-theme="auto"] {
|
||||
--toolbar-bg: var(--light-toolbar-bg);
|
||||
--toolbar-border: var(--light-toolbar-border);
|
||||
--toolbar-text: var(--light-toolbar-text);
|
||||
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
|
||||
--toolbar-button-hover: var(--light-toolbar-button-hover);
|
||||
--toolbar-separator: var(--light-toolbar-button-hover);
|
||||
--bg-secondary: var(--light-bg-secondary);
|
||||
--text-secondary: var(--light-text-secondary);
|
||||
--text-muted: var(--light-text-muted);
|
||||
--border-color: var(--light-border-color);
|
||||
--settings-bg: var(--light-settings-bg);
|
||||
--settings-card-bg: var(--light-settings-card-bg);
|
||||
--settings-text: var(--light-settings-text);
|
||||
--settings-text-secondary: var(--light-settings-text-secondary);
|
||||
--settings-border: var(--light-settings-border);
|
||||
--settings-input-bg: var(--light-settings-input-bg);
|
||||
--settings-input-border: var(--light-settings-input-border);
|
||||
--settings-hover: var(--light-settings-hover);
|
||||
--scrollbar-track: var(--light-scrollbar-track);
|
||||
--scrollbar-thumb: var(--light-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--light-selection-bg);
|
||||
--selection-text: var(--light-selection-text);
|
||||
--text-danger: var(--light-danger-color);
|
||||
--bg-primary: var(--light-bg-primary);
|
||||
--bg-hover: var(--light-bg-hover);
|
||||
--voidraft-bg-gradient: var(--light-loading-bg-gradient);
|
||||
--voidraft-loading-color: var(--light-loading-color);
|
||||
--voidraft-loading-glow: var(--light-loading-glow);
|
||||
--voidraft-loading-done-color: var(--light-loading-done-color);
|
||||
--voidraft-loading-overlay: var(--light-loading-overlay);
|
||||
}
|
||||
}
|
||||
|
||||
/* 手动选择浅色主题 */
|
||||
:root[data-theme="light"] {
|
||||
--toolbar-bg: var(--light-toolbar-bg);
|
||||
--toolbar-border: var(--light-toolbar-border);
|
||||
--toolbar-text: var(--light-toolbar-text);
|
||||
--toolbar-text-secondary: var(--light-toolbar-text-secondary);
|
||||
--toolbar-button-hover: var(--light-toolbar-button-hover);
|
||||
--toolbar-separator: var(--light-toolbar-button-hover);
|
||||
--bg-secondary: var(--light-bg-secondary);
|
||||
--text-secondary: var(--light-text-secondary);
|
||||
--text-muted: var(--light-text-muted);
|
||||
--border-color: var(--light-border-color);
|
||||
--settings-bg: var(--light-settings-bg);
|
||||
--settings-card-bg: var(--light-settings-card-bg);
|
||||
--settings-text: var(--light-settings-text);
|
||||
--settings-text-secondary: var(--light-settings-text-secondary);
|
||||
--settings-border: var(--light-settings-border);
|
||||
--settings-input-bg: var(--light-settings-input-bg);
|
||||
--settings-input-border: var(--light-settings-input-border);
|
||||
--settings-hover: var(--light-settings-hover);
|
||||
--scrollbar-track: var(--light-scrollbar-track);
|
||||
--scrollbar-thumb: var(--light-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--light-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--light-selection-bg);
|
||||
--selection-text: var(--light-selection-text);
|
||||
--text-danger: var(--light-danger-color);
|
||||
--bg-primary: var(--light-bg-primary);
|
||||
--bg-hover: var(--light-bg-hover);
|
||||
--voidraft-bg-gradient: var(--light-loading-bg-gradient);
|
||||
--voidraft-loading-color: var(--light-loading-color);
|
||||
--voidraft-loading-glow: var(--light-loading-glow);
|
||||
--voidraft-loading-done-color: var(--light-loading-done-color);
|
||||
--voidraft-loading-overlay: var(--light-loading-overlay);
|
||||
}
|
||||
|
||||
/* 手动选择深色主题 */
|
||||
:root[data-theme="dark"] {
|
||||
--toolbar-bg: var(--dark-toolbar-bg);
|
||||
--toolbar-border: var(--dark-toolbar-border);
|
||||
--toolbar-text: var(--dark-toolbar-text);
|
||||
--toolbar-text-secondary: var(--dark-toolbar-text-secondary);
|
||||
--toolbar-button-hover: var(--dark-toolbar-button-hover);
|
||||
--toolbar-separator: var(--dark-toolbar-button-hover);
|
||||
--bg-secondary: var(--dark-bg-secondary);
|
||||
--text-secondary: var(--dark-text-secondary);
|
||||
--text-muted: var(--dark-text-muted);
|
||||
--border-color: var(--dark-border-color);
|
||||
--settings-bg: var(--dark-settings-bg);
|
||||
--settings-card-bg: var(--dark-settings-card-bg);
|
||||
--settings-text: var(--dark-settings-text);
|
||||
--settings-text-secondary: var(--dark-settings-text-secondary);
|
||||
--settings-border: var(--dark-settings-border);
|
||||
--settings-input-bg: var(--dark-settings-input-bg);
|
||||
--settings-input-border: var(--dark-settings-input-border);
|
||||
--settings-hover: var(--dark-settings-hover);
|
||||
--scrollbar-track: var(--dark-scrollbar-track);
|
||||
--scrollbar-thumb: var(--dark-scrollbar-thumb);
|
||||
--scrollbar-thumb-hover: var(--dark-scrollbar-thumb-hover);
|
||||
--selection-bg: var(--dark-selection-bg);
|
||||
--selection-text: var(--dark-selection-text);
|
||||
--text-danger: var(--dark-danger-color);
|
||||
--bg-primary: var(--dark-bg-primary);
|
||||
--bg-hover: var(--dark-bg-hover);
|
||||
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
|
||||
--voidraft-loading-color: var(--dark-loading-color);
|
||||
--voidraft-loading-glow: var(--dark-loading-glow);
|
||||
--voidraft-loading-done-color: var(--dark-loading-done-color);
|
||||
--voidraft-loading-overlay: var(--dark-loading-overlay);
|
||||
}
|
177
frontend/src/components/loading/LoadingScreen.vue
Normal file
177
frontend/src/components/loading/LoadingScreen.vue
Normal 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>
|
410
frontend/src/components/monitor/MemoryMonitor.vue
Normal file
410
frontend/src/components/monitor/MemoryMonitor.vue
Normal file
@@ -0,0 +1,410 @@
|
||||
<script setup lang="ts">
|
||||
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个数据点)
|
||||
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>,
|
||||
fallback?: T
|
||||
): Promise<T | undefined> => {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
// 静默处理错误,不输出到控制台
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取内存统计信息
|
||||
const fetchMemoryStats = async () => {
|
||||
const stats = await withSilentErrorHandling(() => SystemService.GetMemoryStats());
|
||||
|
||||
if (!stats) {
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
memoryStats.value = stats;
|
||||
|
||||
// 格式化内存显示 - 主要显示堆内存使用量
|
||||
const heapMB = (stats.heapInUse / 1024 / 1024);
|
||||
if (heapMB < 1) {
|
||||
formattedMemory.value = `${(heapMB * 1024).toFixed(0)}K`;
|
||||
} else if (heapMB < 100) {
|
||||
formattedMemory.value = `${heapMB.toFixed(1)}M`;
|
||||
} else {
|
||||
formattedMemory.value = `${heapMB.toFixed(0)}M`;
|
||||
}
|
||||
|
||||
// 自动调整最大内存值,确保图表能够显示更大范围
|
||||
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);
|
||||
|
||||
// 保持最大数据点数量
|
||||
if (historyData.value.length > maxDataPoints) {
|
||||
historyData.value.shift();
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
drawChart();
|
||||
|
||||
isLoading.value = false;
|
||||
};
|
||||
|
||||
// 绘制实时曲线图 - 简化版
|
||||
const drawChart = () => {
|
||||
if (!canvasRef.value || historyData.value.length === 0) return;
|
||||
|
||||
const canvas = canvasRef.value;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// 设置canvas尺寸
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = rect.width * window.devicePixelRatio;
|
||||
canvas.height = rect.height * window.devicePixelRatio;
|
||||
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
|
||||
const width = rect.width;
|
||||
const height = rect.height;
|
||||
|
||||
// 清除画布
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
// 根据主题选择合适的颜色 - 更柔和的颜色
|
||||
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);
|
||||
ctx.lineTo(width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// 垂直网格线
|
||||
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);
|
||||
ctx.lineTo(x, height);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
if (historyData.value.length < 2) return;
|
||||
|
||||
// 计算数据点位置
|
||||
const dataLength = historyData.value.length;
|
||||
const stepX = width / (maxDataPoints - 1);
|
||||
const startX = width - (dataLength - 1) * stepX;
|
||||
|
||||
// 绘制填充区域 - 更柔和的填充
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX, height);
|
||||
|
||||
// 移动到第一个数据点
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 完成填充路径
|
||||
const lastX = startX + (dataLength - 1) * stepX;
|
||||
ctx.lineTo(lastX, height);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = fillColor;
|
||||
ctx.fill();
|
||||
|
||||
// 绘制主曲线 - 平滑连续的曲线
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(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 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);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.strokeStyle = lineColor;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.stroke();
|
||||
|
||||
// 绘制当前值的高亮点
|
||||
const lastY = height - (historyData.value[dataLength - 1] / 100) * height;
|
||||
|
||||
// 外圈
|
||||
ctx.fillStyle = pointColor;
|
||||
ctx.globalAlpha = 0.4;
|
||||
ctx.beginPath();
|
||||
ctx.arc(lastX, lastY, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// 内圈
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.beginPath();
|
||||
ctx.arc(lastX, lastY, 1.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
// 手动触发GC
|
||||
const triggerGC = async () => {
|
||||
const success = await withSilentErrorHandling(() => SystemService.TriggerGC());
|
||||
|
||||
if (success) {
|
||||
// 延迟一下再获取新的统计信息
|
||||
setTimeout(fetchMemoryStats, 100);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (historyData.value.length > 0) {
|
||||
nextTick(() => drawChart());
|
||||
}
|
||||
};
|
||||
|
||||
// 仅监听系统主题变化
|
||||
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();
|
||||
// 每1秒更新一次内存信息
|
||||
intervalId = setInterval(fetchMemoryStats, 3000);
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('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="`${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>{{ t('monitor.memory') }}</span>
|
||||
</div>
|
||||
<div class="memory-value" v-if="!isLoading">{{ formattedMemory }}</div>
|
||||
<div class="memory-loading" v-else>--</div>
|
||||
</div>
|
||||
<div class="chart-area">
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
class="memory-chart"
|
||||
:class="{ 'loading': isLoading }"
|
||||
></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.memory-monitor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
.monitor-info {
|
||||
.memory-label {
|
||||
color: var(--selection-text);
|
||||
}
|
||||
|
||||
.memory-value {
|
||||
color: var(--toolbar-text);
|
||||
}
|
||||
}
|
||||
|
||||
.chart-area .memory-chart {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.monitor-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.memory-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
span {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.memory-value, .memory-loading {
|
||||
color: var(--toolbar-text-secondary);
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.memory-loading {
|
||||
opacity: 0.5;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-area {
|
||||
height: 48px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 3px;
|
||||
|
||||
.memory-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
297
frontend/src/components/titlebar/LinuxTitleBar.vue
Normal file
297
frontend/src/components/titlebar/LinuxTitleBar.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<div class="linux-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
|
||||
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
|
||||
<div class="titlebar-icon">
|
||||
<img src="/appicon.png" alt="voidraft" />
|
||||
</div>
|
||||
<div class="titlebar-title">{{ titleText }}</div>
|
||||
</div>
|
||||
|
||||
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
|
||||
<button
|
||||
class="titlebar-button minimize-button"
|
||||
@click="minimizeWindow"
|
||||
:title="t('titlebar.minimize')"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M4 8h8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="titlebar-button maximize-button"
|
||||
@click="toggleMaximize"
|
||||
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" v-if="!isMaximized">
|
||||
<rect x="4" y="4" width="8" height="8" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" v-else>
|
||||
<rect x="3" y="5" width="6" height="6" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
<rect x="7" y="3" width="6" height="6" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="titlebar-button close-button"
|
||||
@click="closeWindow"
|
||||
:title="t('titlebar.close')"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M4 4l8 8m0-8L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import * as runtime from '@wailsio/runtime';
|
||||
import { useWindowStore } from '@/stores/windowStore';
|
||||
import { useDocumentStore } from '@/stores/documentStore';
|
||||
|
||||
const { t } = useI18n();
|
||||
const isMaximized = ref(false);
|
||||
const documentStore = useDocumentStore();
|
||||
|
||||
const minimizeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Minimise();
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMaximize = async () => {
|
||||
try {
|
||||
const newState = !isMaximized.value;
|
||||
isMaximized.value = newState;
|
||||
|
||||
if (newState) {
|
||||
await runtime.Window.Maximise();
|
||||
} else {
|
||||
await runtime.Window.UnMaximise();
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
await checkMaximizedState();
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
isMaximized.value = !isMaximized.value;
|
||||
}
|
||||
};
|
||||
|
||||
const closeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Close();
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
}
|
||||
};
|
||||
|
||||
const checkMaximizedState = async () => {
|
||||
try {
|
||||
isMaximized.value = await runtime.Window.IsMaximised();
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
}
|
||||
};
|
||||
|
||||
// 计算标题文本
|
||||
const titleText = computed(() => {
|
||||
const currentDoc = documentStore.currentDocument;
|
||||
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await checkMaximizedState();
|
||||
|
||||
runtime.Events.On('window:maximised', () => {
|
||||
isMaximized.value = true;
|
||||
});
|
||||
|
||||
runtime.Events.On('window:unmaximised', () => {
|
||||
isMaximized.value = false;
|
||||
});
|
||||
|
||||
runtime.Events.On('window:focus', async () => {
|
||||
await checkMaximizedState();
|
||||
});
|
||||
|
||||
});
|
||||
onUnmounted(() => {
|
||||
runtime.Events.Off('window:maximised');
|
||||
runtime.Events.Off('window:unmaximised');
|
||||
runtime.Events.Off('window:focus');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.linux-titlebar {
|
||||
display: flex;
|
||||
height: 34px;
|
||||
background: var(--toolbar-bg, linear-gradient(to bottom, #f6f6f6, #e8e8e8));
|
||||
border-bottom: 1px solid var(--toolbar-border, #d0d0d0);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
width: 100%;
|
||||
font-family: 'Ubuntu', 'Cantarell', 'DejaVu Sans', system-ui, sans-serif;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
-webkit-context-menu: none;
|
||||
-moz-context-menu: none;
|
||||
context-menu: none;
|
||||
}
|
||||
|
||||
.titlebar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
padding-left: 12px;
|
||||
gap: 8px;
|
||||
color: var(--toolbar-text, #333);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: default;
|
||||
|
||||
-webkit-context-menu: none;
|
||||
-moz-context-menu: none;
|
||||
context-menu: none;
|
||||
}
|
||||
|
||||
.titlebar-content .titlebar-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.titlebar-title {
|
||||
font-size: 13px;
|
||||
color: var(--toolbar-text, #333);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.titlebar-controls {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
-webkit-context-menu: none;
|
||||
-moz-context-menu: none;
|
||||
context-menu: none;
|
||||
}
|
||||
|
||||
.titlebar-button {
|
||||
width: 36px;
|
||||
height: 34px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--toolbar-text, #555);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s ease;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--toolbar-button-hover, rgba(0, 0, 0, 0.1));
|
||||
|
||||
svg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--toolbar-button-active, rgba(0, 0, 0, 0.15));
|
||||
}
|
||||
}
|
||||
|
||||
.close-button {
|
||||
&:hover {
|
||||
background: #e74c3c;
|
||||
color: #ffffff;
|
||||
|
||||
svg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #c0392b;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark theme support
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.linux-titlebar {
|
||||
background: var(--toolbar-bg, linear-gradient(to bottom, #3c3c3c, #2e2e2e));
|
||||
border-bottom-color: var(--toolbar-border, #1e1e1e);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.titlebar-content,
|
||||
.titlebar-title {
|
||||
color: var(--toolbar-text, #f0f0f0);
|
||||
}
|
||||
|
||||
.titlebar-button {
|
||||
color: var(--toolbar-text, #ccc);
|
||||
|
||||
&:hover {
|
||||
background: var(--toolbar-button-hover, rgba(255, 255, 255, 0.1));
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--toolbar-button-active, rgba(255, 255, 255, 0.15));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GNOME-like styling variant
|
||||
.linux-titlebar.gnome-style {
|
||||
height: 38px;
|
||||
border-radius: 12px 12px 0 0;
|
||||
|
||||
.titlebar-button {
|
||||
height: 38px;
|
||||
width: 32px;
|
||||
border-radius: 6px;
|
||||
margin: 3px 2px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// KDE-like styling variant
|
||||
.linux-titlebar.kde-style {
|
||||
background: var(--toolbar-bg, #eff0f1);
|
||||
border-bottom: 1px solid var(--toolbar-border, #bdc3c7);
|
||||
|
||||
.titlebar-button {
|
||||
border-radius: 4px;
|
||||
margin: 2px 1px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(61, 174, 233, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
271
frontend/src/components/titlebar/MacOSTitleBar.vue
Normal file
271
frontend/src/components/titlebar/MacOSTitleBar.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<div class="macos-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
|
||||
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
|
||||
<button
|
||||
class="titlebar-button close-button"
|
||||
@click="closeWindow"
|
||||
:title="t('titlebar.close')"
|
||||
>
|
||||
<div class="button-icon">
|
||||
<svg width="6" height="6" viewBox="0 0 6 6" v-show="showControlIcons">
|
||||
<path d="M1 1l4 4m0-4L1 5" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="titlebar-button minimize-button"
|
||||
@click="minimizeWindow"
|
||||
:title="t('titlebar.minimize')"
|
||||
>
|
||||
<div class="button-icon">
|
||||
<svg width="8" height="1" viewBox="0 0 8 1" v-show="showControlIcons">
|
||||
<path d="M0 0h8" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="titlebar-button maximize-button"
|
||||
@click="toggleMaximize"
|
||||
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
|
||||
>
|
||||
<div class="button-icon">
|
||||
<svg width="6" height="6" viewBox="0 0 6 6" v-show="showControlIcons && !isMaximized">
|
||||
<path d="M1 1l4 0 0 4-4 0z" fill="none" stroke="currentColor" stroke-width="1"/>
|
||||
<path d="M2 2l2 0 0 2" fill="none" stroke="currentColor" stroke-width="1"/>
|
||||
</svg>
|
||||
<svg width="6" height="6" viewBox="0 0 6 6" v-show="showControlIcons && isMaximized">
|
||||
<path d="M1 2l4 0 0 3-4 0z" fill="none" stroke="currentColor" stroke-width="1"/>
|
||||
<path d="M2 1l3 0 0 3" fill="none" stroke="currentColor" stroke-width="1"/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
|
||||
<div class="titlebar-title">{{ titleText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import * as runtime from '@wailsio/runtime';
|
||||
import { useWindowStore } from '@/stores/windowStore';
|
||||
import { useDocumentStore } from '@/stores/documentStore';
|
||||
|
||||
const { t } = useI18n();
|
||||
const isMaximized = ref(false);
|
||||
const showControlIcons = ref(false);
|
||||
const documentStore = useDocumentStore();
|
||||
|
||||
const minimizeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Minimise();
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMaximize = async () => {
|
||||
try {
|
||||
const newState = !isMaximized.value;
|
||||
isMaximized.value = newState;
|
||||
|
||||
if (newState) {
|
||||
await runtime.Window.Maximise();
|
||||
} else {
|
||||
await runtime.Window.UnMaximise();
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
await checkMaximizedState();
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
isMaximized.value = !isMaximized.value;
|
||||
}
|
||||
};
|
||||
|
||||
const closeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Close();
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
}
|
||||
};
|
||||
|
||||
const checkMaximizedState = async () => {
|
||||
try {
|
||||
isMaximized.value = await runtime.Window.IsMaximised();
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
}
|
||||
};
|
||||
|
||||
// 计算标题文本
|
||||
const titleText = computed(() => {
|
||||
const currentDoc = documentStore.currentDocument;
|
||||
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await checkMaximizedState();
|
||||
|
||||
runtime.Events.On('window:maximised', () => {
|
||||
isMaximized.value = true;
|
||||
});
|
||||
|
||||
runtime.Events.On('window:unmaximised', () => {
|
||||
isMaximized.value = false;
|
||||
});
|
||||
|
||||
runtime.Events.On('window:focus', async () => {
|
||||
await checkMaximizedState();
|
||||
});
|
||||
|
||||
});
|
||||
onUnmounted(() => {
|
||||
runtime.Events.Off('window:maximised');
|
||||
runtime.Events.Off('window:unmaximised');
|
||||
runtime.Events.Off('window:focus');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.macos-titlebar {
|
||||
display: flex;
|
||||
height: 28px;
|
||||
background: var(--toolbar-bg, #ececec);
|
||||
border-bottom: 1px solid var(--toolbar-border, rgba(0, 0, 0, 0.1));
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
width: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', system-ui, sans-serif;
|
||||
|
||||
-webkit-context-menu: none;
|
||||
-moz-context-menu: none;
|
||||
context-menu: none;
|
||||
|
||||
&:hover {
|
||||
.titlebar-button {
|
||||
.button-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.titlebar-controls {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
padding-left: 8px;
|
||||
gap: 8px;
|
||||
|
||||
-webkit-context-menu: none;
|
||||
-moz-context-menu: none;
|
||||
context-menu: none;
|
||||
}
|
||||
|
||||
.titlebar-button {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
|
||||
.button-icon {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
&:hover .button-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: #ff5f57;
|
||||
|
||||
&:hover {
|
||||
background: #ff453a;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #d7463f;
|
||||
}
|
||||
}
|
||||
|
||||
.minimize-button {
|
||||
background: #ffbd2e;
|
||||
|
||||
&:hover {
|
||||
background: #ffb524;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #e6a220;
|
||||
}
|
||||
}
|
||||
|
||||
.maximize-button {
|
||||
background: #28ca42;
|
||||
|
||||
&:hover {
|
||||
background: #1ebe36;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #1ba932;
|
||||
}
|
||||
}
|
||||
|
||||
.titlebar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
cursor: default;
|
||||
|
||||
-webkit-context-menu: none;
|
||||
-moz-context-menu: none;
|
||||
context-menu: none;
|
||||
}
|
||||
|
||||
.titlebar-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--toolbar-text, #333);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.macos-titlebar {
|
||||
background: var(--toolbar-bg, #2d2d2d);
|
||||
border-bottom-color: var(--toolbar-border, rgba(255, 255, 255, 0.1));
|
||||
}
|
||||
|
||||
.titlebar-title {
|
||||
color: var(--toolbar-text, #fff);
|
||||
}
|
||||
|
||||
.titlebar-button .button-icon {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
</style>
|
17
frontend/src/components/titlebar/WindowTitleBar.vue
Normal file
17
frontend/src/components/titlebar/WindowTitleBar.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<WindowsTitleBar v-if="systemStore.isWindows" />
|
||||
<MacOSTitleBar v-else-if="systemStore.isMacOS" />
|
||||
<LinuxTitleBar v-else />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSystemStore } from '@/stores/systemStore';
|
||||
import WindowsTitleBar from './WindowsTitleBar.vue';
|
||||
import MacOSTitleBar from './MacOSTitleBar.vue';
|
||||
import LinuxTitleBar from './LinuxTitleBar.vue';
|
||||
|
||||
const systemStore = useSystemStore();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
250
frontend/src/components/titlebar/WindowsTitleBar.vue
Normal file
250
frontend/src/components/titlebar/WindowsTitleBar.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<div class="windows-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
|
||||
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
|
||||
<div class="titlebar-icon">
|
||||
<img src="/appicon.png" alt="voidraft"/>
|
||||
</div>
|
||||
<div class="titlebar-title">{{ titleText }}</div>
|
||||
</div>
|
||||
|
||||
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
|
||||
<button
|
||||
class="titlebar-button minimize-button"
|
||||
@click="minimizeWindow"
|
||||
:title="t('titlebar.minimize')"
|
||||
>
|
||||
<span class="titlebar-icon"></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="titlebar-button maximize-button"
|
||||
@click="toggleMaximize"
|
||||
:title="isMaximized ? t('titlebar.restore') : t('titlebar.maximize')"
|
||||
>
|
||||
<span class="titlebar-icon" v-html="maximizeIcon"></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="titlebar-button close-button"
|
||||
@click="closeWindow"
|
||||
:title="t('titlebar.close')"
|
||||
>
|
||||
<span class="titlebar-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, onUnmounted, ref} from 'vue';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import * as runtime from '@wailsio/runtime';
|
||||
import { useWindowStore } from '@/stores/windowStore';
|
||||
import { useDocumentStore } from '@/stores/documentStore';
|
||||
|
||||
const {t} = useI18n();
|
||||
const isMaximized = ref(false);
|
||||
const documentStore = useDocumentStore();
|
||||
|
||||
// 计算属性用于图标,减少重复渲染
|
||||
const maximizeIcon = computed(() => isMaximized.value ? '' : '');
|
||||
|
||||
// 计算标题文本
|
||||
const titleText = computed(() => {
|
||||
const currentDoc = documentStore.currentDocument;
|
||||
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
|
||||
});
|
||||
|
||||
const minimizeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Minimise();
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMaximize = async () => {
|
||||
try {
|
||||
// 立即更新UI状态,提供即时反馈
|
||||
const newState = !isMaximized.value;
|
||||
isMaximized.value = newState;
|
||||
|
||||
// 然后执行实际操作
|
||||
if (newState) {
|
||||
await runtime.Window.Maximise();
|
||||
} else {
|
||||
await runtime.Window.UnMaximise();
|
||||
}
|
||||
|
||||
// 操作完成后再次确认状态(防止操作失败时状态不一致)
|
||||
setTimeout(async () => {
|
||||
await checkMaximizedState();
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
// 如果操作失败,恢复原状态
|
||||
isMaximized.value = !isMaximized.value;
|
||||
}
|
||||
};
|
||||
|
||||
const closeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Close();
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
}
|
||||
};
|
||||
|
||||
const checkMaximizedState = async () => {
|
||||
try {
|
||||
isMaximized.value = await runtime.Window.IsMaximised();
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await checkMaximizedState();
|
||||
|
||||
runtime.Events.On('window:maximised', () => {
|
||||
isMaximized.value = true;
|
||||
});
|
||||
|
||||
runtime.Events.On('window:unmaximised', () => {
|
||||
isMaximized.value = false;
|
||||
});
|
||||
|
||||
runtime.Events.On('window:focus', async () => {
|
||||
await checkMaximizedState();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
runtime.Events.Off('window:maximised');
|
||||
runtime.Events.Off('window:unmaximised');
|
||||
runtime.Events.Off('window:focus');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.windows-titlebar {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
background: var(--toolbar-bg);
|
||||
border-bottom: 1px solid var(--toolbar-border);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
width: 100%;
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
|
||||
-webkit-context-menu: none;
|
||||
-moz-context-menu: none;
|
||||
context-menu: none;
|
||||
}
|
||||
|
||||
.titlebar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
padding-left: 8px;
|
||||
gap: 8px;
|
||||
color: var(--toolbar-text);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
cursor: default;
|
||||
|
||||
-webkit-context-menu: none;
|
||||
-moz-context-menu: none;
|
||||
context-menu: none;
|
||||
}
|
||||
|
||||
.titlebar-content .titlebar-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.titlebar-title {
|
||||
font-size: 12px;
|
||||
color: var(--toolbar-text);
|
||||
}
|
||||
|
||||
.titlebar-controls {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
-webkit-context-menu: none;
|
||||
-moz-context-menu: none;
|
||||
context-menu: none;
|
||||
}
|
||||
|
||||
.titlebar-button {
|
||||
width: 46px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--toolbar-text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.1s ease;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
background: var(--toolbar-button-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--toolbar-button-hover);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.titlebar-button .titlebar-icon {
|
||||
font-family: 'Segoe MDL2 Assets', 'Segoe UI Symbol', 'Segoe UI', system-ui;
|
||||
font-size: 9px;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.1s ease;
|
||||
|
||||
.titlebar-button:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.minimize-button:hover,
|
||||
.maximize-button:hover {
|
||||
background: var(--toolbar-button-hover);
|
||||
}
|
||||
|
||||
.minimize-button:active,
|
||||
.maximize-button:active {
|
||||
background: var(--toolbar-button-hover);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: #c42b1c;
|
||||
color: #ffffff;
|
||||
|
||||
.titlebar-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.close-button:active {
|
||||
background: #a93226;
|
||||
|
||||
.titlebar-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
555
frontend/src/components/toolbar/BlockLanguageSelector.vue
Normal file
555
frontend/src/components/toolbar/BlockLanguageSelector.vue
Normal file
@@ -0,0 +1,555 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useEditorStore } from '@/stores/editorStore';
|
||||
import { SUPPORTED_LANGUAGES, type SupportedLanguage } from '@/views/editor/extensions/codeblock/types';
|
||||
import { getActiveNoteBlock } from '@/views/editor/extensions/codeblock/state';
|
||||
import { changeCurrentBlockLanguage } from '@/views/editor/extensions/codeblock/commands';
|
||||
|
||||
const { t } = useI18n();
|
||||
const editorStore = useEditorStore();
|
||||
|
||||
// 组件状态
|
||||
const showLanguageMenu = ref(false);
|
||||
const searchQuery = ref('');
|
||||
const searchInputRef = ref<HTMLInputElement>();
|
||||
|
||||
// 语言别名映射
|
||||
const LANGUAGE_ALIASES: Record<SupportedLanguage, string> = {
|
||||
auto: 'auto',
|
||||
text: 'txt',
|
||||
json: 'JSON',
|
||||
py: 'python',
|
||||
html: 'HTML',
|
||||
sql: 'SQL',
|
||||
md: 'markdown',
|
||||
java: 'Java',
|
||||
php: 'PHP',
|
||||
css: 'CSS',
|
||||
xml: 'XML',
|
||||
cpp: 'c++',
|
||||
rs: 'rust',
|
||||
cs: 'c#',
|
||||
rb: 'ruby',
|
||||
sh: 'shell',
|
||||
yaml: 'yml',
|
||||
toml: 'TOML',
|
||||
go: 'Go',
|
||||
clj: 'clojure',
|
||||
ex: 'elixir',
|
||||
erl: 'erlang',
|
||||
js: 'javascript',
|
||||
ts: 'typescript',
|
||||
swift: 'Swift',
|
||||
kt: 'kotlin',
|
||||
groovy: 'Groovy',
|
||||
ps1: 'powershell',
|
||||
dart: 'Dart',
|
||||
scala: 'Scala'
|
||||
};
|
||||
|
||||
// 语言显示名称映射
|
||||
const LANGUAGE_NAMES: Record<SupportedLanguage, string> = {
|
||||
auto: 'Auto',
|
||||
text: 'Plain Text',
|
||||
json: 'JSON',
|
||||
py: 'Python',
|
||||
html: 'HTML',
|
||||
sql: 'SQL',
|
||||
md: 'Markdown',
|
||||
java: 'Java',
|
||||
php: 'PHP',
|
||||
css: 'CSS',
|
||||
xml: 'XML',
|
||||
cpp: 'C++',
|
||||
rs: 'Rust',
|
||||
cs: 'C#',
|
||||
rb: 'Ruby',
|
||||
sh: 'Shell',
|
||||
yaml: 'YAML',
|
||||
toml: 'TOML',
|
||||
go: 'Go',
|
||||
clj: 'Clojure',
|
||||
ex: 'Elixir',
|
||||
erl: 'Erlang',
|
||||
js: 'JavaScript',
|
||||
ts: 'TypeScript',
|
||||
swift: 'Swift',
|
||||
kt: 'Kotlin',
|
||||
groovy: 'Groovy',
|
||||
ps1: 'PowerShell',
|
||||
dart: 'Dart',
|
||||
scala: 'Scala'
|
||||
};
|
||||
|
||||
// 当前活动块的语言信息
|
||||
const currentBlockLanguage = ref<{ name: SupportedLanguage; auto: boolean }>({
|
||||
name: 'text',
|
||||
auto: false
|
||||
});
|
||||
|
||||
// 事件监听器引用
|
||||
const eventListeners = ref<{
|
||||
updateListener?: () => void;
|
||||
selectionUpdateListener?: () => void;
|
||||
}>({});
|
||||
|
||||
// 更新当前块语言信息
|
||||
const updateCurrentBlockLanguage = () => {
|
||||
if (!editorStore.editorView) {
|
||||
currentBlockLanguage.value = { name: 'text', auto: false };
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const state = editorStore.editorView.state;
|
||||
const activeBlock = getActiveNoteBlock(state as any);
|
||||
if (activeBlock) {
|
||||
const newLanguage = {
|
||||
name: activeBlock.language.name as SupportedLanguage,
|
||||
auto: activeBlock.language.auto
|
||||
};
|
||||
|
||||
// 只有当语言信息实际发生变化时才更新
|
||||
if (currentBlockLanguage.value.name !== newLanguage.name ||
|
||||
currentBlockLanguage.value.auto !== newLanguage.auto) {
|
||||
currentBlockLanguage.value = newLanguage;
|
||||
}
|
||||
} else {
|
||||
if (currentBlockLanguage.value.name !== 'text' || currentBlockLanguage.value.auto !== false) {
|
||||
currentBlockLanguage.value = { name: 'text', auto: false };
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to get active block language:', error);
|
||||
currentBlockLanguage.value = { name: 'text', auto: false };
|
||||
}
|
||||
};
|
||||
|
||||
// 清理事件监听器
|
||||
const cleanupEventListeners = () => {
|
||||
if (editorStore.editorView?.dom && eventListeners.value.updateListener) {
|
||||
const dom = editorStore.editorView.dom;
|
||||
dom.removeEventListener('click', eventListeners.value.updateListener);
|
||||
dom.removeEventListener('keyup', eventListeners.value.updateListener);
|
||||
dom.removeEventListener('keydown', eventListeners.value.updateListener);
|
||||
dom.removeEventListener('focus', eventListeners.value.updateListener);
|
||||
dom.removeEventListener('mouseup', eventListeners.value.updateListener);
|
||||
|
||||
if (eventListeners.value.selectionUpdateListener) {
|
||||
dom.removeEventListener('selectionchange', eventListeners.value.selectionUpdateListener);
|
||||
}
|
||||
}
|
||||
eventListeners.value = {};
|
||||
};
|
||||
|
||||
// 设置事件监听器
|
||||
const setupEventListeners = (view: any) => {
|
||||
cleanupEventListeners();
|
||||
|
||||
// 监听编辑器状态更新
|
||||
const updateListener = () => {
|
||||
// 使用 requestAnimationFrame 确保在下一帧更新,性能更好
|
||||
requestAnimationFrame(() => {
|
||||
updateCurrentBlockLanguage();
|
||||
});
|
||||
};
|
||||
|
||||
// 监听选择变化
|
||||
const selectionUpdateListener = () => {
|
||||
requestAnimationFrame(() => {
|
||||
updateCurrentBlockLanguage();
|
||||
});
|
||||
};
|
||||
|
||||
// 保存监听器引用
|
||||
eventListeners.value = { updateListener, selectionUpdateListener };
|
||||
|
||||
// 监听关键事件:光标位置变化、文档变化、焦点变化
|
||||
view.dom.addEventListener('click', updateListener);
|
||||
view.dom.addEventListener('keyup', updateListener);
|
||||
view.dom.addEventListener('keydown', updateListener);
|
||||
view.dom.addEventListener('focus', updateListener);
|
||||
view.dom.addEventListener('mouseup', updateListener); // 鼠标选择结束
|
||||
|
||||
// 监听编辑器的选择变化事件
|
||||
if (view.dom.addEventListener) {
|
||||
view.dom.addEventListener('selectionchange', selectionUpdateListener);
|
||||
}
|
||||
|
||||
// 立即更新一次当前状态
|
||||
updateCurrentBlockLanguage();
|
||||
};
|
||||
|
||||
// 监听编辑器状态变化
|
||||
watch(
|
||||
() => editorStore.editorView,
|
||||
(newView) => {
|
||||
if (newView) {
|
||||
setupEventListeners(newView);
|
||||
} else {
|
||||
cleanupEventListeners();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 过滤后的语言列表
|
||||
const filteredLanguages = computed(() => {
|
||||
if (!searchQuery.value) {
|
||||
return SUPPORTED_LANGUAGES;
|
||||
}
|
||||
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return SUPPORTED_LANGUAGES.filter(langId => {
|
||||
const name = LANGUAGE_NAMES[langId];
|
||||
const alias = LANGUAGE_ALIASES[langId];
|
||||
return langId.toLowerCase().includes(query) ||
|
||||
(name && name.toLowerCase().includes(query)) ||
|
||||
(alias && alias.toLowerCase().includes(query));
|
||||
});
|
||||
});
|
||||
|
||||
// 切换语言选择器显示状态
|
||||
const toggleLanguageMenu = () => {
|
||||
showLanguageMenu.value = !showLanguageMenu.value;
|
||||
|
||||
// 如果菜单打开,滚动到当前语言
|
||||
if (showLanguageMenu.value) {
|
||||
scrollToCurrentLanguage();
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭语言选择器
|
||||
const closeLanguageMenu = () => {
|
||||
showLanguageMenu.value = false;
|
||||
searchQuery.value = '';
|
||||
};
|
||||
|
||||
// 选择语言
|
||||
const selectLanguage = (languageId: SupportedLanguage) => {
|
||||
if (!editorStore.editorView) {
|
||||
closeLanguageMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const view = editorStore.editorView;
|
||||
const state = view.state;
|
||||
const dispatch = view.dispatch;
|
||||
|
||||
let targetLanguage: string;
|
||||
let autoDetect: boolean;
|
||||
|
||||
if (languageId === 'auto') {
|
||||
// 设置为自动检测
|
||||
targetLanguage = 'text';
|
||||
autoDetect = true;
|
||||
} else {
|
||||
// 设置为指定语言,关闭自动检测
|
||||
targetLanguage = languageId;
|
||||
autoDetect = false;
|
||||
}
|
||||
|
||||
// 使用修复后的函数来更改语言
|
||||
const success = changeCurrentBlockLanguage(state as any, dispatch, targetLanguage, autoDetect);
|
||||
|
||||
if (success) {
|
||||
// 立即更新当前语言状态
|
||||
updateCurrentBlockLanguage();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Failed to change block language:', error);
|
||||
}
|
||||
|
||||
closeLanguageMenu();
|
||||
};
|
||||
|
||||
// 点击外部关闭
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('.block-language-selector')) {
|
||||
closeLanguageMenu();
|
||||
}
|
||||
};
|
||||
|
||||
// 键盘事件处理
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeLanguageMenu();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
|
||||
// 立即更新一次当前语言状态
|
||||
updateCurrentBlockLanguage();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
cleanupEventListeners();
|
||||
});
|
||||
|
||||
// 获取当前语言的显示名称
|
||||
const getCurrentLanguageName = computed(() => {
|
||||
const lang = currentBlockLanguage.value;
|
||||
if (lang.auto) {
|
||||
return `${lang.name} (auto)`;
|
||||
}
|
||||
return lang.name;
|
||||
});
|
||||
|
||||
// 获取当前显示的语言选项
|
||||
const getCurrentDisplayLanguage = computed(() => {
|
||||
const lang = currentBlockLanguage.value;
|
||||
if (lang.auto) {
|
||||
return 'auto';
|
||||
}
|
||||
return lang.name;
|
||||
});
|
||||
|
||||
// 滚动到当前选择的语言
|
||||
const scrollToCurrentLanguage = () => {
|
||||
nextTick(() => {
|
||||
const currentLang = getCurrentDisplayLanguage.value;
|
||||
const selectorElement = document.querySelector('.block-language-selector');
|
||||
|
||||
if (!selectorElement) return;
|
||||
|
||||
const languageList = selectorElement.querySelector('.language-list') as HTMLElement;
|
||||
const activeOption = selectorElement.querySelector(`.language-option[data-language="${currentLang}"]`) as HTMLElement;
|
||||
|
||||
if (languageList && activeOption) {
|
||||
// 使用 scrollIntoView 进行平滑滚动
|
||||
activeOption.scrollIntoView({
|
||||
behavior: 'auto',
|
||||
block: 'nearest',
|
||||
inline: 'nearest'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="block-language-selector">
|
||||
<button
|
||||
class="language-btn"
|
||||
:title="t('toolbar.blockLanguage')"
|
||||
@click="toggleLanguageMenu"
|
||||
>
|
||||
<span class="language-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="16 18 22 12 16 6"></polyline>
|
||||
<polyline points="8 6 2 12 8 18"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="language-name">{{ getCurrentLanguageName }}</span>
|
||||
<span class="arrow" :class="{ 'open': showLanguageMenu }">▲</span>
|
||||
</button>
|
||||
|
||||
<div class="language-menu" v-if="showLanguageMenu">
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-container">
|
||||
<input
|
||||
ref="searchInputRef"
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="search-input"
|
||||
:placeholder="t('toolbar.searchLanguage')"
|
||||
@keydown.stop
|
||||
/>
|
||||
<svg class="search-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.35-4.35"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- 语言列表 -->
|
||||
<div class="language-list">
|
||||
<div
|
||||
v-for="language in filteredLanguages"
|
||||
:key="language"
|
||||
class="language-option"
|
||||
:class="{ 'active': getCurrentDisplayLanguage === language }"
|
||||
:data-language="language"
|
||||
@click="selectLanguage(language)"
|
||||
>
|
||||
<span class="language-name">{{ LANGUAGE_NAMES[language] || language }}</span>
|
||||
<span class="language-alias">{{ language }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 无结果提示 -->
|
||||
<div v-if="filteredLanguages.length === 0" class="no-results">
|
||||
{{ t('toolbar.noLanguageFound') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.block-language-selector {
|
||||
position: relative;
|
||||
|
||||
.language-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--border-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.language-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.language-name {
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 8px;
|
||||
margin-left: 2px;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.language-menu {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
margin-bottom: 4px;
|
||||
width: 220px;
|
||||
max-height: 280px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
|
||||
.search-container {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 2px;
|
||||
padding: 5px 8px 5px 26px;
|
||||
font-size: 11px;
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
line-height: 1.2;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.language-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
.language-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--border-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--selection-bg);
|
||||
color: var(--selection-text);
|
||||
|
||||
.language-alias {
|
||||
color: var(--selection-text);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.language-name {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.language-alias {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 12px 8px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
.language-list::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.language-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.language-list::-webkit-scrollbar-thumb {
|
||||
background-color: var(--border-color);
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
</style>
|
753
frontend/src/components/toolbar/DocumentSelector.vue
Normal file
753
frontend/src/components/toolbar/DocumentSelector.vue
Normal 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>
|
497
frontend/src/components/toolbar/Toolbar.vue
Normal file
497
frontend/src/components/toolbar/Toolbar.vue
Normal file
@@ -0,0 +1,497 @@
|
||||
<script setup lang="ts">
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {onMounted, onUnmounted, ref, watch, computed} from 'vue';
|
||||
import {useConfigStore} from '@/stores/configStore';
|
||||
import {useEditorStore} from '@/stores/editorStore';
|
||||
import {useUpdateStore} from '@/stores/updateStore';
|
||||
import {useWindowStore} from '@/stores/windowStore';
|
||||
import * as runtime from '@wailsio/runtime';
|
||||
import {useRouter} from 'vue-router';
|
||||
import 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 () => {
|
||||
isCurrentWindowOnTop.value = !isCurrentWindowOnTop.value;
|
||||
await runtime.Window.SetAlwaysOnTop(isCurrentWindowOnTop.value);
|
||||
};
|
||||
|
||||
// 跳转到设置页面
|
||||
const goToSettings = () => {
|
||||
router.push('/settings');
|
||||
};
|
||||
|
||||
// 执行格式化
|
||||
const formatCurrentBlock = () => {
|
||||
if (!canFormatCurrentBlock.value || !editorStore.editorView) return;
|
||||
formatBlockContent(editorStore.editorView);
|
||||
};
|
||||
|
||||
// 格式化按钮状态更新
|
||||
const updateFormatButtonState = () => {
|
||||
// 安全检查
|
||||
const view = editorStore.editorView;
|
||||
if (!view) {
|
||||
canFormatCurrentBlock.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取活动块和语言信息
|
||||
const state = view.state;
|
||||
const activeBlock = getActiveNoteBlock(state as any);
|
||||
|
||||
// 检查块和语言格式化支持
|
||||
canFormatCurrentBlock.value = !!(
|
||||
activeBlock &&
|
||||
getLanguage(activeBlock.language.name as any)?.prettier
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('Error checking format capability:', error);
|
||||
canFormatCurrentBlock.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 创建带300ms防抖的更新函数
|
||||
const debouncedUpdateFormatButton = (() => {
|
||||
let timeout: number | null = null;
|
||||
|
||||
return () => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
timeout = window.setTimeout(() => {
|
||||
updateFormatButtonState();
|
||||
timeout = null;
|
||||
}, 300);
|
||||
};
|
||||
})();
|
||||
|
||||
// 编辑器事件管理
|
||||
const setupEditorListeners = (view: any) => {
|
||||
if (!view?.dom) return [];
|
||||
|
||||
const events = [
|
||||
{ type: 'click', handler: updateFormatButtonState },
|
||||
{ type: 'keyup', handler: debouncedUpdateFormatButton },
|
||||
{ type: 'focus', handler: updateFormatButtonState }
|
||||
];
|
||||
|
||||
// 注册所有事件
|
||||
events.forEach(event => view.dom.addEventListener(event.type, event.handler));
|
||||
|
||||
// 返回清理函数数组
|
||||
return events.map(event =>
|
||||
() => view.dom.removeEventListener(event.type, event.handler)
|
||||
);
|
||||
};
|
||||
|
||||
// 监听编辑器视图变化
|
||||
let cleanupListeners: (() => void)[] = [];
|
||||
|
||||
watch(
|
||||
() => editorStore.editorView,
|
||||
(newView) => {
|
||||
// 清理旧监听器
|
||||
cleanupListeners.forEach(cleanup => cleanup());
|
||||
cleanupListeners = [];
|
||||
|
||||
if (newView) {
|
||||
// 初始更新状态
|
||||
updateFormatButtonState();
|
||||
// 设置新监听器
|
||||
cleanupListeners = setupEditorListeners(newView);
|
||||
} else {
|
||||
canFormatCurrentBlock.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 组件生命周期
|
||||
const isLoaded = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
isLoaded.value = true;
|
||||
// 首次更新格式化状态
|
||||
updateFormatButtonState();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理所有事件监听器
|
||||
cleanupListeners.forEach(cleanup => cleanup());
|
||||
cleanupListeners = [];
|
||||
});
|
||||
|
||||
// 组件加载后初始化置顶状态
|
||||
watch(isLoaded, async (loaded) => {
|
||||
if (loaded) {
|
||||
// 初始化时从配置文件读取置顶状态
|
||||
isCurrentWindowOnTop.value = configStore.config.general.alwaysOnTop;
|
||||
await setWindowAlwaysOnTop(isCurrentWindowOnTop.value);
|
||||
}
|
||||
});
|
||||
|
||||
const handleUpdateButtonClick = async () => {
|
||||
if (updateStore.hasUpdate && !updateStore.isUpdating && !updateStore.updateSuccess) {
|
||||
// 开始下载更新
|
||||
await updateStore.applyUpdate();
|
||||
} else if (updateStore.updateSuccess) {
|
||||
// 更新成功后,点击重启
|
||||
await updateStore.restartApplication();
|
||||
}
|
||||
};
|
||||
|
||||
// 更新按钮标题计算属性
|
||||
const updateButtonTitle = computed(() => {
|
||||
if (updateStore.isChecking) return t('settings.checking');
|
||||
if (updateStore.isUpdating) return t('settings.updating');
|
||||
if (updateStore.updateSuccess) return t('settings.updateSuccessRestartRequired');
|
||||
if (updateStore.hasUpdate) return `${t('settings.newVersionAvailable')}: ${updateStore.updateResult?.latestVersion || ''}`;
|
||||
return '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toolbar-container">
|
||||
<div class="statistics">
|
||||
<span class="stat-item" :title="t('toolbar.editor.lines')">{{ t('toolbar.editor.lines') }}: <span
|
||||
class="stat-value">{{
|
||||
editorStore.documentStats.lines
|
||||
}}</span></span>
|
||||
<span class="stat-item" :title="t('toolbar.editor.characters')">{{ t('toolbar.editor.characters') }}: <span
|
||||
class="stat-value">{{
|
||||
editorStore.documentStats.characters
|
||||
}}</span></span>
|
||||
<span class="stat-item" :title="t('toolbar.editor.selected')"
|
||||
v-if="editorStore.documentStats.selectedCharacters > 0">
|
||||
{{ t('toolbar.editor.selected') }}: <span class="stat-value">{{
|
||||
editorStore.documentStats.selectedCharacters
|
||||
}}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<span class="font-size" :title="t('toolbar.fontSizeTooltip')" @click="() => configStore.resetFontSize()">
|
||||
{{ 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">
|
||||
<path
|
||||
d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/>
|
||||
<path d="M5 3v4"/>
|
||||
<path d="M19 17v4"/>
|
||||
<path d="M3 5h4"/>
|
||||
<path d="M17 19h4"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- 更新按钮 - 根据状态显示不同图标 -->
|
||||
<div
|
||||
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': isCurrentWindowOnTop }"
|
||||
:title="t('toolbar.alwaysOnTop')"
|
||||
@click="toggleAlwaysOnTop"
|
||||
>
|
||||
<svg class="pin-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M557.44 104.96l361.6 361.6-60.16 64-26.88-33.92-181.12 181.12L617.6 832l-60.16 60.16-181.12-184.32-211.2 211.2-60.16-60.16 211.2-211.2-181.12-181.12 60.16-60.16 151.04-30.08 181.12-181.12-30.72-30.08 64-60.16zM587.52 256L387.84 455.04l-120.32 23.68 277.76 277.76 23.68-120.32L768 436.48z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
||||
<button v-if="windowStore.isMainWindow" class="settings-btn" :title="t('toolbar.settings')" @click="goToSettings">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path
|
||||
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.toolbar-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
padding: 0 12px;
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
user-select: none;
|
||||
|
||||
.statistics {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.stat-item {
|
||||
color: var(--text-muted);
|
||||
|
||||
.stat-value {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.font-size {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
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 {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--border-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: rgba(181, 206, 168, 0.2);
|
||||
|
||||
.pin-icon {
|
||||
fill: #b5cea8;
|
||||
}
|
||||
}
|
||||
|
||||
.pin-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: var(--text-muted);
|
||||
transition: fill 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.format-button {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--border-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
stroke: var(--text-muted);
|
||||
transition: stroke 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover svg {
|
||||
stroke: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,94 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {useEditorStore} from '@/stores/editor';
|
||||
|
||||
const editorStore = useEditorStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="toolbar-container">
|
||||
<div class="statistics">
|
||||
<span class="stat-item" title="行数">Ln: <span class="stat-value">{{
|
||||
editorStore.documentStats.lines
|
||||
}}</span></span>
|
||||
<span class="stat-item" title="字符数">Ch: <span class="stat-value">{{
|
||||
editorStore.documentStats.characters
|
||||
}}</span></span>
|
||||
<span class="stat-item" title="选中字符数" v-if="editorStore.documentStats.selectedCharacters > 0">
|
||||
Sel: <span class="stat-value">{{ editorStore.documentStats.selectedCharacters }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<span class="font-size" title="字体大小 (Ctrl+滚轮调整)">
|
||||
{{ editorStore.fontSize }}px
|
||||
</span>
|
||||
<span class="encoding">{{ editorStore.encoding }}</span>
|
||||
<button class="settings-btn" @click="editorStore.openSettings">
|
||||
<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>
|
||||
<path
|
||||
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.toolbar-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
padding: 0 12px;
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
|
||||
.statistics {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.stat-item {
|
||||
color: var(--text-muted);
|
||||
|
||||
.stat-value {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.font-size {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.encoding {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,8 +0,0 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
|
||||
export const fontTheme = EditorView.theme({
|
||||
'&': {
|
||||
fontFamily: '"Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", Arial, sans-serif',
|
||||
fontSize: '12px'
|
||||
}
|
||||
})
|
@@ -1,186 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {onBeforeUnmount, onMounted, ref} from 'vue';
|
||||
import {
|
||||
crosshairCursor,
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
EditorView,
|
||||
highlightActiveLineGutter,
|
||||
highlightSpecialChars,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
rectangularSelection,
|
||||
} from '@codemirror/view';
|
||||
import {EditorState} from '@codemirror/state';
|
||||
import {baseDark} from "@/editor/theme/base-dark";
|
||||
import {
|
||||
bracketMatching,
|
||||
defaultHighlightStyle,
|
||||
foldGutter,
|
||||
foldKeymap,
|
||||
indentOnInput,
|
||||
syntaxHighlighting
|
||||
} from '@codemirror/language';
|
||||
import {defaultKeymap, history, historyKeymap} from '@codemirror/commands';
|
||||
import {highlightSelectionMatches, searchKeymap} from '@codemirror/search';
|
||||
import {autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap} from '@codemirror/autocomplete';
|
||||
import {lintKeymap} from '@codemirror/lint';
|
||||
import {fontTheme} from "@/editor/font/font";
|
||||
import { useEditorStore } from '@/stores/editor';
|
||||
|
||||
// 使用Pinia store
|
||||
const editorStore = useEditorStore();
|
||||
|
||||
const props = defineProps({
|
||||
initialDoc: {
|
||||
type: String,
|
||||
default: '// 在此处编写代码'
|
||||
}
|
||||
});
|
||||
|
||||
const editorElement = ref<HTMLElement | null>(null);
|
||||
|
||||
// 处理滚轮缩放字体
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
// 检查是否按住了Ctrl键
|
||||
if (event.ctrlKey) {
|
||||
// 阻止默认行为(防止页面缩放)
|
||||
event.preventDefault();
|
||||
|
||||
// 根据滚轮方向增大或减小字体
|
||||
if (event.deltaY < 0) {
|
||||
// 向上滚动,增大字体
|
||||
editorStore.increaseFontSize();
|
||||
} else {
|
||||
// 向下滚动,减小字体
|
||||
editorStore.decreaseFontSize();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 更新统计信息
|
||||
const updateStats = () => {
|
||||
if (!editorStore.editorView) return;
|
||||
|
||||
const view = editorStore.editorView;
|
||||
const state = view.state;
|
||||
const doc = state.doc;
|
||||
const text = doc.toString();
|
||||
|
||||
// 计算选中的字符数
|
||||
let selectedChars = 0;
|
||||
const selections = state.selection;
|
||||
if (selections) {
|
||||
for (let i = 0; i < selections.ranges.length; i++) {
|
||||
const range = selections.ranges[i];
|
||||
selectedChars += range.to - range.from;
|
||||
}
|
||||
}
|
||||
|
||||
editorStore.updateDocumentStats({
|
||||
lines: doc.lines,
|
||||
characters: text.length,
|
||||
selectedCharacters: selectedChars
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!editorElement.value) return;
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: props.initialDoc,
|
||||
extensions: [
|
||||
baseDark,
|
||||
fontTheme,
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
foldGutter(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
EditorView.lineWrapping,
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
syntaxHighlighting(defaultHighlightStyle, {fallback: true}),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
autocompletion(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
highlightSelectionMatches(),
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
...lintKeymap
|
||||
]),
|
||||
EditorView.updateListener.of(update => {
|
||||
if (update.docChanged || update.selectionSet) {
|
||||
updateStats();
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
const view = new EditorView({
|
||||
state,
|
||||
parent: editorElement.value
|
||||
});
|
||||
|
||||
// 将编辑器实例保存到store
|
||||
editorStore.setEditorView(view);
|
||||
|
||||
// 初始化统计
|
||||
updateStats();
|
||||
|
||||
// 应用初始字体大小
|
||||
editorStore.applyFontSize();
|
||||
|
||||
// 添加滚轮事件监听
|
||||
editorElement.value.addEventListener('wheel', handleWheel, { passive: false });
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 移除滚轮事件监听
|
||||
if (editorElement.value) {
|
||||
editorElement.value.removeEventListener('wheel', handleWheel);
|
||||
}
|
||||
|
||||
if (editorStore.editorView) {
|
||||
editorStore.editorView.destroy();
|
||||
editorStore.setEditorView(null);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="editor-container">
|
||||
<div ref="editorElement" class="editor"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.editor-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.editor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.cm-editor) {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.cm-scroller) {
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
@@ -1,128 +0,0 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'aura',
|
||||
dark: true,
|
||||
background: '#21202e',
|
||||
foreground: '#edecee',
|
||||
selection: '#A198EC7F',
|
||||
cursor: '#a277ff',
|
||||
dropdownBackground: '#21202e',
|
||||
dropdownBorder: '#3b334b',
|
||||
activeLine: '#4d4b6622',
|
||||
lineNumber: '#a394f033',
|
||||
lineNumberActive: '#cdccce',
|
||||
matchingBracket: '#a394f033',
|
||||
keyword: '#a277ff',
|
||||
storage: '#a277ff',
|
||||
variable: '#edecee',
|
||||
parameter: '#edecee',
|
||||
function: '#ffca85',
|
||||
string: '#61ffca',
|
||||
constant: '#61ffca',
|
||||
type: '#82e2ff',
|
||||
class: '#82e2ff',
|
||||
number: '#61ffca',
|
||||
comment: '#6d6d6d',
|
||||
heading: '#a277ff',
|
||||
invalid: '#ff6767',
|
||||
regexp: '#61ffca',
|
||||
}
|
||||
|
||||
export const auraTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const auraHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const aura: Extension = [
|
||||
auraTheme,
|
||||
syntaxHighlighting(auraHighlightStyle),
|
||||
]
|
@@ -1,129 +0,0 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'base-dark',
|
||||
dark: true,
|
||||
background: '#252B37',
|
||||
foreground: '#9BB586',
|
||||
selection: '#1A5888',
|
||||
cursor: '#F8F8F2',
|
||||
dropdownBackground: '#282A36',
|
||||
dropdownBorder: '#191A21',
|
||||
activeLine: '#2E333F',
|
||||
lineNumber: '#676d7c',
|
||||
lineNumberActive: '#F8F8F2',
|
||||
lineNumberBackground: '#212731',
|
||||
matchingBracket: '#44475A',
|
||||
keyword: '#FF79C6',
|
||||
storage: '#FF79C6',
|
||||
variable: '#F8F8F2',
|
||||
parameter: '#F8F8F2',
|
||||
function: '#50FA7B',
|
||||
string: '#F1FA8C',
|
||||
constant: '#BD93F9',
|
||||
type: '#8BE9FD',
|
||||
class: '#8BE9FD',
|
||||
number: '#BD93F9',
|
||||
comment: '#6272A4',
|
||||
heading: '#BD93F9',
|
||||
invalid: '#FF5555',
|
||||
regexp: '#F1FA8C',
|
||||
}
|
||||
|
||||
export const draculaTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.lineNumberBackground,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const draculaHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const baseDark: Extension = [
|
||||
draculaTheme,
|
||||
syntaxHighlighting(draculaHighlightStyle),
|
||||
]
|
@@ -1,128 +0,0 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'dracula',
|
||||
dark: true,
|
||||
background: '#282A36',
|
||||
foreground: '#F8F8F2',
|
||||
selection: '#44475A',
|
||||
cursor: '#F8F8F2',
|
||||
dropdownBackground: '#282A36',
|
||||
dropdownBorder: '#191A21',
|
||||
activeLine: '#53576c22',
|
||||
lineNumber: '#6272A4',
|
||||
lineNumberActive: '#F8F8F2',
|
||||
matchingBracket: '#44475A',
|
||||
keyword: '#FF79C6',
|
||||
storage: '#FF79C6',
|
||||
variable: '#F8F8F2',
|
||||
parameter: '#F8F8F2',
|
||||
function: '#50FA7B',
|
||||
string: '#F1FA8C',
|
||||
constant: '#BD93F9',
|
||||
type: '#8BE9FD',
|
||||
class: '#8BE9FD',
|
||||
number: '#BD93F9',
|
||||
comment: '#6272A4',
|
||||
heading: '#BD93F9',
|
||||
invalid: '#FF5555',
|
||||
regexp: '#F1FA8C',
|
||||
}
|
||||
|
||||
export const draculaTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const draculaHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const dracula: Extension = [
|
||||
draculaTheme,
|
||||
syntaxHighlighting(draculaHighlightStyle),
|
||||
]
|
@@ -1,128 +0,0 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'githubDark',
|
||||
dark: true,
|
||||
background: '#24292e',
|
||||
foreground: '#d1d5da',
|
||||
selection: '#3392FF44',
|
||||
cursor: '#c8e1ff',
|
||||
dropdownBackground: '#24292e',
|
||||
dropdownBorder: '#1b1f23',
|
||||
activeLine: '#4d566022',
|
||||
lineNumber: '#444d56',
|
||||
lineNumberActive: '#e1e4e8',
|
||||
matchingBracket: '#17E5E650',
|
||||
keyword: '#f97583',
|
||||
storage: '#f97583',
|
||||
variable: '#ffab70',
|
||||
parameter: '#e1e4e8',
|
||||
function: '#79b8ff',
|
||||
string: '#9ecbff',
|
||||
constant: '#79b8ff',
|
||||
type: '#79b8ff',
|
||||
class: '#b392f0',
|
||||
number: '#79b8ff',
|
||||
comment: '#6a737d',
|
||||
heading: '#79b8ff',
|
||||
invalid: '#f97583',
|
||||
regexp: '#9ecbff',
|
||||
}
|
||||
|
||||
export const githubDarkTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const githubDarkHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const githubDark: Extension = [
|
||||
githubDarkTheme,
|
||||
syntaxHighlighting(githubDarkHighlightStyle),
|
||||
]
|
@@ -1,128 +0,0 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'githubLight',
|
||||
dark: false,
|
||||
background: '#fff',
|
||||
foreground: '#444d56',
|
||||
selection: '#0366d625',
|
||||
cursor: '#044289',
|
||||
dropdownBackground: '#fff',
|
||||
dropdownBorder: '#e1e4e8',
|
||||
activeLine: '#c6c6c622',
|
||||
lineNumber: '#1b1f234d',
|
||||
lineNumberActive: '#24292e',
|
||||
matchingBracket: '#34d05840',
|
||||
keyword: '#d73a49',
|
||||
storage: '#d73a49',
|
||||
variable: '#e36209',
|
||||
parameter: '#24292e',
|
||||
function: '#005cc5',
|
||||
string: '#032f62',
|
||||
constant: '#005cc5',
|
||||
type: '#005cc5',
|
||||
class: '#6f42c1',
|
||||
number: '#005cc5',
|
||||
comment: '#6a737d',
|
||||
heading: '#005cc5',
|
||||
invalid: '#cb2431',
|
||||
regexp: '#032f62',
|
||||
}
|
||||
|
||||
export const githubLightTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const githubLightHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const githubLight: Extension = [
|
||||
githubLightTheme,
|
||||
syntaxHighlighting(githubLightHighlightStyle),
|
||||
]
|
@@ -1,128 +0,0 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'materialDark',
|
||||
dark: true,
|
||||
background: '#263238',
|
||||
foreground: '#EEFFFF',
|
||||
selection: '#80CBC420',
|
||||
cursor: '#FFCC00',
|
||||
dropdownBackground: '#263238',
|
||||
dropdownBorder: '#FFFFFF10',
|
||||
activeLine: '#4c616c22',
|
||||
lineNumber: '#37474F',
|
||||
lineNumberActive: '#607a86',
|
||||
matchingBracket: '#263238',
|
||||
keyword: '#C792EA',
|
||||
storage: '#C792EA',
|
||||
variable: '#EEFFFF',
|
||||
parameter: '#EEFFFF',
|
||||
function: '#82AAFF',
|
||||
string: '#C3E88D',
|
||||
constant: '#F78C6C',
|
||||
type: '#B2CCD6',
|
||||
class: '#FFCB6B',
|
||||
number: '#F78C6C',
|
||||
comment: '#546E7A',
|
||||
heading: '#C3E88D',
|
||||
invalid: '#FF5370',
|
||||
regexp: '#89DDFF',
|
||||
}
|
||||
|
||||
export const materialDarkTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const materialDarkHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const materialDark: Extension = [
|
||||
materialDarkTheme,
|
||||
syntaxHighlighting(materialDarkHighlightStyle),
|
||||
]
|
@@ -1,128 +0,0 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'materialLight',
|
||||
dark: false,
|
||||
background: '#FAFAFA',
|
||||
foreground: '#90A4AE',
|
||||
selection: '#80CBC440',
|
||||
cursor: '#272727',
|
||||
dropdownBackground: '#FAFAFA',
|
||||
dropdownBorder: '#00000010',
|
||||
activeLine: '#c2c2c222',
|
||||
lineNumber: '#CFD8DC',
|
||||
lineNumberActive: '#7E939E',
|
||||
matchingBracket: '#FAFAFA',
|
||||
keyword: '#7C4DFF',
|
||||
storage: '#7C4DFF',
|
||||
variable: '#90A4AE',
|
||||
parameter: '#90A4AE',
|
||||
function: '#6182B8',
|
||||
string: '#91B859',
|
||||
constant: '#F76D47',
|
||||
type: '#8796B0',
|
||||
class: '#FFB62C',
|
||||
number: '#F76D47',
|
||||
comment: '#90A4AE',
|
||||
heading: '#91B859',
|
||||
invalid: '#E53935',
|
||||
regexp: '#39ADB5',
|
||||
}
|
||||
|
||||
export const materialLightTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const materialLightHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const materialLight: Extension = [
|
||||
materialLightTheme,
|
||||
syntaxHighlighting(materialLightHighlightStyle),
|
||||
]
|
@@ -1,128 +0,0 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'solarizedDark',
|
||||
dark: true,
|
||||
background: '#002B36',
|
||||
foreground: '#93A1A1',
|
||||
selection: '#274642',
|
||||
cursor: '#D30102',
|
||||
dropdownBackground: '#002B36',
|
||||
dropdownBorder: '#2AA19899',
|
||||
activeLine: '#005b7022',
|
||||
lineNumber: '#93A1A1',
|
||||
lineNumberActive: '#949494',
|
||||
matchingBracket: '#073642',
|
||||
keyword: '#859900',
|
||||
storage: '#93A1A1',
|
||||
variable: '#268BD2',
|
||||
parameter: '#268BD2',
|
||||
function: '#268BD2',
|
||||
string: '#2AA198',
|
||||
constant: '#CB4B16',
|
||||
type: '#CB4B16',
|
||||
class: '#CB4B16',
|
||||
number: '#D33682',
|
||||
comment: '#586E75',
|
||||
heading: '#268BD2',
|
||||
invalid: '#DC322F',
|
||||
regexp: '#DC322F',
|
||||
}
|
||||
|
||||
export const solarizedDarkTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const solarizedDarkHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const solarizedDark: Extension = [
|
||||
solarizedDarkTheme,
|
||||
syntaxHighlighting(solarizedDarkHighlightStyle),
|
||||
]
|
@@ -1,128 +0,0 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'solarizedLight',
|
||||
dark: false,
|
||||
background: '#FDF6E3',
|
||||
foreground: '#586E75',
|
||||
selection: '#EEE8D5',
|
||||
cursor: '#657B83',
|
||||
dropdownBackground: '#FDF6E3',
|
||||
dropdownBorder: '#D3AF86',
|
||||
activeLine: '#d5bd5c22',
|
||||
lineNumber: '#586E75',
|
||||
lineNumberActive: '#567983',
|
||||
matchingBracket: '#EEE8D5',
|
||||
keyword: '#859900',
|
||||
storage: '#586E75',
|
||||
variable: '#268BD2',
|
||||
parameter: '#268BD2',
|
||||
function: '#268BD2',
|
||||
string: '#2AA198',
|
||||
constant: '#CB4B16',
|
||||
type: '#CB4B16',
|
||||
class: '#CB4B16',
|
||||
number: '#D33682',
|
||||
comment: '#93A1A1',
|
||||
heading: '#268BD2',
|
||||
invalid: '#DC322F',
|
||||
regexp: '#DC322F',
|
||||
}
|
||||
|
||||
export const solarizedLightTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const solarizedLightHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const solarizedLight: Extension = [
|
||||
solarizedLightTheme,
|
||||
syntaxHighlighting(solarizedLightHighlightStyle),
|
||||
]
|
@@ -1,128 +0,0 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'tokyoNightDay',
|
||||
dark: false,
|
||||
background: '#e1e2e7',
|
||||
foreground: '#6a6f8e',
|
||||
selection: '#8591b840',
|
||||
cursor: '#3760bf',
|
||||
dropdownBackground: '#e1e2e7',
|
||||
dropdownBorder: '#6a6f8e',
|
||||
activeLine: '#a7aaba22',
|
||||
lineNumber: '#b3b6cd',
|
||||
lineNumberActive: '#68709a',
|
||||
matchingBracket: '#e9e9ec',
|
||||
keyword: '#9854f1',
|
||||
storage: '#9854f1',
|
||||
variable: '#3760bf',
|
||||
parameter: '#3760bf',
|
||||
function: '#2e7de9',
|
||||
string: '#587539',
|
||||
constant: '#9854f1',
|
||||
type: '#07879d',
|
||||
class: '#3760bf',
|
||||
number: '#b15c00',
|
||||
comment: '#9da3c2',
|
||||
heading: '#006a83',
|
||||
invalid: '#ff3e64',
|
||||
regexp: '#2e5857',
|
||||
}
|
||||
|
||||
export const tokyoNightDayTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const tokyoNightDayHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const tokyoNightDay: Extension = [
|
||||
tokyoNightDayTheme,
|
||||
syntaxHighlighting(tokyoNightDayHighlightStyle),
|
||||
]
|
@@ -1,128 +0,0 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'tokyoNightStorm',
|
||||
dark: true,
|
||||
background: '#24283b',
|
||||
foreground: '#7982a9',
|
||||
selection: '#6f7bb630',
|
||||
cursor: '#c0caf5',
|
||||
dropdownBackground: '#24283b',
|
||||
dropdownBorder: '#7982a9',
|
||||
activeLine: '#4d547722',
|
||||
lineNumber: '#3b4261',
|
||||
lineNumberActive: '#737aa2',
|
||||
matchingBracket: '#1f2335',
|
||||
keyword: '#bb9af7',
|
||||
storage: '#bb9af7',
|
||||
variable: '#c0caf5',
|
||||
parameter: '#c0caf5',
|
||||
function: '#7aa2f7',
|
||||
string: '#9ece6a',
|
||||
constant: '#bb9af7',
|
||||
type: '#2ac3de',
|
||||
class: '#c0caf5',
|
||||
number: '#ff9e64',
|
||||
comment: '#565f89',
|
||||
heading: '#89ddff',
|
||||
invalid: '#ff5370',
|
||||
regexp: '#b4f9f8',
|
||||
}
|
||||
|
||||
export const tokyoNightStormTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const tokyoNightStormHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const tokyoNightStorm: Extension = [
|
||||
tokyoNightStormTheme,
|
||||
syntaxHighlighting(tokyoNightStormHighlightStyle),
|
||||
]
|
@@ -1,128 +0,0 @@
|
||||
import {EditorView} from '@codemirror/view'
|
||||
import {Extension} from '@codemirror/state'
|
||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language'
|
||||
import {tags as t} from '@lezer/highlight'
|
||||
|
||||
export const config = {
|
||||
name: 'tokyoNight',
|
||||
dark: true,
|
||||
background: '#1a1b26',
|
||||
foreground: '#787c99',
|
||||
selection: '#515c7e40',
|
||||
cursor: '#c0caf5',
|
||||
dropdownBackground: '#1a1b26',
|
||||
dropdownBorder: '#787c99',
|
||||
activeLine: '#43455c22',
|
||||
lineNumber: '#363b54',
|
||||
lineNumberActive: '#737aa2',
|
||||
matchingBracket: '#16161e',
|
||||
keyword: '#bb9af7',
|
||||
storage: '#bb9af7',
|
||||
variable: '#c0caf5',
|
||||
parameter: '#c0caf5',
|
||||
function: '#7aa2f7',
|
||||
string: '#9ece6a',
|
||||
constant: '#bb9af7',
|
||||
type: '#0db9d7',
|
||||
class: '#c0caf5',
|
||||
number: '#ff9e64',
|
||||
comment: '#444b6a',
|
||||
heading: '#89ddff',
|
||||
invalid: '#ff5370',
|
||||
regexp: '#b4f9f8',
|
||||
}
|
||||
|
||||
export const tokyoNightTheme = EditorView.theme({
|
||||
'&': {
|
||||
color: config.foreground,
|
||||
backgroundColor: config.background,
|
||||
},
|
||||
|
||||
'.cm-content': {caretColor: config.cursor},
|
||||
|
||||
'.cm-cursor, .cm-dropCursor': {borderLeftColor: config.cursor},
|
||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {backgroundColor: config.selection},
|
||||
|
||||
'.cm-panels': {backgroundColor: config.dropdownBackground, color: config.foreground},
|
||||
'.cm-panels.cm-panels-top': {borderBottom: '2px solid black'},
|
||||
'.cm-panels.cm-panels-bottom': {borderTop: '2px solid black'},
|
||||
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: config.dropdownBackground,
|
||||
outline: `1px solid ${config.dropdownBorder}`
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: config.selection
|
||||
},
|
||||
|
||||
'.cm-activeLine': {backgroundColor: config.activeLine},
|
||||
'.cm-selectionMatch': {backgroundColor: config.selection},
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: config.matchingBracket,
|
||||
outline: 'none'
|
||||
},
|
||||
|
||||
'.cm-gutters': {
|
||||
backgroundColor: config.background,
|
||||
color: config.foreground,
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-activeLineGutter': {backgroundColor: config.background},
|
||||
|
||||
'.cm-lineNumbers .cm-gutterElement': {color: config.lineNumber},
|
||||
'.cm-lineNumbers .cm-activeLineGutter': {color: config.lineNumberActive},
|
||||
|
||||
'.cm-foldPlaceholder': {
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
color: config.foreground
|
||||
},
|
||||
'.cm-tooltip': {
|
||||
border: `1px solid ${config.dropdownBorder}`,
|
||||
backgroundColor: config.dropdownBackground,
|
||||
color: config.foreground,
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent'
|
||||
},
|
||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||
borderTopColor: config.foreground,
|
||||
borderBottomColor: config.foreground,
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
'& > ul > li[aria-selected]': {
|
||||
background: config.selection,
|
||||
color: config.foreground,
|
||||
}
|
||||
}
|
||||
}, {dark: config.dark})
|
||||
|
||||
export const tokyoNightHighlightStyle = HighlightStyle.define([
|
||||
{tag: t.keyword, color: config.keyword},
|
||||
{tag: [t.name, t.deleted, t.character, t.macroName], color: config.variable},
|
||||
{tag: [t.propertyName], color: config.function},
|
||||
{tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: config.string},
|
||||
{tag: [t.function(t.variableName), t.labelName], color: config.function},
|
||||
{tag: [t.color, t.constant(t.name), t.standard(t.name)], color: config.constant},
|
||||
{tag: [t.definition(t.name), t.separator], color: config.variable},
|
||||
{tag: [t.className], color: config.class},
|
||||
{tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: config.number},
|
||||
{tag: [t.typeName], color: config.type, fontStyle: config.type},
|
||||
{tag: [t.operator, t.operatorKeyword], color: config.keyword},
|
||||
{tag: [t.url, t.escape, t.regexp, t.link], color: config.regexp},
|
||||
{tag: [t.meta, t.comment], color: config.comment},
|
||||
{tag: t.strong, fontWeight: 'bold'},
|
||||
{tag: t.emphasis, fontStyle: 'italic'},
|
||||
{tag: t.link, textDecoration: 'underline'},
|
||||
{tag: t.heading, fontWeight: 'bold', color: config.heading},
|
||||
{tag: [t.atom, t.bool, t.special(t.variableName)], color: config.variable},
|
||||
{tag: t.invalid, color: config.invalid},
|
||||
{tag: t.strikethrough, textDecoration: 'line-through'},
|
||||
])
|
||||
|
||||
export const tokyoNight: Extension = [
|
||||
tokyoNightTheme,
|
||||
syntaxHighlighting(tokyoNightHighlightStyle),
|
||||
]
|
18
frontend/src/i18n/index.ts
Normal file
18
frontend/src/i18n/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {createI18n} from 'vue-i18n';
|
||||
import messages from './locales';
|
||||
|
||||
// 创建i18n实例
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
compositionOnly: true,
|
||||
globalInjection: true,
|
||||
silentTranslationWarn: true,
|
||||
locale: 'zh-CN',
|
||||
fallbackLocale: 'zh-CN',
|
||||
silentFallbackWarn: true,
|
||||
missingWarn: true,
|
||||
fallbackWarn: false,
|
||||
messages
|
||||
});
|
||||
|
||||
export default i18n;
|
294
frontend/src/i18n/locales/en-US.ts
Normal file
294
frontend/src/i18n/locales/en-US.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
export default {
|
||||
locale: 'en-US',
|
||||
titlebar: {
|
||||
minimize: 'Minimize',
|
||||
maximize: 'Maximize',
|
||||
restore: 'Restore Down',
|
||||
close: 'Close'
|
||||
},
|
||||
toolbar: {
|
||||
editor: {
|
||||
lines: 'Ln',
|
||||
characters: 'Ch',
|
||||
selected: 'Sel'
|
||||
},
|
||||
fontSizeTooltip: 'Font Size (Ctrl+wheel to adjust)',
|
||||
settings: 'Settings',
|
||||
alwaysOnTop: 'Always on Top',
|
||||
blockLanguage: 'Block Language',
|
||||
searchLanguage: 'Search language...',
|
||||
noLanguageFound: 'No language found',
|
||||
formatHint: 'Click Format Block (Ctrl+Shift+F)',
|
||||
// Document selector
|
||||
selectDocument: 'Select Document',
|
||||
searchOrCreateDocument: 'Search or enter new document name...',
|
||||
createDocument: 'Create',
|
||||
noDocumentFound: 'No document found',
|
||||
loading: 'Loading...',
|
||||
rename: 'Rename',
|
||||
delete: 'Delete',
|
||||
confirm: 'Confirm',
|
||||
confirmDelete: 'Click again to confirm delete',
|
||||
openInNewWindow: 'Open in New Window',
|
||||
alreadyOpenInNewWindow: 'Already open in another window',
|
||||
documentNameTooLong: 'Document name cannot exceed {max} characters',
|
||||
documentNameRequired: 'Document name cannot be empty',
|
||||
cannotDeleteLastDocument: 'Cannot delete the last document',
|
||||
cannotDeleteDefaultDocument: 'Cannot delete the default document',
|
||||
unknownTime: 'Unknown time',
|
||||
invalidDate: 'Invalid date',
|
||||
timeError: 'Time error',
|
||||
},
|
||||
languages: {
|
||||
'zh-CN': 'Chinese',
|
||||
'en-US': 'English'
|
||||
},
|
||||
systemTheme: {
|
||||
dark: 'Dark',
|
||||
light: 'Light',
|
||||
auto: 'Follow System'
|
||||
},
|
||||
keybindings: {
|
||||
headers: {
|
||||
shortcut: 'Shortcut',
|
||||
category: 'Category',
|
||||
description: 'Description'
|
||||
},
|
||||
commands: {
|
||||
showSearch: 'Show search panel',
|
||||
hideSearch: 'Hide search panel',
|
||||
searchToggleCase: 'Toggle case-sensitive matching',
|
||||
searchToggleWord: 'Toggle whole word matching',
|
||||
searchToggleRegex: 'Toggle regular expression matching',
|
||||
searchShowReplace: 'Show replace functionality',
|
||||
searchReplaceAll: 'Replace all matches',
|
||||
blockSelectAll: 'Select all in block',
|
||||
blockAddAfterCurrent: 'Add new block after current',
|
||||
blockAddAfterLast: 'Add new block at end',
|
||||
blockAddBeforeCurrent: 'Add new block before current',
|
||||
blockGotoPrevious: 'Go to previous block',
|
||||
blockGotoNext: 'Go to next block',
|
||||
blockSelectPrevious: 'Select previous block',
|
||||
blockSelectNext: 'Select next block',
|
||||
blockDelete: 'Delete current block',
|
||||
blockMoveUp: 'Move current block up',
|
||||
blockMoveDown: 'Move current block down',
|
||||
blockDeleteLine: 'Delete line',
|
||||
blockMoveLineUp: 'Move line up',
|
||||
blockMoveLineDown: 'Move line down',
|
||||
blockTransposeChars: 'Transpose characters',
|
||||
blockFormat: 'Format code block',
|
||||
blockCopy: 'Copy',
|
||||
blockCut: 'Cut',
|
||||
blockPaste: 'Paste',
|
||||
historyUndo: 'Undo',
|
||||
historyRedo: 'Redo',
|
||||
historyUndoSelection: 'Undo selection',
|
||||
historyRedoSelection: 'Redo selection',
|
||||
foldCode: 'Fold code',
|
||||
unfoldCode: 'Unfold code',
|
||||
foldAll: 'Fold all',
|
||||
unfoldAll: 'Unfold all',
|
||||
cursorSyntaxLeft: 'Cursor syntax left',
|
||||
cursorSyntaxRight: 'Cursor syntax right',
|
||||
selectSyntaxLeft: 'Select syntax left',
|
||||
selectSyntaxRight: 'Select syntax right',
|
||||
copyLineUp: 'Copy line up',
|
||||
copyLineDown: 'Copy line down',
|
||||
insertBlankLine: 'Insert blank line',
|
||||
selectLine: 'Select line',
|
||||
selectParentSyntax: 'Select parent syntax',
|
||||
indentLess: 'Indent less',
|
||||
indentMore: 'Indent more',
|
||||
indentSelection: 'Indent selection',
|
||||
cursorMatchingBracket: 'Cursor matching bracket',
|
||||
toggleComment: 'Toggle comment',
|
||||
toggleBlockComment: 'Toggle block comment',
|
||||
insertNewlineAndIndent: 'Insert newline and indent',
|
||||
deleteCharBackward: 'Delete character backward',
|
||||
deleteCharForward: 'Delete character forward',
|
||||
deleteGroupBackward: 'Delete group backward',
|
||||
deleteGroupForward: 'Delete group forward',
|
||||
textHighlightToggle: 'Toggle text highlight',
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
backToEditor: 'Back to Editor',
|
||||
systemInfo: 'System Info',
|
||||
general: 'General',
|
||||
editing: 'Editor',
|
||||
appearance: 'Appearance',
|
||||
keyBindings: 'Key Bindings',
|
||||
updates: 'Updates',
|
||||
reset: 'Reset',
|
||||
apply: 'Apply',
|
||||
cancel: 'Cancel',
|
||||
dangerZone: 'Danger Zone',
|
||||
resetAllSettings: 'Reset All Settings',
|
||||
confirmReset: 'Confirm the reset?',
|
||||
globalHotkey: 'Global Keyboard Shortcuts',
|
||||
enableGlobalHotkey: 'Enable Global Hotkeys',
|
||||
window: 'Window/Application',
|
||||
showInSystemTray: 'Show in System Tray',
|
||||
enableSystemTray: 'Enable System Tray',
|
||||
alwaysOnTop: 'Always on Top',
|
||||
startup: 'Startup Settings',
|
||||
startAtLogin: 'Start at Login',
|
||||
dataStorage: 'Data Storage',
|
||||
dataPath: 'Data Storage Path',
|
||||
clickToSelectPath: 'Click to select path',
|
||||
resetDefault: 'Reset Default',
|
||||
resetToDefaultPath: 'Reset to default path',
|
||||
fontSize: 'Font Size',
|
||||
fontSizeDescription: 'Editor font size',
|
||||
fontSettings: 'Font Settings',
|
||||
fontFamily: 'Font Family',
|
||||
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',
|
||||
tabSize: 'Tab Size',
|
||||
tabType: 'Tab Type',
|
||||
spaces: 'Spaces',
|
||||
tabs: 'Tabs',
|
||||
enableTabIndent: 'Enable Tab Indent',
|
||||
language: 'Interface Language',
|
||||
systemTheme: 'System Theme',
|
||||
saveOptions: 'Save Options',
|
||||
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'
|
||||
}
|
||||
};
|
7
frontend/src/i18n/locales/index.ts
Normal file
7
frontend/src/i18n/locales/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import zhCN from './zh-CN';
|
||||
import enUS from './en-US';
|
||||
|
||||
export default {
|
||||
'zh-CN': zhCN,
|
||||
'en-US': enUS
|
||||
};
|
295
frontend/src/i18n/locales/zh-CN.ts
Normal file
295
frontend/src/i18n/locales/zh-CN.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
export default {
|
||||
locale: 'zh-CN',
|
||||
titlebar: {
|
||||
minimize: '最小化',
|
||||
maximize: '最大化',
|
||||
restore: '向下还原',
|
||||
close: '关闭'
|
||||
},
|
||||
toolbar: {
|
||||
editor: {
|
||||
lines: 'Ln',
|
||||
characters: 'Ch',
|
||||
selected: 'Sel'
|
||||
},
|
||||
fontSizeTooltip: '字体大小 (Ctrl+滚轮调整)',
|
||||
settings: '设置',
|
||||
alwaysOnTop: '窗口置顶',
|
||||
blockLanguage: '块语言',
|
||||
searchLanguage: '搜索语言...',
|
||||
noLanguageFound: '未找到匹配的语言',
|
||||
formatHint: '点击格式化区块(Ctrl+Shift+F)',
|
||||
// 文档选择器
|
||||
selectDocument: '选择文档',
|
||||
searchOrCreateDocument: '搜索或输入新文档名...',
|
||||
createDocument: '创建',
|
||||
noDocumentFound: '没有找到文档',
|
||||
loading: '加载中...',
|
||||
rename: '重命名',
|
||||
delete: '删除',
|
||||
confirm: '确认',
|
||||
confirmDelete: '再次点击确认删除',
|
||||
openInNewWindow: '在新窗口中打开',
|
||||
alreadyOpenInNewWindow: '已在新窗口中打开',
|
||||
documentNameTooLong: '文档名称不能超过{max}个字符',
|
||||
documentNameRequired: '文档名称不能为空',
|
||||
cannotDeleteLastDocument: '无法删除最后一个文档',
|
||||
cannotDeleteDefaultDocument: '无法删除默认文档',
|
||||
unknownTime: '未知时间',
|
||||
invalidDate: '无效日期',
|
||||
timeError: '时间错误',
|
||||
},
|
||||
languages: {
|
||||
'zh-CN': '简体中文',
|
||||
'en-US': 'English'
|
||||
},
|
||||
systemTheme: {
|
||||
dark: '深色',
|
||||
light: '浅色',
|
||||
auto: '跟随系统'
|
||||
},
|
||||
keybindings: {
|
||||
headers: {
|
||||
shortcut: '快捷键',
|
||||
category: '分类',
|
||||
description: '描述'
|
||||
},
|
||||
commands: {
|
||||
showSearch: '显示搜索面板',
|
||||
hideSearch: '隐藏搜索面板',
|
||||
searchToggleCase: '切换大小写敏感匹配',
|
||||
searchToggleWord: '切换整词匹配',
|
||||
searchToggleRegex: '切换正则表达式匹配',
|
||||
searchShowReplace: '显示替换功能',
|
||||
searchReplaceAll: '替换全部匹配项',
|
||||
blockSelectAll: '块内选择全部',
|
||||
blockAddAfterCurrent: '在当前块后添加新块',
|
||||
blockAddAfterLast: '在最后添加新块',
|
||||
blockAddBeforeCurrent: '在当前块前添加新块',
|
||||
blockGotoPrevious: '跳转到上一个块',
|
||||
blockGotoNext: '跳转到下一个块',
|
||||
blockSelectPrevious: '选择上一个块',
|
||||
blockSelectNext: '选择下一个块',
|
||||
blockDelete: '删除当前块',
|
||||
blockMoveUp: '向上移动当前块',
|
||||
blockMoveDown: '向下移动当前块',
|
||||
blockDeleteLine: '删除行',
|
||||
blockMoveLineUp: '向上移动行',
|
||||
blockMoveLineDown: '向下移动行',
|
||||
blockTransposeChars: '字符转置',
|
||||
blockFormat: '格式化代码块',
|
||||
blockCopy: '复制',
|
||||
blockCut: '剪切',
|
||||
blockPaste: '粘贴',
|
||||
historyUndo: '撤销',
|
||||
historyRedo: '重做',
|
||||
historyUndoSelection: '撤销选择',
|
||||
historyRedoSelection: '重做选择',
|
||||
foldCode: '折叠代码',
|
||||
unfoldCode: '展开代码',
|
||||
foldAll: '折叠全部',
|
||||
unfoldAll: '展开全部',
|
||||
cursorSyntaxLeft: '光标按语法左移',
|
||||
cursorSyntaxRight: '光标按语法右移',
|
||||
selectSyntaxLeft: '按语法选择左侧',
|
||||
selectSyntaxRight: '按语法选择右侧',
|
||||
copyLineUp: '向上复制行',
|
||||
copyLineDown: '向下复制行',
|
||||
insertBlankLine: '插入空行',
|
||||
selectLine: '选择行',
|
||||
selectParentSyntax: '选择父级语法',
|
||||
indentLess: '减少缩进',
|
||||
indentMore: '增加缩进',
|
||||
indentSelection: '缩进选择',
|
||||
cursorMatchingBracket: '光标到匹配括号',
|
||||
toggleComment: '切换注释',
|
||||
toggleBlockComment: '切换块注释',
|
||||
insertNewlineAndIndent: '插入新行并缩进',
|
||||
deleteCharBackward: '向后删除字符',
|
||||
deleteCharForward: '向前删除字符',
|
||||
deleteGroupBackward: '向后删除组',
|
||||
deleteGroupForward: '向前删除组',
|
||||
textHighlightToggle: '切换文本高亮',
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
title: '设置',
|
||||
backToEditor: '返回编辑器',
|
||||
systemInfo: '系统信息',
|
||||
general: '常规',
|
||||
editing: '编辑器',
|
||||
appearance: '外观',
|
||||
extensions: '扩展',
|
||||
keyBindings: '快捷键',
|
||||
updates: '更新',
|
||||
reset: '重置',
|
||||
apply: '应用',
|
||||
cancel: '取消',
|
||||
dangerZone: '危险操作',
|
||||
resetAllSettings: '重置所有设置',
|
||||
confirmReset: '确认重置?',
|
||||
globalHotkey: '全局键盘快捷键',
|
||||
enableGlobalHotkey: '启用全局热键',
|
||||
window: '窗口/应用程序',
|
||||
showInSystemTray: '在系统托盘中显示',
|
||||
enableSystemTray: '启用系统托盘',
|
||||
alwaysOnTop: '窗口始终置顶',
|
||||
startup: '启动设置',
|
||||
startAtLogin: '开机自启动',
|
||||
dataStorage: '数据存储',
|
||||
dataPath: '数据存储路径',
|
||||
clickToSelectPath: '点击选择路径',
|
||||
resetDefault: '恢复默认',
|
||||
resetToDefaultPath: '恢复为默认路径',
|
||||
fontSize: '字体大小',
|
||||
fontSizeDescription: '编辑器字体大小',
|
||||
fontSettings: '字体设置',
|
||||
fontFamily: '字体',
|
||||
fontFamilyDescription: '选择编辑器字体',
|
||||
fontWeight: '字体粗细',
|
||||
fontWeightDescription: '设置字体的粗细程度',
|
||||
lineHeight: '行高',
|
||||
lineHeightDescription: '设置文本行之间的间距',
|
||||
tabSettings: 'Tab 设置',
|
||||
tabSize: 'Tab 大小',
|
||||
tabType: 'Tab 类型',
|
||||
spaces: '空格',
|
||||
tabs: '制表符',
|
||||
enableTabIndent: '启用 Tab 缩进',
|
||||
language: '界面语言',
|
||||
systemTheme: '系统主题',
|
||||
saveOptions: '保存选项',
|
||||
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: '点击清理内存'
|
||||
}
|
||||
};
|
@@ -1,19 +1,15 @@
|
||||
import {createApp} from 'vue';
|
||||
import App from './App.vue';
|
||||
import '@/assets/styles/index.css';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import Aura from '@primeuix/themes/aura';
|
||||
import {createPinia} from 'pinia';
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||
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(PrimeVue, {
|
||||
theme: {
|
||||
preset: Aura
|
||||
}
|
||||
});
|
||||
app.use(i18n);
|
||||
app.use(router);
|
||||
app.mount('#app');
|
62
frontend/src/router/index.ts
Normal file
62
frontend/src/router/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import {createRouter, createWebHashHistory, createWebHistory, RouteRecordRaw} from 'vue-router';
|
||||
import Editor from '@/views/editor/Editor.vue';
|
||||
import Settings from '@/views/settings/Settings.vue';
|
||||
import GeneralPage from '@/views/settings/pages/GeneralPage.vue';
|
||||
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[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Editor',
|
||||
component: Editor
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
redirect: '/settings/general',
|
||||
component: Settings,
|
||||
children: [
|
||||
{
|
||||
path: 'general',
|
||||
name: 'SettingsGeneral',
|
||||
component: GeneralPage
|
||||
},
|
||||
{
|
||||
path: 'editing',
|
||||
name: 'SettingsEditing',
|
||||
component: EditingPage
|
||||
},
|
||||
{
|
||||
path: 'appearance',
|
||||
name: 'SettingsAppearance',
|
||||
component: AppearancePage
|
||||
},
|
||||
{
|
||||
path: 'extensions',
|
||||
name: 'SettingsExtensions',
|
||||
component: ExtensionsPage
|
||||
},
|
||||
{
|
||||
path: 'key-bindings',
|
||||
name: 'SettingsKeyBindings',
|
||||
component: KeyBindingsPage
|
||||
},
|
||||
{
|
||||
path: 'updates',
|
||||
name: 'SettingsUpdates',
|
||||
component: UpdatesPage
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: routes
|
||||
});
|
||||
|
||||
export default router;
|
599
frontend/src/stores/configStore.ts
Normal file
599
frontend/src/stores/configStore.ts
Normal file
@@ -0,0 +1,599 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, reactive} from 'vue';
|
||||
import {ConfigService, StartupService} from '@/../bindings/voidraft/internal/services';
|
||||
import {
|
||||
AppConfig,
|
||||
AppearanceConfig,
|
||||
EditingConfig,
|
||||
GeneralConfig,
|
||||
LanguageType,
|
||||
SystemThemeType,
|
||||
TabType,
|
||||
UpdatesConfig,
|
||||
UpdateSourceType,
|
||||
} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {ConfigUtils} from '@/utils/configUtils';
|
||||
import {WindowController} from '@/utils/windowController';
|
||||
import * as runtime from '@wailsio/runtime';
|
||||
// 国际化相关导入
|
||||
export type SupportedLocaleType = 'zh-CN' | 'en-US';
|
||||
|
||||
// 支持的语言列表
|
||||
export const SUPPORTED_LOCALES = [
|
||||
{
|
||||
code: 'zh-CN' as SupportedLocaleType,
|
||||
name: '简体中文'
|
||||
},
|
||||
{
|
||||
code: 'en-US' as SupportedLocaleType,
|
||||
name: 'English'
|
||||
}
|
||||
] as const;
|
||||
|
||||
// 配置键映射和限制的类型定义
|
||||
type GeneralConfigKeyMap = {
|
||||
readonly [K in keyof GeneralConfig]: string;
|
||||
};
|
||||
|
||||
type EditingConfigKeyMap = {
|
||||
readonly [K in keyof EditingConfig]: string;
|
||||
};
|
||||
|
||||
type AppearanceConfigKeyMap = {
|
||||
readonly [K in keyof AppearanceConfig]: string;
|
||||
};
|
||||
|
||||
type UpdatesConfigKeyMap = {
|
||||
readonly [K in keyof UpdatesConfig]: string;
|
||||
};
|
||||
|
||||
type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight';
|
||||
|
||||
// 配置键映射
|
||||
const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
|
||||
alwaysOnTop: 'general.alwaysOnTop',
|
||||
dataPath: 'general.dataPath',
|
||||
enableSystemTray: 'general.enableSystemTray',
|
||||
startAtLogin: 'general.startAtLogin',
|
||||
enableGlobalHotkey: 'general.enableGlobalHotkey',
|
||||
globalHotkey: 'general.globalHotkey'
|
||||
} as const;
|
||||
|
||||
const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
|
||||
fontSize: 'editing.fontSize',
|
||||
fontFamily: 'editing.fontFamily',
|
||||
fontWeight: 'editing.fontWeight',
|
||||
lineHeight: 'editing.lineHeight',
|
||||
enableTabIndent: 'editing.enableTabIndent',
|
||||
tabSize: 'editing.tabSize',
|
||||
tabType: 'editing.tabType',
|
||||
autoSaveDelay: 'editing.autoSaveDelay'
|
||||
} as const;
|
||||
|
||||
const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
|
||||
language: 'appearance.language',
|
||||
systemTheme: 'appearance.systemTheme',
|
||||
customTheme: 'appearance.customTheme'
|
||||
} as const;
|
||||
|
||||
const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
|
||||
version: 'updates.version',
|
||||
autoUpdate: 'updates.autoUpdate',
|
||||
primarySource: 'updates.primarySource',
|
||||
backupSource: 'updates.backupSource',
|
||||
backupBeforeUpdate: 'updates.backupBeforeUpdate',
|
||||
updateTimeout: 'updates.updateTimeout',
|
||||
github: 'updates.github',
|
||||
gitea: 'updates.gitea'
|
||||
} as const;
|
||||
|
||||
// 配置限制
|
||||
const CONFIG_LIMITS = {
|
||||
fontSize: {min: 12, max: 28, default: 13},
|
||||
tabSize: {min: 2, max: 8, default: 4},
|
||||
lineHeight: {min: 1.0, max: 3.0, default: 1.5},
|
||||
tabType: {values: [TabType.TabTypeSpaces, TabType.TabTypeTab], default: TabType.TabTypeSpaces}
|
||||
} as const;
|
||||
|
||||
// 创建获取翻译的函数
|
||||
export const createFontOptions = (t: (key: string) => string) => [
|
||||
{
|
||||
label: t('settings.fontFamilies.harmonyOS'),
|
||||
value: '"HarmonyOS Sans SC", "HarmonyOS Sans", "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial, sans-serif'
|
||||
},
|
||||
{
|
||||
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: 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'
|
||||
}
|
||||
];
|
||||
|
||||
// 常用字体选项
|
||||
export const FONT_OPTIONS = createFontOptions((key) => key);
|
||||
|
||||
// 获取浏览器的默认语言
|
||||
const getBrowserLanguage = (): SupportedLocaleType => {
|
||||
const browserLang = navigator.language;
|
||||
const langCode = browserLang.split('-')[0];
|
||||
|
||||
// 检查是否支持此语言
|
||||
const supportedLang = SUPPORTED_LOCALES.find(locale =>
|
||||
locale.code.startsWith(langCode) || locale.code.split('-')[0] === langCode
|
||||
);
|
||||
|
||||
return supportedLang?.code || 'zh-CN';
|
||||
};
|
||||
|
||||
// 默认配置
|
||||
const DEFAULT_CONFIG: AppConfig = {
|
||||
general: {
|
||||
alwaysOnTop: false,
|
||||
dataPath: '',
|
||||
enableSystemTray: true,
|
||||
startAtLogin: false,
|
||||
enableGlobalHotkey: false,
|
||||
globalHotkey: {
|
||||
ctrl: false,
|
||||
shift: false,
|
||||
alt: true,
|
||||
win: false,
|
||||
key: 'X'
|
||||
}
|
||||
},
|
||||
editing: {
|
||||
fontSize: CONFIG_LIMITS.fontSize.default,
|
||||
fontFamily: FONT_OPTIONS[0].value,
|
||||
fontWeight: 'normal',
|
||||
lineHeight: CONFIG_LIMITS.lineHeight.default,
|
||||
enableTabIndent: true,
|
||||
tabSize: CONFIG_LIMITS.tabSize.default,
|
||||
tabType: CONFIG_LIMITS.tabType.default,
|
||||
autoSaveDelay: 5000
|
||||
},
|
||||
appearance: {
|
||||
language: LanguageType.LangZhCN,
|
||||
systemTheme: SystemThemeType.SystemThemeAuto,
|
||||
customTheme: {
|
||||
darkTheme: {
|
||||
// 基础色调
|
||||
background: '#252B37',
|
||||
backgroundSecondary: '#213644',
|
||||
surface: '#474747',
|
||||
foreground: '#9BB586',
|
||||
foregroundSecondary: '#9c9c9c',
|
||||
|
||||
// 语法高亮
|
||||
comment: '#6272a4',
|
||||
keyword: '#ff79c6',
|
||||
string: '#f1fa8c',
|
||||
function: '#50fa7b',
|
||||
number: '#bd93f9',
|
||||
operator: '#ff79c6',
|
||||
variable: '#8fbcbb',
|
||||
type: '#8be9fd',
|
||||
|
||||
// 界面元素
|
||||
cursor: '#fff',
|
||||
selection: '#0865a9aa',
|
||||
selectionBlur: '#225377aa',
|
||||
activeLine: 'rgba(255,255,255,0.04)',
|
||||
lineNumber: 'rgba(255,255,255, 0.15)',
|
||||
activeLineNumber: 'rgba(255,255,255, 0.6)',
|
||||
|
||||
// 边框分割线
|
||||
borderColor: '#1e222a',
|
||||
borderLight: 'rgba(255,255,255, 0.1)',
|
||||
|
||||
// 搜索匹配
|
||||
searchMatch: '#8fbcbb',
|
||||
matchingBracket: 'rgba(255,255,255,0.1)'
|
||||
},
|
||||
lightTheme: {
|
||||
// 基础色调
|
||||
background: '#ffffff',
|
||||
backgroundSecondary: '#f1faf1',
|
||||
surface: '#f5f5f5',
|
||||
foreground: '#444d56',
|
||||
foregroundSecondary: '#6a737d',
|
||||
|
||||
// 语法高亮
|
||||
comment: '#6a737d',
|
||||
keyword: '#d73a49',
|
||||
string: '#032f62',
|
||||
function: '#005cc5',
|
||||
number: '#005cc5',
|
||||
operator: '#d73a49',
|
||||
variable: '#24292e',
|
||||
type: '#6f42c1',
|
||||
|
||||
// 界面元素
|
||||
cursor: '#000',
|
||||
selection: '#77baff8c',
|
||||
selectionBlur: '#b2c2ca85',
|
||||
activeLine: '#000000',
|
||||
lineNumber: '#000000',
|
||||
activeLineNumber: '#000000',
|
||||
|
||||
// 边框分割线
|
||||
borderColor: '#dfdfdf',
|
||||
borderLight: '#0000000C',
|
||||
|
||||
// 搜索匹配
|
||||
searchMatch: '#005cc5',
|
||||
matchingBracket: 'rgba(0,0,0,0.1)'
|
||||
}
|
||||
}
|
||||
},
|
||||
updates: {
|
||||
version: "1.0.0",
|
||||
autoUpdate: true,
|
||||
primarySource: UpdateSourceType.UpdateSourceGithub,
|
||||
backupSource: UpdateSourceType.UpdateSourceGitea,
|
||||
backupBeforeUpdate: true,
|
||||
updateTimeout: 30,
|
||||
github: {
|
||||
owner: "landaiqing",
|
||||
repo: "voidraft",
|
||||
},
|
||||
gitea: {
|
||||
baseURL: "https://git.landaiqing.cn",
|
||||
owner: "landaiqing",
|
||||
repo: "voidraft",
|
||||
}
|
||||
},
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastUpdated: new Date().toString(),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const useConfigStore = defineStore('config', () => {
|
||||
const {locale, t} = useI18n();
|
||||
|
||||
// 响应式状态
|
||||
const state = reactive({
|
||||
config: {...DEFAULT_CONFIG} as AppConfig,
|
||||
isLoading: false,
|
||||
configLoaded: false
|
||||
});
|
||||
|
||||
// 初始化FONT_OPTIONS国际化版本
|
||||
const localizedFontOptions = computed(() => createFontOptions(t));
|
||||
|
||||
// 计算属性 - 使用工厂函数简化
|
||||
const createLimitComputed = (key: NumberConfigKey) => computed(() => CONFIG_LIMITS[key]);
|
||||
const limits = Object.fromEntries(
|
||||
(['fontSize', 'tabSize', 'lineHeight'] as const).map(key => [key, createLimitComputed(key)])
|
||||
) as Record<NumberConfigKey, ReturnType<typeof createLimitComputed>>;
|
||||
|
||||
// 通用配置更新方法
|
||||
const updateGeneralConfig = async <K extends keyof GeneralConfig>(key: K, value: GeneralConfig[K]): Promise<void> => {
|
||||
// 确保配置已加载
|
||||
if (!state.configLoaded && !state.isLoading) {
|
||||
await initConfig();
|
||||
}
|
||||
|
||||
const backendKey = GENERAL_CONFIG_KEY_MAP[key];
|
||||
if (!backendKey) {
|
||||
throw new Error(`No backend key mapping found for general.${key.toString()}`);
|
||||
}
|
||||
|
||||
await ConfigService.Set(backendKey, value);
|
||||
state.config.general[key] = value;
|
||||
};
|
||||
|
||||
const updateEditingConfig = async <K extends keyof EditingConfig>(key: K, value: EditingConfig[K]): Promise<void> => {
|
||||
// 确保配置已加载
|
||||
if (!state.configLoaded && !state.isLoading) {
|
||||
await initConfig();
|
||||
}
|
||||
|
||||
const backendKey = EDITING_CONFIG_KEY_MAP[key];
|
||||
if (!backendKey) {
|
||||
throw new Error(`No backend key mapping found for editing.${key.toString()}`);
|
||||
}
|
||||
|
||||
await ConfigService.Set(backendKey, value);
|
||||
state.config.editing[key] = value;
|
||||
};
|
||||
|
||||
const updateAppearanceConfig = async <K extends keyof AppearanceConfig>(key: K, value: AppearanceConfig[K]): Promise<void> => {
|
||||
// 确保配置已加载
|
||||
if (!state.configLoaded && !state.isLoading) {
|
||||
await initConfig();
|
||||
}
|
||||
|
||||
const backendKey = APPEARANCE_CONFIG_KEY_MAP[key];
|
||||
if (!backendKey) {
|
||||
throw new Error(`No backend key mapping found for appearance.${key.toString()}`);
|
||||
}
|
||||
|
||||
await ConfigService.Set(backendKey, value);
|
||||
state.config.appearance[key] = value;
|
||||
};
|
||||
|
||||
const updateUpdatesConfig = async <K extends keyof UpdatesConfig>(key: K, value: UpdatesConfig[K]): Promise<void> => {
|
||||
// 确保配置已加载
|
||||
if (!state.configLoaded && !state.isLoading) {
|
||||
await initConfig();
|
||||
}
|
||||
|
||||
const backendKey = UPDATES_CONFIG_KEY_MAP[key];
|
||||
if (!backendKey) {
|
||||
throw new Error(`No backend key mapping found for updates.${key.toString()}`);
|
||||
}
|
||||
|
||||
await ConfigService.Set(backendKey, value);
|
||||
state.config.updates[key] = value;
|
||||
};
|
||||
|
||||
// 加载配置
|
||||
const initConfig = async (): Promise<void> => {
|
||||
if (state.isLoading) return;
|
||||
|
||||
state.isLoading = true;
|
||||
try {
|
||||
const appConfig = await ConfigService.GetConfig();
|
||||
|
||||
if (appConfig) {
|
||||
// 合并配置
|
||||
if (appConfig.general) Object.assign(state.config.general, appConfig.general);
|
||||
if (appConfig.editing) Object.assign(state.config.editing, appConfig.editing);
|
||||
if (appConfig.appearance) Object.assign(state.config.appearance, appConfig.appearance);
|
||||
if (appConfig.updates) Object.assign(state.config.updates, appConfig.updates);
|
||||
if (appConfig.metadata) Object.assign(state.config.metadata, appConfig.metadata);
|
||||
}
|
||||
|
||||
state.configLoaded = true;
|
||||
|
||||
// 初始化热键监听器
|
||||
const windowController = WindowController.getInstance();
|
||||
await windowController.initializeHotkeyListener();
|
||||
} finally {
|
||||
state.isLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 通用数值调整器工厂
|
||||
const createAdjuster = <T extends NumberConfigKey>(key: T) => {
|
||||
const limit = CONFIG_LIMITS[key];
|
||||
const clamp = (value: number) => ConfigUtils.clamp(value, limit.min, limit.max);
|
||||
|
||||
return {
|
||||
increase: async () => await updateEditingConfig(key, clamp(state.config.editing[key] + 1)),
|
||||
decrease: async () => await updateEditingConfig(key, clamp(state.config.editing[key] - 1)),
|
||||
set: async (value: number) => await updateEditingConfig(key, clamp(value)),
|
||||
reset: async () => await updateEditingConfig(key, limit.default)
|
||||
};
|
||||
};
|
||||
|
||||
// 通用布尔值切换器
|
||||
const createGeneralToggler = <T extends keyof GeneralConfig>(key: T) =>
|
||||
async () => await updateGeneralConfig(key, !state.config.general[key] as GeneralConfig[T]);
|
||||
|
||||
const createEditingToggler = <T extends keyof EditingConfig>(key: T) =>
|
||||
async () => await updateEditingConfig(key, !state.config.editing[key] as EditingConfig[T]);
|
||||
|
||||
// 枚举值切换器
|
||||
const createEnumToggler = <T extends TabType>(key: 'tabType', values: readonly T[]) =>
|
||||
async () => {
|
||||
const currentIndex = values.indexOf(state.config.editing[key] as T);
|
||||
const nextIndex = (currentIndex + 1) % values.length;
|
||||
return await updateEditingConfig(key, values[nextIndex]);
|
||||
};
|
||||
|
||||
// 重置配置
|
||||
const resetConfig = async (): Promise<void> => {
|
||||
if (state.isLoading) return;
|
||||
|
||||
state.isLoading = true;
|
||||
try {
|
||||
|
||||
await ConfigService.ResetConfig()
|
||||
const appConfig = await ConfigService.GetConfig();
|
||||
if (appConfig) {
|
||||
state.config = JSON.parse(JSON.stringify(appConfig)) as AppConfig;
|
||||
}
|
||||
} finally {
|
||||
state.isLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 语言设置方法
|
||||
const setLanguage = async (language: LanguageType): Promise<void> => {
|
||||
await updateAppearanceConfig('language', language);
|
||||
|
||||
// 同步更新前端语言
|
||||
const frontendLocale = ConfigUtils.backendLanguageToFrontend(language);
|
||||
locale.value = frontendLocale as any;
|
||||
};
|
||||
|
||||
// 系统主题设置方法
|
||||
const setSystemTheme = async (systemTheme: SystemThemeType): Promise<void> => {
|
||||
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 {
|
||||
// 如果配置未加载,先加载配置
|
||||
if (!state.configLoaded) {
|
||||
await initConfig();
|
||||
}
|
||||
|
||||
// 同步前端语言设置
|
||||
const frontendLocale = ConfigUtils.backendLanguageToFrontend(state.config.appearance.language);
|
||||
locale.value = frontendLocale as any;
|
||||
} catch (error) {
|
||||
const browserLang = getBrowserLanguage();
|
||||
locale.value = browserLang as any;
|
||||
}
|
||||
};
|
||||
|
||||
// 创建数值调整器实例
|
||||
const adjusters = {
|
||||
fontSize: createAdjuster('fontSize'),
|
||||
tabSize: createAdjuster('tabSize'),
|
||||
lineHeight: createAdjuster('lineHeight')
|
||||
};
|
||||
|
||||
// 创建切换器实例
|
||||
const togglers = {
|
||||
tabIndent: createEditingToggler('enableTabIndent'),
|
||||
alwaysOnTop: async () => {
|
||||
await updateGeneralConfig('alwaysOnTop', !state.config.general.alwaysOnTop);
|
||||
// 立即应用窗口置顶状态
|
||||
await runtime.Window.SetAlwaysOnTop(state.config.general.alwaysOnTop);
|
||||
},
|
||||
tabType: createEnumToggler('tabType', CONFIG_LIMITS.tabType.values)
|
||||
};
|
||||
|
||||
// 字符串配置设置器
|
||||
const setters = {
|
||||
fontFamily: async (value: string) => await updateEditingConfig('fontFamily', value),
|
||||
fontWeight: async (value: string) => await updateEditingConfig('fontWeight', value),
|
||||
dataPath: async (value: string) => await updateGeneralConfig('dataPath', value),
|
||||
autoSaveDelay: async (value: number) => await updateEditingConfig('autoSaveDelay', value)
|
||||
};
|
||||
|
||||
return {
|
||||
// 状态
|
||||
config: computed(() => state.config),
|
||||
configLoaded: computed(() => state.configLoaded),
|
||||
isLoading: computed(() => state.isLoading),
|
||||
localizedFontOptions,
|
||||
|
||||
// 限制常量
|
||||
...limits,
|
||||
|
||||
// 核心方法
|
||||
initConfig,
|
||||
resetConfig,
|
||||
|
||||
// 语言相关方法
|
||||
setLanguage,
|
||||
initializeLanguage,
|
||||
|
||||
// 主题相关方法
|
||||
setSystemTheme,
|
||||
updateCustomTheme,
|
||||
setCustomTheme,
|
||||
|
||||
// 字体大小操作
|
||||
...adjusters.fontSize,
|
||||
increaseFontSize: adjusters.fontSize.increase,
|
||||
decreaseFontSize: adjusters.fontSize.decrease,
|
||||
resetFontSize: adjusters.fontSize.reset,
|
||||
setFontSize: adjusters.fontSize.set,
|
||||
|
||||
// Tab操作
|
||||
toggleTabIndent: togglers.tabIndent,
|
||||
setEnableTabIndent: (value: boolean) => updateEditingConfig('enableTabIndent', value),
|
||||
...adjusters.tabSize,
|
||||
increaseTabSize: adjusters.tabSize.increase,
|
||||
decreaseTabSize: adjusters.tabSize.decrease,
|
||||
setTabSize: adjusters.tabSize.set,
|
||||
toggleTabType: togglers.tabType,
|
||||
|
||||
// 行高操作
|
||||
setLineHeight: adjusters.lineHeight.set,
|
||||
|
||||
// 窗口操作
|
||||
toggleAlwaysOnTop: togglers.alwaysOnTop,
|
||||
setAlwaysOnTop: (value: boolean) => updateGeneralConfig('alwaysOnTop', value),
|
||||
|
||||
// 字体操作
|
||||
setFontFamily: setters.fontFamily,
|
||||
setFontWeight: setters.fontWeight,
|
||||
|
||||
// 路径操作
|
||||
setDataPath: setters.dataPath,
|
||||
|
||||
// 保存配置相关方法
|
||||
setAutoSaveDelay: setters.autoSaveDelay,
|
||||
|
||||
// 热键配置相关方法
|
||||
setEnableGlobalHotkey: (value: boolean) => updateGeneralConfig('enableGlobalHotkey', value),
|
||||
setGlobalHotkey: (hotkey: any) => updateGeneralConfig('globalHotkey', hotkey),
|
||||
|
||||
// 系统托盘配置相关方法
|
||||
setEnableSystemTray: (value: boolean) => updateGeneralConfig('enableSystemTray', value),
|
||||
|
||||
// 开机启动配置相关方法
|
||||
setStartAtLogin: async (value: boolean) => {
|
||||
// 先更新配置文件
|
||||
await updateGeneralConfig('startAtLogin', value);
|
||||
// 再调用系统设置API
|
||||
await StartupService.SetEnabled(value);
|
||||
},
|
||||
|
||||
// 更新配置相关方法
|
||||
setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value)
|
||||
};
|
||||
});
|
251
frontend/src/stores/documentStore.ts
Normal file
251
frontend/src/stores/documentStore.ts
Normal file
@@ -0,0 +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 documents = ref<Record<number, Document>>({});
|
||||
const recentDocumentIds = ref<number[]>([SCRATCH_DOCUMENT_ID]);
|
||||
const currentDocumentId = ref<number | null>(null);
|
||||
const currentDocument = ref<Document | null>(null);
|
||||
|
||||
// === UI状态 ===
|
||||
const showDocumentSelector = ref(false);
|
||||
const isLoading = ref(false);
|
||||
|
||||
// === 计算属性 ===
|
||||
const documentList = computed(() =>
|
||||
Object.values(documents.value).sort((a, b) => {
|
||||
const aIndex = recentDocumentIds.value.indexOf(a.id);
|
||||
const bIndex = recentDocumentIds.value.indexOf(b.id);
|
||||
|
||||
// 按最近使用排序
|
||||
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']
|
||||
}
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user