Compare commits
99 Commits
e8b5478bec
...
master
Author | SHA1 | Date | |
---|---|---|---|
f37c659c89 | |||
9fff7bcfca | |||
b4b0ad9bba | |||
6d8fdf62f1 | |||
9f53d7421d | |||
80c8ecb4cf | |||
d10059a82d | |||
737f83cd5f | |||
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 | |||
ab0255a775 | |||
72c2666932 |
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.
|
174
README.md
174
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
|
||||
|
||||
```
|
||||
wails3 dev
|
||||
```
|
||||
### Developer-Friendly
|
||||
|
||||
This will start your application and enable hot-reloading for both frontend and backend changes.
|
||||
- Multi-language code block support - Syntax highlighting for 30+ programming languages
|
||||
- Smart language detection - Automatically recognizes code block language types
|
||||
- Code formatting - Built-in Prettier support for one-click code beautification
|
||||
- Block editing mode - Split content into independent code blocks, each with different language settings
|
||||
- Multi-window support - edit multiple documents at the same time
|
||||
- Support for custom themes - Custom editor themes
|
||||
|
||||
3. To build your application for production, use:
|
||||
### Modern Interface
|
||||
|
||||
```
|
||||
wails3 build
|
||||
```
|
||||
- Clean and elegant design - Simple yet sophisticated user interface
|
||||
- Dark/Light themes - Adapts to different usage scenarios
|
||||
- Multi-language support - Built-in internationalization with multiple language switching
|
||||
|
||||
This will create a production-ready executable in the `build` directory.
|
||||
### Extension System
|
||||
|
||||
## Exploring Wails3 Features
|
||||
- Modular architecture - Extensible editor based on CodeMirror 6
|
||||
- Rich extensions - Includes various practical editor extensions
|
||||
- Rainbow bracket matching
|
||||
- VSCode-style search and replace
|
||||
- Color picker
|
||||
- Built-in translation tool
|
||||
- Text highlighting
|
||||
- Code folding
|
||||
- Hyperlink support
|
||||
- Checkbox support
|
||||
- Minimap
|
||||
|
||||
Now that you have your project set up, it's time to explore the features that Wails3 offers:
|
||||
## Quick Start
|
||||
|
||||
1. **Check out the examples**: The best way to learn is by example. Visit the `examples` directory in the `v3/examples` directory to see various sample applications.
|
||||
### Download and Use
|
||||
|
||||
2. **Run an example**: To run any of the examples, navigate to the example's directory and use:
|
||||
1. Visit the releases page: https://github.com/landaiqing/voidraft/releases
|
||||
2. Select and download the appropriate version
|
||||
3. Run the installer and start using
|
||||
|
||||
```
|
||||
go run .
|
||||
```
|
||||
### Development Environment
|
||||
|
||||
Note: Some examples may be under development during the alpha phase.
|
||||
```bash
|
||||
# Clone the project
|
||||
git clone https://github.com/landaiqing/voidraft
|
||||
cd voidraft
|
||||
|
||||
3. **Explore the documentation**: Visit the [Wails3 documentation](https://v3alpha.wails.io/) for in-depth guides and API references.
|
||||
# Install frontend dependencies
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
cd ..
|
||||
|
||||
4. **Join the community**: Have questions or want to share your progress? Join the [Wails Discord](https://discord.gg/JDdSxwjhGf) or visit the [Wails discussions on GitHub](https://github.com/wailsapp/wails/discussions).
|
||||
# Start development server
|
||||
wails3 dev
|
||||
```
|
||||
|
||||
### Production Build
|
||||
|
||||
```bash
|
||||
# Build application
|
||||
wails3 package
|
||||
```
|
||||
|
||||
After building, the executable will be generated in the `bin` directory.
|
||||
|
||||
## Technical Architecture
|
||||
|
||||
### Core Technologies
|
||||
|
||||
| Layer | Technology Stack |
|
||||
|-------|------------------|
|
||||
| Desktop Framework | Wails3 |
|
||||
| Backend Language | Go 1.21+ |
|
||||
| Frontend Framework | Vue 3 + TypeScript |
|
||||
| Editor Core | CodeMirror 6 |
|
||||
| Build Tool | Vite |
|
||||
| Data Storage | SQLite |
|
||||
|
||||
## Project Structure
|
||||
|
||||
Take a moment to familiarize yourself with your project structure:
|
||||
```
|
||||
Voidraft/
|
||||
├── frontend/ # Vue 3 frontend application
|
||||
│ ├── src/
|
||||
│ │ ├── views/editor/ # Editor core views
|
||||
│ │ │ ├── extensions/ # Editor extension system
|
||||
│ │ │ │ ├── codeblock/ # Code block support
|
||||
│ │ │ │ ├── translator/ # Translation tool
|
||||
│ │ │ │ ├── minimap/ # Code minimap
|
||||
│ │ │ │ ├── vscodeSearch/ # VSCode-style search
|
||||
│ │ │ │ └── ... # More extensions
|
||||
│ │ │ └── ...
|
||||
│ │ ├── components/ # Reusable components
|
||||
│ │ ├── stores/ # Pinia state management
|
||||
│ │ └── utils/ # Utility functions
|
||||
│ └── public/ # Static assets
|
||||
├── internal/ # Go backend core
|
||||
│ ├── services/ # Core business services
|
||||
│ ├── models/ # Data model definitions
|
||||
│ └── events/ # Event handling mechanisms
|
||||
└── main.go # Application entry point
|
||||
```
|
||||
|
||||
- `frontend/`: Contains your frontend code (HTML, CSS, JavaScript/TypeScript)
|
||||
- `main.go`: The entry point of your Go backend
|
||||
- `app.go`: Define your application structure and methods here
|
||||
- `wails.json`: Configuration file for your Wails project
|
||||
## Development Roadmap
|
||||
|
||||
## Next Steps
|
||||
### Platform Extension Plans
|
||||
|
||||
1. Modify the frontend in the `frontend/` directory to create your desired UI.
|
||||
2. Add backend functionality in `main.go`.
|
||||
3. Use `wails3 dev` to see your changes in real-time.
|
||||
4. When ready, build your application with `wails3 build`.
|
||||
| Platform | Status | Expected Time |
|
||||
|----------|--------|---------------|
|
||||
| macOS | Planned | Future versions |
|
||||
| Linux | Planned | Future versions |
|
||||
|
||||
Happy coding with Wails3! If you encounter any issues or have questions, don't hesitate to consult the documentation or reach out to the Wails community.
|
||||
### Planned Features
|
||||
- ✅ Custom themes - Customize editor themes
|
||||
- ✅ Multi-window support - Support editing multiple documents simultaneously
|
||||
- ✅ Data synchronization - Cloud backup for documents
|
||||
- [ ] Enhanced clipboard - Monitor and manage clipboard history
|
||||
- [ ] Extension system - Support for custom plugins
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
> Standing on the shoulders of giants, paying tribute to the open source spirit
|
||||
|
||||
The birth of VoidRaft is inseparable from the following excellent open source projects:
|
||||
|
||||
### Special Thanks
|
||||
|
||||
- **[Heynote](https://github.com/heyman/heynote/)** - VoidRaft is a feature-enhanced version based on Heynote's concept
|
||||
- Inherits Heynote's elegant block editing philosophy
|
||||
- Adds more practical features on the original foundation
|
||||
- Rebuilt with modern technology stack
|
||||
- Provides richer extension system and customization options
|
||||
- Thanks to the Heynote team for bringing innovative ideas to the developer community
|
||||
|
||||
### Core Technology Stack
|
||||
|
||||
| Technology | Purpose | Link |
|
||||
|------------|---------|------|
|
||||
| Wails3 | Cross-platform desktop application framework | [wails.io](https://v3alpha.wails.io/) |
|
||||
| Vue.js | Progressive frontend framework | [vuejs.org](https://vuejs.org/) |
|
||||
| CodeMirror 6 | Modern code editor | [codemirror.net](https://codemirror.net/) |
|
||||
| Prettier | Code formatting tool | [prettier.io](https://prettier.io/) |
|
||||
| TypeScript | Type-safe JavaScript | [typescriptlang.org](https://www.typescriptlang.org/) |
|
||||
|
||||
## License
|
||||
|
||||
This project is open source under the [MIT License](LICENSE).
|
||||
|
||||
Welcome to Fork, Star, and contribute code.
|
||||
|
||||
[](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="*"/>
|
||||
|
1
docs/CNAME
Normal file
1
docs/CNAME
Normal file
@@ -0,0 +1 @@
|
||||
voidraft.landaiqing.cn
|
75
docs/changelog.html
Normal file
75
docs/changelog.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>VoidRaft - Changelog</title>
|
||||
<link rel="stylesheet" href="css/styles.css">
|
||||
<link rel="stylesheet" href="css/changelog.css">
|
||||
<link rel="icon" href="img/favicon.ico">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body class="theme-dark">
|
||||
<div class="container">
|
||||
<!-- 主卡片 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1 class="card-title" data-en="VoidRaft Changelog" data-zh="VoidRaft 更新日志">VoidRaft Changelog</h1>
|
||||
<div class="card-controls">
|
||||
<button id="theme-toggle" class="btn btn-secondary" title="切换主题">
|
||||
<i class="fas fa-sun"></i> <span data-en="Theme" data-zh="主题">Theme</span>
|
||||
</button>
|
||||
<button id="lang-toggle" class="btn btn-secondary">
|
||||
<i class="fas fa-language"></i> 中/EN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<!-- 导航区域 -->
|
||||
<div class="nav-links">
|
||||
<a href="index.html" class="btn btn-secondary">
|
||||
<i class="fas fa-home"></i> <span data-en="Home" data-zh="首页">Home</span>
|
||||
</a>
|
||||
<a href="https://github.com/landaiqing/voidraft" class="btn btn-secondary">
|
||||
<i class="fab fa-github"></i> <span data-en="Source Code" data-zh="源代码">Source Code</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 加载中提示 -->
|
||||
<div id="loading" class="loading-container">
|
||||
<div class="loading-spinner"></div>
|
||||
<p data-en="Loading releases..." data-zh="正在加载版本信息...">Loading releases...</p>
|
||||
</div>
|
||||
|
||||
<!-- 更新日志内容 -->
|
||||
<div id="changelog" class="changelog-container">
|
||||
<!-- 通过JavaScript动态填充内容 -->
|
||||
</div>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<div id="error-message" class="error-container" style="display: none;">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<p data-en="Failed to load release information. Please try again later."
|
||||
data-zh="加载版本信息失败,请稍后再试。">Failed to load release information. Please try again later.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="footer">
|
||||
<p class="footer-text" data-en="© 2025 VoidRaft - An elegant text snippet recording tool designed for developers" data-zh="© 2025 VoidRaft - 专为开发者打造的优雅文本片段记录工具">© 2023-2024 VoidRaft - An elegant text snippet recording tool designed for developers</p>
|
||||
<div class="footer-links">
|
||||
<a href="https://github.com/landaiqing/voidraft" target="_blank" class="footer-link">GitHub</a>
|
||||
<a href="https://github.com/landaiqing/voidraft/issues" target="_blank" class="footer-link" data-en="Issues" data-zh="问题反馈">Issues</a>
|
||||
<a href="https://github.com/landaiqing/voidraft/releases" target="_blank" class="footer-link" data-en="Releases" data-zh="版本发布">Releases</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/script.js"></script>
|
||||
<script src="js/changelog.js"></script>
|
||||
</body>
|
||||
</html>
|
347
docs/css/changelog.css
Normal file
347
docs/css/changelog.css
Normal file
@@ -0,0 +1,347 @@
|
||||
/* 更新日志页面样式 */
|
||||
.nav-links {
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||
border-left-color: var(--primary-color);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
.theme-dark .loading-spinner {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
border-left-color: var(--primary-color);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-container {
|
||||
text-align: center;
|
||||
color: var(--error-color);
|
||||
padding: 20px;
|
||||
border: 2px dashed var(--error-color);
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(var(--card-bg-rgb), 0.7);
|
||||
}
|
||||
|
||||
.error-container i {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* 更新日志容器 */
|
||||
.changelog-container {
|
||||
display: none;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.release {
|
||||
margin-bottom: 40px;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
padding-left: 20px;
|
||||
background-color: rgba(var(--card-bg-rgb), 0.5);
|
||||
padding: 15px 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.release-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.release-version {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.release-date {
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.release-badge {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
background-color: var(--primary-color);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.release-badge.pre-release {
|
||||
background-color: var(--warning-color);
|
||||
}
|
||||
|
||||
.release-description {
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.release-assets {
|
||||
background-color: rgba(var(--light-bg-rgb), 0.7);
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.release-assets-title {
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.asset-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.asset-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
.asset-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.asset-icon {
|
||||
margin-right: 10px;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.asset-name {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.asset-size {
|
||||
font-size: 12px;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 资源下载按钮 */
|
||||
.download-btn {
|
||||
margin-left: 10px;
|
||||
padding: 3px 10px;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.download-btn:hover {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.markdown-content {
|
||||
line-height: 1.8;
|
||||
overflow-wrap: break-word;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.markdown-content h1,
|
||||
.markdown-content h2,
|
||||
.markdown-content h3 {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.markdown-content ul,
|
||||
.markdown-content ol {
|
||||
padding-left: 20px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.markdown-content li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.markdown-content li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown-content hr {
|
||||
border: none;
|
||||
border-top: 2px dashed var(--border-color);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.markdown-content br {
|
||||
display: block;
|
||||
content: "";
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.markdown-content code {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
background-color: rgba(128, 128, 128, 0.1);
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
background-color: rgba(128, 128, 128, 0.1);
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.markdown-content pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-content a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.data-source {
|
||||
padding: 10px 15px;
|
||||
margin-bottom: 20px;
|
||||
background-color: rgba(var(--light-bg-rgb), 0.7);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.data-source a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.data-source a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Markdown内容样式增强 */
|
||||
.markdown-content blockquote {
|
||||
border-left: 4px solid var(--primary-color);
|
||||
padding: 10px 15px;
|
||||
margin: 15px 0;
|
||||
background-color: rgba(var(--light-bg-rgb), 0.5);
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.markdown-content ul,
|
||||
.markdown-content ol {
|
||||
padding-left: 20px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* 移动设备响应式优化 */
|
||||
@media (max-width: 768px) {
|
||||
.release-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.release-assets {
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.asset-item {
|
||||
flex-wrap: wrap;
|
||||
padding: 12px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.asset-name {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.asset-size {
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
margin-left: 10px;
|
||||
padding: 5px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.release {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.asset-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.asset-icon {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.asset-size {
|
||||
margin-left: 0;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
margin-left: 0;
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保日志页面页脚样式一致 */
|
||||
.footer {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
margin: 0 0 15px 0;
|
||||
}
|
45
docs/css/ibm-plex-mono-font.css
Normal file
45
docs/css/ibm-plex-mono-font.css
Normal file
@@ -0,0 +1,45 @@
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iIq129k.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1isq129k.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iAq129k.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iEq129k.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1i8q1w.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
27
docs/css/space-mono-font.css
Normal file
27
docs/css/space-mono-font.css
Normal file
@@ -0,0 +1,27 @@
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYE58RWq7.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYE98RWq7.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Space Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../font/space-mono/i7dPIFZifjKcF5UAWdDRYEF8RQ.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
717
docs/css/styles.css
Normal file
717
docs/css/styles.css
Normal file
@@ -0,0 +1,717 @@
|
||||
@import url('./space-mono-font.css');
|
||||
@import url('./ibm-plex-mono-font.css');
|
||||
|
||||
/* 浅色主题 */
|
||||
:root {
|
||||
--bg-color: #fefefe;
|
||||
--text-color: #000000;
|
||||
--primary-color: #F08080;
|
||||
--primary-color-rgb: 240, 128, 128;
|
||||
--secondary-color: #ff006e;
|
||||
--accent-color: #073B4C;
|
||||
--card-bg: #ffffff;
|
||||
--card-bg-rgb: 255, 255, 255;
|
||||
--border-color: #000000;
|
||||
--light-bg: #f0f0f0;
|
||||
--light-bg-rgb: 240, 240, 240;
|
||||
--shadow-color: rgba(240, 128, 128, 0.5);
|
||||
--success-color: #27c93f;
|
||||
--warning-color: #FFD166;
|
||||
--error-color: #ff006e;
|
||||
--info-color: #118ab2;
|
||||
--code-bg: #ffffff;
|
||||
--code-bg-rgb: 255, 255, 255;
|
||||
--preview-header-bg: #f0f0f0;
|
||||
--preview-header-bg-rgb: 240, 240, 240;
|
||||
--grid-color-1: rgba(0, 0, 0, 0.08);
|
||||
--grid-color-2: rgba(0, 0, 0, 0.05);
|
||||
--header-title-color: #000000;
|
||||
}
|
||||
|
||||
/* 暗色主题变量 */
|
||||
.theme-dark {
|
||||
--bg-color: #121212;
|
||||
--text-color: #ffffff;
|
||||
--primary-color: #F08080;
|
||||
--primary-color-rgb: 240, 128, 128;
|
||||
--secondary-color: #ff006e;
|
||||
--accent-color: #118ab2;
|
||||
--card-bg: #1e1e1e;
|
||||
--card-bg-rgb: 30, 30, 30;
|
||||
--border-color: #ffffff;
|
||||
--light-bg: #2a2a2a;
|
||||
--light-bg-rgb: 42, 42, 42;
|
||||
--shadow-color: rgba(240, 128, 128, 0.5);
|
||||
--success-color: #27c93f;
|
||||
--warning-color: #FFD166;
|
||||
--error-color: #ff006e;
|
||||
--info-color: #118ab2;
|
||||
--code-bg: #1e1e1e;
|
||||
--code-bg-rgb: 30, 30, 30;
|
||||
--preview-header-bg: #252526;
|
||||
--preview-header-bg-rgb: 37, 37, 38;
|
||||
--grid-color-1: rgba(255, 255, 255, 0.08);
|
||||
--grid-color-2: rgba(255, 255, 255, 0.05);
|
||||
--header-title-color: #000000;
|
||||
}
|
||||
|
||||
/* 主题切换和语言切换的过渡效果 */
|
||||
.theme-transition,
|
||||
.theme-transition *,
|
||||
.lang-transition,
|
||||
.lang-transition * {
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@keyframes gridMove {
|
||||
0% {
|
||||
background-position: 0px 0px, 0px 0px, 0px 0px, 0px 0px;
|
||||
}
|
||||
100% {
|
||||
background-position: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
background-image:
|
||||
linear-gradient(var(--grid-color-1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--grid-color-1) 1px, transparent 1px),
|
||||
linear-gradient(var(--grid-color-2) 0.5px, transparent 0.5px),
|
||||
linear-gradient(90deg, var(--grid-color-2) 0.5px, transparent 0.5px);
|
||||
background-size: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
|
||||
background-position: center;
|
||||
animation: gridMove 40s linear infinite;
|
||||
font-family: 'Space Mono', monospace;
|
||||
color: var(--text-color);
|
||||
line-height: 1.6;
|
||||
padding: 20px;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 卡片容器 */
|
||||
.card {
|
||||
background-color: var(--card-bg);
|
||||
background-image:
|
||||
linear-gradient(var(--grid-color-1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--grid-color-1) 1px, transparent 1px),
|
||||
linear-gradient(var(--grid-color-2) 0.5px, transparent 0.5px),
|
||||
linear-gradient(90deg, var(--grid-color-2) 0.5px, transparent 0.5px);
|
||||
background-size: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
|
||||
background-position: center;
|
||||
border: 4px solid var(--border-color);
|
||||
box-shadow: 12px 12px 0 var(--shadow-color);
|
||||
margin-bottom: 40px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 16px 16px 0 var(--shadow-color);
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
.card-header {
|
||||
background-color: rgba(var(--primary-color-rgb), 0.9);
|
||||
border-bottom: 4px solid var(--border-color);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
color: var(--header-title-color);
|
||||
}
|
||||
|
||||
.card-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background: var(--secondary-color);
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
border: 3px solid var(--border-color);
|
||||
box-shadow: 4px 4px 0 var(--shadow-color);
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: var(--card-bg);
|
||||
color: var(--primary-color);
|
||||
border: 3px solid var(--primary-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--light-bg);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--card-bg);
|
||||
color: var(--primary-color);
|
||||
border: 3px solid var(--primary-color);
|
||||
}
|
||||
|
||||
/* 卡片内容 */
|
||||
.card-content {
|
||||
padding: 30px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-color: rgba(var(--card-bg-rgb), 0.5);
|
||||
}
|
||||
|
||||
/* Logo区域 */
|
||||
.logo-container {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.logo-frame {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
background: var(--card-bg);
|
||||
border: 4px solid var(--border-color);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
object-fit: contain;
|
||||
border: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 16px;
|
||||
margin: 10px 0 0;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* 介绍区域 */
|
||||
.intro-box {
|
||||
border: 2px dashed var(--border-color);
|
||||
padding: 20px;
|
||||
background-color: rgba(var(--light-bg-rgb), 0.7);
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
font-size: 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 按钮组 */
|
||||
.button-group {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
/* 特性网格 */
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 30px;
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
/* 特性卡片 */
|
||||
.feature-card {
|
||||
background-color: rgba(var(--card-bg-rgb), 0.8);
|
||||
border: 3px solid var(--border-color);
|
||||
box-shadow: 5px 5px 0 var(--shadow-color);
|
||||
padding: 20px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 7px 7px 0 var(--shadow-color);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 15px;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 预览区域 */
|
||||
.preview-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.preview-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 预览窗口 */
|
||||
.preview-window {
|
||||
border: 3px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 10px;
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
background-color: rgba(var(--card-bg-rgb), 0.7);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 5px 5px 0 var(--shadow-color);
|
||||
}
|
||||
|
||||
/* 预览头部 */
|
||||
.preview-header {
|
||||
background-color: rgba(var(--preview-header-bg-rgb), 0.9);
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.preview-controls {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.preview-btn {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.preview-btn:nth-child(1) {
|
||||
background-color: #ff5f56;
|
||||
}
|
||||
|
||||
.preview-btn:nth-child(2) {
|
||||
background-color: #ffbd2e;
|
||||
}
|
||||
|
||||
.preview-btn:nth-child(3) {
|
||||
background-color: #27c93f;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 13px;
|
||||
opacity: 0.8;
|
||||
color: var(--text-color);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* 预览内容 */
|
||||
.preview-content {
|
||||
padding: 15px;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
background-color: rgba(var(--code-bg-rgb), 0.5);
|
||||
}
|
||||
|
||||
/* 代码块容器 */
|
||||
.code-block-wrapper {
|
||||
background-color: rgba(var(--code-bg-rgb), 0.8);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 块头部 */
|
||||
.block-header {
|
||||
background-color: rgba(var(--light-bg-rgb), 0.8);
|
||||
padding: 8px 12px;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.block-language {
|
||||
color: rgba(128, 128, 128, 0.8);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.block-language::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 5px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23888'%3E%3Cpath d='M9.7,16.7L5.3,12.3C4.9,11.9 4.9,11.1 5.3,10.7C5.7,10.3 6.3,10.3 6.7,10.7L10.5,14.5L17.3,7.7C17.7,7.3 18.3,7.3 18.7,7.7C19.1,8.1 19.1,8.7 18.7,9.1L11.3,16.7C10.9,17.1 10.1,17.1 9.7,16.7Z'/%3E%3C/svg%3E");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
tab-size: 4;
|
||||
-moz-tab-size: 4;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.theme-dark .code-block-wrapper {
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.theme-dark .block-header {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.theme-dark .block-language {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.theme-dark .block-language::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23aaa'%3E%3Cpath d='M9.7,16.7L5.3,12.3C4.9,11.9 4.9,11.1 5.3,10.7C5.7,10.3 6.3,10.3 6.7,10.7L10.5,14.5L17.3,7.7C17.7,7.3 18.3,7.3 18.7,7.7C19.1,8.1 19.1,8.7 18.7,9.1L11.3,16.7C10.9,17.1 10.1,17.1 9.7,16.7Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.theme-dark .code-block {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
/* 代码高亮 */
|
||||
.theme-dark .keyword { color: #c586c0; }
|
||||
.theme-dark .function { color: #dcdcaa; }
|
||||
.theme-dark .variable { color: #9cdcfe; }
|
||||
.theme-dark .string { color: #ce9178; }
|
||||
.theme-dark .comment { color: #6a9955; }
|
||||
.theme-dark .class { color: #4ec9b0; }
|
||||
.theme-dark .parameter { color: #9cdcfe; }
|
||||
.theme-dark .built-in { color: #4ec9b0; }
|
||||
|
||||
/* 浅色主题代码高亮 */
|
||||
.keyword { color: #af00db; }
|
||||
.function { color: #795e26; }
|
||||
.variable { color: #001080; }
|
||||
.string { color: #a31515; }
|
||||
.comment { color: #008000; }
|
||||
.class { color: #267f99; }
|
||||
.parameter { color: #001080; }
|
||||
.built-in { color: #267f99; }
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
border: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.theme-dark .light-theme-img {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.theme-dark .dark-theme-img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body:not(.theme-dark) .dark-theme-img {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body:not(.theme-dark) .light-theme-img {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* 技术栈列表 */
|
||||
.tech-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 技术栈列表 */
|
||||
.tech-item {
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
border: 2px solid var(--border-color);
|
||||
background-color: rgba(var(--light-bg-rgb), 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tech-icon {
|
||||
margin-right: 15px;
|
||||
color: var(--secondary-color);
|
||||
font-size: 20px;
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tech-name {
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.tech-desc {
|
||||
font-size: 14px;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* 页脚 */
|
||||
.footer {
|
||||
border-top: 2px solid var(--border-color);
|
||||
padding: 20px 0;
|
||||
margin-top: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: var(--secondary-color);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.card-controls {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo-frame {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 针对移动设备的响应式优化 */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-controls {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* 预览区域优化 */
|
||||
.preview-content {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.block-header {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
/* 日志界面导航链接优化 */
|
||||
.nav-links {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.nav-links .btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
/* 特性卡片优化 */
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 预览窗口优化 */
|
||||
.preview-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.preview-window {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 技术栈列表小屏幕优化 */
|
||||
.tech-item {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tech-desc {
|
||||
width: 100%;
|
||||
padding-left: 40px; /* 图标宽度+右边距 */
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* 日志界面资源列表项优化 */
|
||||
.asset-item {
|
||||
flex-wrap: wrap;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.asset-name {
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.asset-size {
|
||||
order: 2;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
order: 3;
|
||||
margin-left: 0;
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* 页脚链接优化 */
|
||||
.footer {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
margin-top: 15px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
BIN
docs/font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1i8q1w.woff2
Normal file
BIN
docs/font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1i8q1w.woff2
Normal file
Binary file not shown.
BIN
docs/font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iAq129k.woff2
Normal file
BIN
docs/font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iAq129k.woff2
Normal file
Binary file not shown.
BIN
docs/font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iEq129k.woff2
Normal file
BIN
docs/font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iEq129k.woff2
Normal file
Binary file not shown.
BIN
docs/font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iIq129k.woff2
Normal file
BIN
docs/font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1iIq129k.woff2
Normal file
Binary file not shown.
BIN
docs/font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1isq129k.woff2
Normal file
BIN
docs/font/ibm-plex-mono/-F63fjptAgt5VM-kVkqdyU8n1isq129k.woff2
Normal file
Binary file not shown.
BIN
docs/font/space-mono/i7dPIFZifjKcF5UAWdDRYE58RWq7.woff2
Normal file
BIN
docs/font/space-mono/i7dPIFZifjKcF5UAWdDRYE58RWq7.woff2
Normal file
Binary file not shown.
BIN
docs/font/space-mono/i7dPIFZifjKcF5UAWdDRYE98RWq7.woff2
Normal file
BIN
docs/font/space-mono/i7dPIFZifjKcF5UAWdDRYE98RWq7.woff2
Normal file
Binary file not shown.
BIN
docs/font/space-mono/i7dPIFZifjKcF5UAWdDRYEF8RQ.woff2
Normal file
BIN
docs/font/space-mono/i7dPIFZifjKcF5UAWdDRYEF8RQ.woff2
Normal file
Binary file not shown.
BIN
docs/img/favicon.ico
Normal file
BIN
docs/img/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
BIN
docs/img/logo.png
Normal file
BIN
docs/img/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
BIN
docs/img/screenshot-dark.png
Normal file
BIN
docs/img/screenshot-dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
BIN
docs/img/screenshot-light.png
Normal file
BIN
docs/img/screenshot-light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
256
docs/index.html
Normal file
256
docs/index.html
Normal file
@@ -0,0 +1,256 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>VoidRaft - An elegant text snippet recording tool designed for developers.</title>
|
||||
<meta name="description" content="VoidRaft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
|
||||
<meta name="keywords" content="text editor, code snippets, developer tools, syntax highlighting, code formatting, multi-language, VoidRaft">
|
||||
<meta name="author" content="VoidRaft Team">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="https://landaiqing.github.io/voidraft/">
|
||||
|
||||
<!-- Internationalization / hreflang -->
|
||||
<link rel="alternate" hreflang="en" href="https://landaiqing.github.io/voidraft/">
|
||||
<link rel="alternate" hreflang="zh" href="https://landaiqing.github.io/voidraft/?lang=zh">
|
||||
<link rel="alternate" hreflang="x-default" href="https://landaiqing.github.io/voidraft/">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://landaiqing.github.io/voidraft/">
|
||||
<meta property="og:title" content="VoidRaft - An elegant text snippet recording tool designed for developers">
|
||||
<meta property="og:description" content="VoidRaft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
|
||||
<meta property="og:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
|
||||
<meta property="og:site_name" content="VoidRaft">
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:url" content="https://landaiqing.github.io/voidraft/">
|
||||
<meta property="twitter:title" content="VoidRaft - An elegant text snippet recording tool designed for developers">
|
||||
<meta property="twitter:description" content="VoidRaft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.">
|
||||
<meta property="twitter:image" content="https://landaiqing.github.io/voidraft/img/screenshot-dark.png">
|
||||
|
||||
<link rel="stylesheet" href="./css/styles.css">
|
||||
<link rel="icon" href="./img/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- Structured Data -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "VoidRaft",
|
||||
"description": "An elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.",
|
||||
"url": "https://landaiqing.github.io/voidraft/",
|
||||
"downloadUrl": "https://github.com/landaiqing/voidraft/releases",
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "VoidRaft"
|
||||
},
|
||||
"operatingSystem": ["Windows", "macOS", "Linux"],
|
||||
"applicationCategory": "DeveloperApplication",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "USD"
|
||||
},
|
||||
"screenshot": "https://landaiqing.github.io/voidraft/img/screenshot-dark.png",
|
||||
"softwareVersion": "Latest",
|
||||
"programmingLanguage": ["Go", "TypeScript", "Vue.js"],
|
||||
"codeRepository": "https://github.com/landaiqing/voidraft"
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="theme-dark">
|
||||
<div class="container">
|
||||
<!-- 主卡片 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1 class="card-title">VoidRaft</h1>
|
||||
<div class="card-controls">
|
||||
<button id="theme-toggle" class="btn btn-secondary" title="切换主题">
|
||||
<i class="fas fa-sun"></i> <span data-en="Theme" data-zh="主题">Theme</span>
|
||||
</button>
|
||||
<button id="lang-toggle" class="btn btn-secondary">
|
||||
<i class="fas fa-language"></i> 中/EN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<!-- Logo和介绍 -->
|
||||
<div class="logo-container">
|
||||
<div class="logo-frame">
|
||||
<img src="img/logo.png" alt="VoidRaft Logo" class="logo-image">
|
||||
</div>
|
||||
<h2 class="logo-text" data-en="VoidRaft" data-zh="VoidRaft">VoidRaft</h2>
|
||||
<p class="tagline" data-en="An elegant text snippet recording tool" data-zh="优雅的文本片段记录工具">An elegant text snippet recording tool</p>
|
||||
</div>
|
||||
|
||||
<div class="intro-box">
|
||||
<p class="intro-text" data-en="Designed for developers to record, organize, and manage various text snippets anytime, anywhere." data-zh="专为开发者打造,随时随地记录、整理和管理各种文本片段。">Designed for developers to record, organize, and manage various text snippets anytime, anywhere.</p>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<a href="https://github.com/landaiqing/voidraft/releases" class="btn" data-en="Download" data-zh="下载">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</a>
|
||||
<a href="https://github.com/landaiqing/voidraft" class="btn btn-secondary" data-en="Source Code" data-zh="源代码">
|
||||
<i class="fab fa-github"></i> Source Code
|
||||
</a>
|
||||
<a href="changelog.html" class="btn btn-secondary" data-en="Changelog" data-zh="更新日志">
|
||||
<i class="fas fa-history"></i> Changelog
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 特性部分 -->
|
||||
<h2 data-en="Core Features" data-zh="核心特性">Core Features</h2>
|
||||
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-code"></i>
|
||||
</div>
|
||||
<h3 class="feature-title" data-en="Developer-Friendly" data-zh="开发者友好">Developer-Friendly</h3>
|
||||
<p class="feature-desc" data-en="Multi-language code blocks with syntax highlighting for 30+ programming languages" data-zh="多语言代码块支持,为30+种编程语言提供语法高亮">Multi-language code blocks with syntax highlighting for 30+ programming languages</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-magic"></i>
|
||||
</div>
|
||||
<h3 class="feature-title" data-en="Code Formatting" data-zh="代码格式化">Code Formatting</h3>
|
||||
<p class="feature-desc" data-en="Built-in Prettier support for one-click code beautification" data-zh="内置Prettier支持,一键美化代码">Built-in Prettier support for one-click code beautification</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-palette"></i>
|
||||
</div>
|
||||
<h3 class="feature-title" data-en="Custom Themes" data-zh="自定义主题">Custom Themes</h3>
|
||||
<p class="feature-desc" data-en="Dark/Light themes with full customization options" data-zh="深色/浅色主题,支持完全自定义">Dark/Light themes with full customization options</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-clone"></i>
|
||||
</div>
|
||||
<h3 class="feature-title" data-en="Multi-Window" data-zh="多窗口支持">Multi-Window</h3>
|
||||
<p class="feature-desc" data-en="Edit multiple documents simultaneously" data-zh="同时编辑多个文档">Edit multiple documents simultaneously</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-layer-group"></i>
|
||||
</div>
|
||||
<h3 class="feature-title" data-en="Block Editing" data-zh="块状编辑">Block Editing</h3>
|
||||
<p class="feature-desc" data-en="Split content into independent code blocks with different language settings" data-zh="将内容分割为独立的代码块,每个块可设置不同语言">Split content into independent code blocks with different language settings</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-puzzle-piece"></i>
|
||||
</div>
|
||||
<h3 class="feature-title" data-en="Extensions" data-zh="丰富扩展">Extensions</h3>
|
||||
<p class="feature-desc" data-en="Rainbow brackets, VSCode-style search, color picker, translation tool, and more" data-zh="彩虹括号、VSCode风格搜索、颜色选择器、翻译工具等多种扩展">Rainbow brackets, VSCode-style search, color picker, translation tool, and more</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预览部分 -->
|
||||
<h2 data-en="Preview" data-zh="预览">Preview</h2>
|
||||
|
||||
<div class="preview-container">
|
||||
<div class="preview-window">
|
||||
<div class="preview-header">
|
||||
<div class="preview-controls">
|
||||
<span class="preview-btn"></span>
|
||||
<span class="preview-btn"></span>
|
||||
<span class="preview-btn"></span>
|
||||
</div>
|
||||
<div class="preview-title">voidraft</div>
|
||||
</div>
|
||||
<div class="preview-content">
|
||||
<div class="code-block-wrapper">
|
||||
<div class="block-header">
|
||||
<div class="block-language">javascript</div>
|
||||
</div>
|
||||
<pre class="code-block">
|
||||
<span class="keyword">function</span> <span class="function">createDocument</span>() {
|
||||
<span class="keyword">const</span> <span class="variable">doc</span> = <span class="keyword">new</span> <span class="class">Document</span>();
|
||||
|
||||
<span class="variable">doc</span>.<span class="function">addCodeBlock</span>(<span class="string">'javascript'</span>, <span class="string">`
|
||||
<span class="keyword">function</span> <span class="function">greeting</span>(<span class="parameter">name</span>) {
|
||||
<span class="keyword">return</span> <span class="string">`Hello, </span>${<span class="parameter">name</span>}<span class="string">!`</span>;
|
||||
}
|
||||
|
||||
<span class="built-in">console</span>.<span class="function">log</span>(<span class="function">greeting</span>(<span class="string">'World'</span>));
|
||||
`</span>);
|
||||
|
||||
<span class="keyword">return</span> <span class="variable">doc</span>;
|
||||
}</pre>
|
||||
</div>
|
||||
|
||||
<div class="code-block-wrapper" style="margin-top: 10px;">
|
||||
<div class="block-header">
|
||||
<div class="block-language">text</div>
|
||||
</div>
|
||||
<pre class="code-block">
|
||||
<span class="comment">// VoidRaft - An elegant text snippet recording tool</span>
|
||||
<span class="comment">// Multi-language support | Code formatting | Custom themes</span>
|
||||
<span class="comment">// A modern text editor designed for developers</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-window">
|
||||
<img src="img/screenshot-dark.png" alt="VoidRaft 界面预览" class="preview-image dark-theme-img">
|
||||
<img src="img/screenshot-light.png" alt="VoidRaft 界面预览" class="preview-image light-theme-img" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 技术栈部分 -->
|
||||
<h2 data-en="Technical Stack" data-zh="技术栈">Technical Stack</h2>
|
||||
|
||||
<ul class="tech-list">
|
||||
<li class="tech-item">
|
||||
<div class="tech-icon"><i class="fas fa-desktop"></i></div>
|
||||
<span class="tech-name">Wails3</span>
|
||||
<span class="tech-desc" data-en="Cross-platform desktop application framework" data-zh="跨平台桌面应用框架">Cross-platform desktop application framework</span>
|
||||
</li>
|
||||
<li class="tech-item">
|
||||
<div class="tech-icon"><i class="fas fa-cogs"></i></div>
|
||||
<span class="tech-name">Go 1.21+</span>
|
||||
<span class="tech-desc" data-en="Fast and efficient backend language" data-zh="快速高效的后端语言">Fast and efficient backend language</span>
|
||||
</li>
|
||||
<li class="tech-item">
|
||||
<div class="tech-icon"><i class="fab fa-vuejs"></i></div>
|
||||
<span class="tech-name">Vue 3 + TypeScript</span>
|
||||
<span class="tech-desc" data-en="Modern frontend framework" data-zh="现代化前端框架">Modern frontend framework</span>
|
||||
</li>
|
||||
<li class="tech-item">
|
||||
<div class="tech-icon"><i class="fas fa-edit"></i></div>
|
||||
<span class="tech-name">CodeMirror 6</span>
|
||||
<span class="tech-desc" data-en="Modern code editor with extension support" data-zh="支持扩展的现代化代码编辑器">Modern code editor with extension support</span>
|
||||
</li>
|
||||
<li class="tech-item">
|
||||
<div class="tech-icon"><i class="fas fa-database"></i></div>
|
||||
<span class="tech-name">SQLite</span>
|
||||
<span class="tech-desc" data-en="Lightweight database for document storage" data-zh="轻量级文档存储数据库">Lightweight database for document storage</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="footer">
|
||||
<p class="footer-text" data-en="© 2025 VoidRaft - An elegant text snippet recording tool designed for developers" data-zh="© 2025 VoidRaft - 专为开发者打造的优雅文本片段记录工具">© 2025 VoidRaft - An elegant text snippet recording tool designed for developers</p>
|
||||
<div class="footer-links">
|
||||
<a href="https://github.com/landaiqing/voidraft" target="_blank" class="footer-link">GitHub</a>
|
||||
<a href="https://github.com/landaiqing/voidraft/issues" target="_blank" class="footer-link" data-en="Issues" data-zh="问题反馈">Issues</a>
|
||||
<a href="https://github.com/landaiqing/voidraft/releases" target="_blank" class="footer-link" data-en="Releases" data-zh="版本发布">Releases</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
705
docs/js/changelog.js
Normal file
705
docs/js/changelog.js
Normal file
@@ -0,0 +1,705 @@
|
||||
/**
|
||||
* VoidRaft - Changelog Script
|
||||
* 从GitHub API获取发布信息,支持Gitea备用源
|
||||
*/
|
||||
|
||||
/**
|
||||
* 仓库配置类
|
||||
*/
|
||||
class RepositoryConfig {
|
||||
constructor() {
|
||||
this.repos = {
|
||||
github: {
|
||||
owner: 'landaiqing',
|
||||
name: 'voidraft',
|
||||
apiUrl: 'https://api.github.com/repos/landaiqing/voidraft/releases',
|
||||
releasesUrl: 'https://github.com/landaiqing/voidraft/releases'
|
||||
},
|
||||
gitea: {
|
||||
owner: 'landaiqing',
|
||||
name: 'voidraft',
|
||||
domain: 'git.landaiqing.cn',
|
||||
apiUrl: 'https://git.landaiqing.cn/api/v1/repos/landaiqing/voidraft/releases',
|
||||
releasesUrl: 'https://git.landaiqing.cn/landaiqing/voidraft/releases'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取仓库配置
|
||||
* @param {string} source - 'github' 或 'gitea'
|
||||
*/
|
||||
getRepo(source) {
|
||||
return this.repos[source];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有仓库配置
|
||||
*/
|
||||
getAllRepos() {
|
||||
return this.repos;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 国际化消息管理类
|
||||
*/
|
||||
class I18nMessages {
|
||||
constructor() {
|
||||
this.messages = {
|
||||
loading: {
|
||||
en: 'Loading releases...',
|
||||
zh: '正在加载版本信息...'
|
||||
},
|
||||
noReleases: {
|
||||
en: 'No release information found',
|
||||
zh: '没有找到版本发布信息'
|
||||
},
|
||||
fetchError: {
|
||||
en: 'Failed to load release information. Please try again later.',
|
||||
zh: '无法获取版本信息,请稍后再试'
|
||||
},
|
||||
githubApiError: {
|
||||
en: 'GitHub API returned an error status: ',
|
||||
zh: 'GitHub API返回错误状态: '
|
||||
},
|
||||
giteaApiError: {
|
||||
en: 'Gitea API returned an error status: ',
|
||||
zh: 'Gitea API返回错误状态: '
|
||||
},
|
||||
dataSource: {
|
||||
en: 'Data source: ',
|
||||
zh: '数据来源: '
|
||||
},
|
||||
downloads: {
|
||||
en: 'Downloads',
|
||||
zh: '下载资源'
|
||||
},
|
||||
download: {
|
||||
en: 'Download',
|
||||
zh: '下载'
|
||||
},
|
||||
preRelease: {
|
||||
en: 'Pre-release',
|
||||
zh: '预发布'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息
|
||||
* @param {string} key - 消息键
|
||||
* @param {string} lang - 语言代码
|
||||
*/
|
||||
getMessage(key, lang = 'en') {
|
||||
return this.messages[key] && this.messages[key][lang] || this.messages[key]['en'] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前语言
|
||||
*/
|
||||
getCurrentLang() {
|
||||
return window.currentLang || 'en';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API客户端类
|
||||
*/
|
||||
class APIClient {
|
||||
constructor(repositoryConfig, i18nMessages) {
|
||||
this.repositoryConfig = repositoryConfig;
|
||||
this.i18nMessages = i18nMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定源获取发布信息
|
||||
* @param {string} source - 'github' 或 'gitea'
|
||||
*/
|
||||
async fetchReleases(source) {
|
||||
const repo = this.repositoryConfig.getRepo(source);
|
||||
const errorMessageKey = source === 'github' ? 'githubApiError' : 'giteaApiError';
|
||||
|
||||
const options = {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
};
|
||||
|
||||
if (source === 'github') {
|
||||
return this.fetchFromGitHub(repo, options, errorMessageKey);
|
||||
} else {
|
||||
return this.fetchFromGitea(repo, options, errorMessageKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从GitHub获取数据
|
||||
* @param {Object} repo - 仓库配置
|
||||
* @param {Object} options - 请求选项
|
||||
* @param {string} errorMessageKey - 错误消息键
|
||||
*/
|
||||
async fetchFromGitHub(repo, options, errorMessageKey) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
options.signal = controller.signal;
|
||||
options.headers['Accept'] = 'application/vnd.github.v3+json';
|
||||
|
||||
try {
|
||||
const response = await fetch(repo.apiUrl, options);
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${this.i18nMessages.getMessage(errorMessageKey, this.i18nMessages.getCurrentLang())}${response.status}`);
|
||||
}
|
||||
|
||||
const releases = await response.json();
|
||||
|
||||
if (!releases || releases.length === 0) {
|
||||
throw new Error(this.i18nMessages.getMessage('noReleases', this.i18nMessages.getCurrentLang()));
|
||||
}
|
||||
|
||||
return releases;
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Gitea获取数据
|
||||
* @param {Object} repo - 仓库配置
|
||||
* @param {Object} options - 请求选项
|
||||
* @param {string} errorMessageKey - 错误消息键
|
||||
*/
|
||||
async fetchFromGitea(repo, options, errorMessageKey) {
|
||||
const response = await fetch(repo.apiUrl, options);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${this.i18nMessages.getMessage(errorMessageKey, this.i18nMessages.getCurrentLang())}${response.status}`);
|
||||
}
|
||||
|
||||
const releases = await response.json();
|
||||
|
||||
if (!releases || releases.length === 0) {
|
||||
throw new Error(this.i18nMessages.getMessage('noReleases', this.i18nMessages.getCurrentLang()));
|
||||
}
|
||||
|
||||
return releases;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UI管理类
|
||||
*/
|
||||
class UIManager {
|
||||
constructor(i18nMessages) {
|
||||
this.i18nMessages = i18nMessages;
|
||||
this.elements = {
|
||||
loading: document.getElementById('loading'),
|
||||
changelog: document.getElementById('changelog'),
|
||||
error: document.getElementById('error-message')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示加载状态
|
||||
*/
|
||||
showLoading() {
|
||||
this.elements.loading.style.display = 'block';
|
||||
this.elements.error.style.display = 'none';
|
||||
this.elements.changelog.innerHTML = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏加载状态
|
||||
*/
|
||||
hideLoading() {
|
||||
this.elements.loading.style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误消息
|
||||
* @param {string} message - 错误消息
|
||||
*/
|
||||
showError(message) {
|
||||
const errorMessageElement = this.elements.error.querySelector('p');
|
||||
if (errorMessageElement) {
|
||||
errorMessageElement.textContent = message;
|
||||
} else {
|
||||
this.elements.error.textContent = message;
|
||||
}
|
||||
this.elements.error.style.display = 'block';
|
||||
this.hideLoading();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示发布信息
|
||||
* @param {Array} releases - 发布信息数组
|
||||
* @param {string} source - 数据源
|
||||
*/
|
||||
displayReleases(releases, source) {
|
||||
this.hideLoading();
|
||||
|
||||
// 清除现有内容
|
||||
this.elements.changelog.innerHTML = '';
|
||||
|
||||
// 创建数据源元素
|
||||
const sourceElement = this.createSourceElement(source);
|
||||
this.elements.changelog.appendChild(sourceElement);
|
||||
|
||||
// 创建发布信息元素
|
||||
releases.forEach(release => {
|
||||
const releaseElement = this.createReleaseElement(release, source);
|
||||
this.elements.changelog.appendChild(releaseElement);
|
||||
});
|
||||
|
||||
this.elements.changelog.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建数据源元素
|
||||
* @param {string} source - 数据源
|
||||
*/
|
||||
createSourceElement(source) {
|
||||
const sourceElement = document.createElement('div');
|
||||
sourceElement.className = 'data-source';
|
||||
|
||||
// 创建带有国际化支持的源标签
|
||||
const sourceLabel = document.createElement('span');
|
||||
sourceLabel.setAttribute('data-en', this.i18nMessages.getMessage('dataSource', 'en'));
|
||||
sourceLabel.setAttribute('data-zh', this.i18nMessages.getMessage('dataSource', 'zh'));
|
||||
sourceLabel.textContent = this.i18nMessages.getMessage('dataSource', this.i18nMessages.getCurrentLang());
|
||||
|
||||
// 创建链接
|
||||
const sourceLink = document.createElement('a');
|
||||
const repositoryConfig = new RepositoryConfig();
|
||||
sourceLink.href = repositoryConfig.getRepo(source).releasesUrl;
|
||||
sourceLink.textContent = source === 'github' ? 'GitHub' : 'Gitea';
|
||||
sourceLink.target = '_blank';
|
||||
|
||||
// 组装元素
|
||||
sourceElement.appendChild(sourceLabel);
|
||||
sourceElement.appendChild(sourceLink);
|
||||
|
||||
return sourceElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建发布信息元素
|
||||
* @param {Object} release - 发布信息对象
|
||||
* @param {string} source - 数据源
|
||||
*/
|
||||
createReleaseElement(release, source) {
|
||||
const releaseElement = document.createElement('div');
|
||||
releaseElement.className = 'release';
|
||||
|
||||
// 格式化发布日期
|
||||
const releaseDate = new Date(release.published_at || release.created_at);
|
||||
const formattedDate = DateFormatter.formatDate(releaseDate);
|
||||
|
||||
// 创建头部
|
||||
const headerElement = this.createReleaseHeader(release, formattedDate);
|
||||
releaseElement.appendChild(headerElement);
|
||||
|
||||
// 添加发布说明
|
||||
if (release.body) {
|
||||
const descriptionElement = document.createElement('div');
|
||||
descriptionElement.className = 'release-description markdown-content';
|
||||
descriptionElement.innerHTML = MarkdownParser.parseMarkdown(release.body);
|
||||
releaseElement.appendChild(descriptionElement);
|
||||
}
|
||||
|
||||
// 添加下载资源
|
||||
const assets = AssetManager.getAssetsFromRelease(release, source);
|
||||
if (assets && assets.length > 0) {
|
||||
const assetsElement = this.createAssetsElement(assets);
|
||||
releaseElement.appendChild(assetsElement);
|
||||
}
|
||||
|
||||
return releaseElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建发布信息头部
|
||||
*/
|
||||
createReleaseHeader(release, formattedDate) {
|
||||
const headerElement = document.createElement('div');
|
||||
headerElement.className = 'release-header';
|
||||
|
||||
// 版本元素
|
||||
const versionElement = document.createElement('div');
|
||||
versionElement.className = 'release-version';
|
||||
|
||||
// 版本文本
|
||||
const versionText = document.createElement('span');
|
||||
versionText.textContent = release.name || release.tag_name;
|
||||
versionElement.appendChild(versionText);
|
||||
|
||||
// 预发布标记
|
||||
if (release.prerelease) {
|
||||
const preReleaseTag = document.createElement('span');
|
||||
preReleaseTag.className = 'release-badge pre-release';
|
||||
preReleaseTag.setAttribute('data-en', this.i18nMessages.getMessage('preRelease', 'en'));
|
||||
preReleaseTag.setAttribute('data-zh', this.i18nMessages.getMessage('preRelease', 'zh'));
|
||||
preReleaseTag.textContent = this.i18nMessages.getMessage('preRelease', this.i18nMessages.getCurrentLang());
|
||||
versionElement.appendChild(preReleaseTag);
|
||||
}
|
||||
|
||||
// 日期元素
|
||||
const dateElement = document.createElement('div');
|
||||
dateElement.className = 'release-date';
|
||||
dateElement.textContent = formattedDate;
|
||||
|
||||
headerElement.appendChild(versionElement);
|
||||
headerElement.appendChild(dateElement);
|
||||
|
||||
return headerElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建资源文件元素
|
||||
* @param {Array} assets - 资源文件数组
|
||||
*/
|
||||
createAssetsElement(assets) {
|
||||
const assetsElement = document.createElement('div');
|
||||
assetsElement.className = 'release-assets';
|
||||
|
||||
// 资源标题
|
||||
const assetsTitle = document.createElement('div');
|
||||
assetsTitle.className = 'release-assets-title';
|
||||
assetsTitle.setAttribute('data-en', this.i18nMessages.getMessage('downloads', 'en'));
|
||||
assetsTitle.setAttribute('data-zh', this.i18nMessages.getMessage('downloads', 'zh'));
|
||||
assetsTitle.textContent = this.i18nMessages.getMessage('downloads', this.i18nMessages.getCurrentLang());
|
||||
|
||||
// 资源列表
|
||||
const assetList = document.createElement('ul');
|
||||
assetList.className = 'asset-list';
|
||||
|
||||
// 添加每个资源
|
||||
assets.forEach(asset => {
|
||||
const assetItem = this.createAssetItem(asset);
|
||||
assetList.appendChild(assetItem);
|
||||
});
|
||||
|
||||
assetsElement.appendChild(assetsTitle);
|
||||
assetsElement.appendChild(assetList);
|
||||
|
||||
return assetsElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建资源文件项
|
||||
* @param {Object} asset - 资源文件对象
|
||||
*/
|
||||
createAssetItem(asset) {
|
||||
const assetItem = document.createElement('li');
|
||||
assetItem.className = 'asset-item';
|
||||
|
||||
// 文件图标
|
||||
const iconElement = document.createElement('i');
|
||||
iconElement.className = `asset-icon fas fa-${FileIconHelper.getFileIcon(asset.name)}`;
|
||||
|
||||
// 文件名
|
||||
const nameElement = document.createElement('span');
|
||||
nameElement.className = 'asset-name';
|
||||
nameElement.textContent = asset.name;
|
||||
|
||||
// 文件大小
|
||||
const sizeElement = document.createElement('span');
|
||||
sizeElement.className = 'asset-size';
|
||||
sizeElement.textContent = FileSizeFormatter.formatFileSize(asset.size);
|
||||
|
||||
// 下载链接
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.className = 'download-btn';
|
||||
downloadLink.href = asset.browser_download_url;
|
||||
downloadLink.target = '_blank';
|
||||
downloadLink.setAttribute('data-en', this.i18nMessages.getMessage('download', 'en'));
|
||||
downloadLink.setAttribute('data-zh', this.i18nMessages.getMessage('download', 'zh'));
|
||||
downloadLink.textContent = this.i18nMessages.getMessage('download', this.i18nMessages.getCurrentLang());
|
||||
|
||||
// 组装资源项
|
||||
assetItem.appendChild(iconElement);
|
||||
assetItem.appendChild(nameElement);
|
||||
assetItem.appendChild(sizeElement);
|
||||
assetItem.appendChild(downloadLink);
|
||||
|
||||
return assetItem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源管理器类
|
||||
*/
|
||||
class AssetManager {
|
||||
/**
|
||||
* 从发布信息中获取资源文件
|
||||
* @param {Object} release - 发布信息对象
|
||||
* @param {string} source - 数据源
|
||||
*/
|
||||
static getAssetsFromRelease(release, source) {
|
||||
let assets = [];
|
||||
|
||||
if (source === 'github') {
|
||||
assets = release.assets || [];
|
||||
} else { // Gitea
|
||||
assets = release.assets || [];
|
||||
// 检查Gitea特定的资源结构
|
||||
if (!assets.length && release.attachments) {
|
||||
assets = release.attachments.map(attachment => ({
|
||||
name: attachment.name,
|
||||
size: attachment.size,
|
||||
browser_download_url: attachment.browser_download_url
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return assets;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件图标助手类
|
||||
*/
|
||||
class FileIconHelper {
|
||||
/**
|
||||
* 根据文件扩展名获取图标
|
||||
* @param {string} filename - 文件名
|
||||
*/
|
||||
static getFileIcon(filename) {
|
||||
const extension = filename.split('.').pop().toLowerCase();
|
||||
|
||||
const iconMap = {
|
||||
'exe': 'download',
|
||||
'msi': 'download',
|
||||
'dmg': 'download',
|
||||
'pkg': 'download',
|
||||
'deb': 'download',
|
||||
'rpm': 'download',
|
||||
'tar': 'file-archive',
|
||||
'gz': 'file-archive',
|
||||
'zip': 'file-archive',
|
||||
'7z': 'file-archive',
|
||||
'rar': 'file-archive',
|
||||
'pdf': 'file-pdf',
|
||||
'txt': 'file-alt',
|
||||
'md': 'file-alt',
|
||||
'json': 'file-code',
|
||||
'xml': 'file-code',
|
||||
'yml': 'file-code',
|
||||
'yaml': 'file-code'
|
||||
};
|
||||
|
||||
return iconMap[extension] || 'file';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件大小格式化器类
|
||||
*/
|
||||
class FileSizeFormatter {
|
||||
/**
|
||||
* 格式化文件大小
|
||||
* @param {number} bytes - 字节数
|
||||
*/
|
||||
static formatFileSize(bytes) {
|
||||
if (!bytes) return '';
|
||||
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
|
||||
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期格式化器类
|
||||
*/
|
||||
class DateFormatter {
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param {Date} date - 日期对象
|
||||
*/
|
||||
static formatDate(date) {
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
};
|
||||
|
||||
const lang = window.currentLang || 'en';
|
||||
const locale = lang === 'zh' ? 'zh-CN' : 'en-US';
|
||||
|
||||
return date.toLocaleDateString(locale, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Markdown解析器类
|
||||
*/
|
||||
class MarkdownParser {
|
||||
/**
|
||||
* 简单的Markdown解析
|
||||
* @param {string} markdown - Markdown文本
|
||||
*/
|
||||
static parseMarkdown(markdown) {
|
||||
if (!markdown) return '';
|
||||
|
||||
// 预处理:保留原始换行符,用特殊标记替换
|
||||
const preservedLineBreaks = '___LINE_BREAK___';
|
||||
markdown = markdown.replace(/\n/g, preservedLineBreaks);
|
||||
|
||||
// 引用块 - > text
|
||||
markdown = markdown.replace(/>\s*(.*?)(?=>|$)/g, '<blockquote>$1</blockquote>');
|
||||
markdown = markdown.replace(/>\s*(.*?)(?=>|$)/g, '<blockquote>$1</blockquote>');
|
||||
|
||||
// 链接 - [text](url)
|
||||
markdown = markdown.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||
|
||||
// 标题 - # Heading
|
||||
markdown = markdown.replace(/^### (.*?)(?=___LINE_BREAK___|$)/gm, '<h3>$1</h3>');
|
||||
markdown = markdown.replace(/^## (.*?)(?=___LINE_BREAK___|$)/gm, '<h2>$1</h2>');
|
||||
markdown = markdown.replace(/^# (.*?)(?=___LINE_BREAK___|$)/gm, '<h1>$1</h1>');
|
||||
|
||||
// 粗体 - **text**
|
||||
markdown = markdown.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||
|
||||
// 斜体 - *text*
|
||||
markdown = markdown.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
||||
|
||||
// 代码块 - ```code```
|
||||
markdown = markdown.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
||||
|
||||
// 行内代码 - `code`
|
||||
markdown = markdown.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
|
||||
// 处理列表项
|
||||
// 先将每个列表项转换为HTML
|
||||
markdown = markdown.replace(/- (.*?)(?=___LINE_BREAK___- |___LINE_BREAK___$|$)/g, '<li>$1</li>');
|
||||
markdown = markdown.replace(/\* (.*?)(?=___LINE_BREAK___\* |___LINE_BREAK___$|$)/g, '<li>$1</li>');
|
||||
markdown = markdown.replace(/\d+\. (.*?)(?=___LINE_BREAK___\d+\. |___LINE_BREAK___$|$)/g, '<li>$1</li>');
|
||||
|
||||
// 然后将连续的列表项包装在ul或ol中
|
||||
const listItemRegex = /<li>.*?<\/li>/g;
|
||||
const listItems = markdown.match(listItemRegex) || [];
|
||||
|
||||
if (listItems.length > 0) {
|
||||
// 将连续的列表项组合在一起
|
||||
let lastIndex = 0;
|
||||
let result = '';
|
||||
let inList = false;
|
||||
|
||||
listItems.forEach(item => {
|
||||
const itemIndex = markdown.indexOf(item, lastIndex);
|
||||
|
||||
// 添加列表项之前的内容
|
||||
if (itemIndex > lastIndex) {
|
||||
result += markdown.substring(lastIndex, itemIndex);
|
||||
}
|
||||
|
||||
// 如果不在列表中,开始一个新列表
|
||||
if (!inList) {
|
||||
result += '<ul>';
|
||||
inList = true;
|
||||
}
|
||||
|
||||
// 添加列表项
|
||||
result += item;
|
||||
|
||||
// 更新lastIndex
|
||||
lastIndex = itemIndex + item.length;
|
||||
|
||||
// 检查下一个内容是否是列表项
|
||||
const nextItemIndex = markdown.indexOf('<li>', lastIndex);
|
||||
if (nextItemIndex === -1 || nextItemIndex > lastIndex + 20) { // 如果下一个列表项不紧邻
|
||||
result += '</ul>';
|
||||
inList = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 添加剩余内容
|
||||
if (lastIndex < markdown.length) {
|
||||
result += markdown.substring(lastIndex);
|
||||
}
|
||||
|
||||
markdown = result;
|
||||
}
|
||||
|
||||
// 处理水平分隔线
|
||||
markdown = markdown.replace(/---/g, '<hr>');
|
||||
|
||||
// 恢复换行符
|
||||
markdown = markdown.replace(/___LINE_BREAK___/g, '<br>');
|
||||
|
||||
// 处理段落
|
||||
markdown = markdown.replace(/<br><br>/g, '</p><p>');
|
||||
|
||||
// 包装在段落标签中
|
||||
if (!markdown.startsWith('<p>')) {
|
||||
markdown = `<p>${markdown}</p>`;
|
||||
}
|
||||
|
||||
return markdown;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新日志主应用类
|
||||
*/
|
||||
class ChangelogApp {
|
||||
constructor() {
|
||||
this.repositoryConfig = new RepositoryConfig();
|
||||
this.i18nMessages = new I18nMessages();
|
||||
this.apiClient = new APIClient(this.repositoryConfig, this.i18nMessages);
|
||||
this.uiManager = new UIManager(this.i18nMessages);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化应用
|
||||
*/
|
||||
init() {
|
||||
this.uiManager.showLoading();
|
||||
|
||||
// 首先尝试GitHub API
|
||||
this.apiClient.fetchReleases('github')
|
||||
.then(releases => {
|
||||
this.uiManager.displayReleases(releases, 'github');
|
||||
})
|
||||
.catch(() => {
|
||||
// GitHub失败时尝试Gitea
|
||||
return this.apiClient.fetchReleases('gitea')
|
||||
.then(releases => {
|
||||
this.uiManager.displayReleases(releases, 'gitea');
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取发布信息失败:', error);
|
||||
this.uiManager.showError(this.i18nMessages.getMessage('fetchError', this.i18nMessages.getCurrentLang()));
|
||||
});
|
||||
|
||||
// 监听语言变化事件
|
||||
document.addEventListener('languageChanged', () => this.updateUI());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新UI元素(当语言变化时)
|
||||
*/
|
||||
updateUI() {
|
||||
const elementsToUpdate = document.querySelectorAll('[data-en][data-zh]');
|
||||
const currentLang = this.i18nMessages.getCurrentLang();
|
||||
|
||||
elementsToUpdate.forEach(element => {
|
||||
const text = element.getAttribute(`data-${currentLang}`);
|
||||
if (text) {
|
||||
element.textContent = text;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 当DOM加载完成时初始化应用
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new ChangelogApp();
|
||||
});
|
443
docs/js/script.js
Normal file
443
docs/js/script.js
Normal file
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* VoidRaft - Website Script
|
||||
*/
|
||||
|
||||
/**
|
||||
* 主题管理类
|
||||
*/
|
||||
class ThemeManager {
|
||||
constructor() {
|
||||
this.themeToggle = document.getElementById('theme-toggle');
|
||||
this.currentTheme = this.getInitialTheme();
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取初始主题
|
||||
*/
|
||||
getInitialTheme() {
|
||||
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
return savedTheme || (prefersDarkScheme.matches ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化主题管理器
|
||||
*/
|
||||
init() {
|
||||
if (!this.themeToggle) return;
|
||||
|
||||
this.setTheme(this.currentTheme);
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定事件
|
||||
*/
|
||||
bindEvents() {
|
||||
this.themeToggle.addEventListener('click', () => {
|
||||
this.toggleTheme();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换主题
|
||||
*/
|
||||
toggleTheme() {
|
||||
document.body.classList.add('theme-transition');
|
||||
|
||||
const newTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
|
||||
this.setTheme(newTheme);
|
||||
this.saveTheme(newTheme);
|
||||
|
||||
setTimeout(() => document.body.classList.remove('theme-transition'), 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置主题
|
||||
* @param {string} theme - 'dark' 或 'light'
|
||||
*/
|
||||
setTheme(theme) {
|
||||
this.currentTheme = theme;
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
document.body.classList.toggle('theme-dark', isDark);
|
||||
document.body.classList.toggle('theme-light', !isDark);
|
||||
|
||||
this.updateToggleIcon(isDark);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新切换按钮图标
|
||||
* @param {boolean} isDark - 是否为暗色主题
|
||||
*/
|
||||
updateToggleIcon(isDark) {
|
||||
if (this.themeToggle) {
|
||||
const icon = this.themeToggle.querySelector('i');
|
||||
if (icon) {
|
||||
icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存主题到本地存储
|
||||
* @param {string} theme - 主题名称
|
||||
*/
|
||||
saveTheme(theme) {
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 语言管理类
|
||||
*/
|
||||
class LanguageManager {
|
||||
constructor() {
|
||||
this.langToggle = document.getElementById('lang-toggle');
|
||||
this.currentLang = this.getInitialLanguage();
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取初始语言
|
||||
*/
|
||||
getInitialLanguage() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const urlLang = urlParams.get('lang');
|
||||
const savedLang = localStorage.getItem('lang');
|
||||
const browserLang = navigator.language.startsWith('zh') ? 'zh' : 'en';
|
||||
return urlLang || savedLang || browserLang;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化语言管理器
|
||||
*/
|
||||
init() {
|
||||
if (!this.langToggle) return;
|
||||
|
||||
window.currentLang = this.currentLang;
|
||||
this.setLanguage(this.currentLang);
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定事件
|
||||
*/
|
||||
bindEvents() {
|
||||
this.langToggle.addEventListener('click', () => {
|
||||
this.toggleLanguage();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换语言
|
||||
*/
|
||||
toggleLanguage() {
|
||||
document.body.classList.add('lang-transition');
|
||||
|
||||
const newLang = this.currentLang === 'zh' ? 'en' : 'zh';
|
||||
this.setLanguage(newLang);
|
||||
this.saveLanguage(newLang);
|
||||
this.updateURL(newLang);
|
||||
this.notifyLanguageChange(newLang);
|
||||
|
||||
setTimeout(() => document.body.classList.remove('lang-transition'), 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置页面语言
|
||||
* @param {string} lang - 'zh' 或 'en'
|
||||
*/
|
||||
setLanguage(lang) {
|
||||
this.currentLang = lang;
|
||||
window.currentLang = lang;
|
||||
|
||||
this.updatePageElements(lang);
|
||||
this.updateHTMLLang(lang);
|
||||
this.updateToggleButton(lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新页面元素文本
|
||||
* @param {string} lang - 语言代码
|
||||
*/
|
||||
updatePageElements(lang) {
|
||||
document.querySelectorAll('[data-zh][data-en]').forEach(el => {
|
||||
el.textContent = el.getAttribute(`data-${lang}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新HTML语言属性
|
||||
* @param {string} lang - 语言代码
|
||||
*/
|
||||
updateHTMLLang(lang) {
|
||||
document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新切换按钮文本
|
||||
* @param {string} lang - 语言代码
|
||||
*/
|
||||
updateToggleButton(lang) {
|
||||
if (this.langToggle) {
|
||||
const text = lang === 'zh' ? 'EN/中' : '中/EN';
|
||||
this.langToggle.innerHTML = `<i class="fas fa-language"></i> ${text}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存语言到本地存储
|
||||
* @param {string} lang - 语言代码
|
||||
*/
|
||||
saveLanguage(lang) {
|
||||
localStorage.setItem('lang', lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新URL参数
|
||||
* @param {string} lang - 语言代码
|
||||
*/
|
||||
updateURL(lang) {
|
||||
const newUrl = new URL(window.location);
|
||||
if (lang === 'zh') {
|
||||
newUrl.searchParams.set('lang', 'zh');
|
||||
} else {
|
||||
newUrl.searchParams.delete('lang');
|
||||
}
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知语言变更
|
||||
* @param {string} lang - 语言代码
|
||||
*/
|
||||
notifyLanguageChange(lang) {
|
||||
window.dispatchEvent(new CustomEvent('languageChanged', { detail: { lang } }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前语言
|
||||
*/
|
||||
getCurrentLanguage() {
|
||||
return this.currentLang;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SEO管理类
|
||||
*/
|
||||
class SEOManager {
|
||||
constructor(languageManager) {
|
||||
this.languageManager = languageManager;
|
||||
this.metaTexts = {
|
||||
en: {
|
||||
description: 'VoidRaft is an elegant text snippet recording tool designed for developers. Features multi-language code blocks, syntax highlighting, code formatting, custom themes, and more.',
|
||||
title: 'VoidRaft - An elegant text snippet recording tool designed for developers.',
|
||||
ogTitle: 'VoidRaft - An elegant text snippet recording tool designed for developers'
|
||||
},
|
||||
zh: {
|
||||
description: 'VoidRaft 是专为开发者打造的优雅文本片段记录工具。支持多语言代码块、语法高亮、代码格式化、自定义主题等功能。',
|
||||
title: 'VoidRaft - 专为开发者打造的优雅文本片段记录工具',
|
||||
ogTitle: 'VoidRaft - 专为开发者打造的优雅文本片段记录工具'
|
||||
}
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化SEO管理器
|
||||
*/
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.updateMetaTags(this.languageManager.getCurrentLanguage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定事件
|
||||
*/
|
||||
bindEvents() {
|
||||
window.addEventListener('languageChanged', (event) => {
|
||||
this.updateMetaTags(event.detail.lang);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新SEO元标签
|
||||
* @param {string} lang - 当前语言
|
||||
*/
|
||||
updateMetaTags(lang) {
|
||||
const texts = this.metaTexts[lang];
|
||||
|
||||
this.updateMetaDescription(texts.description);
|
||||
this.updateOpenGraphTags(texts.ogTitle, texts.description);
|
||||
this.updateTwitterCardTags(texts.ogTitle, texts.description);
|
||||
this.updatePageTitle(texts.title);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新meta描述
|
||||
* @param {string} description - 描述文本
|
||||
*/
|
||||
updateMetaDescription(description) {
|
||||
const metaDesc = document.querySelector('meta[name="description"]');
|
||||
if (metaDesc) {
|
||||
metaDesc.content = description;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Open Graph标签
|
||||
* @param {string} title - 标题
|
||||
* @param {string} description - 描述
|
||||
*/
|
||||
updateOpenGraphTags(title, description) {
|
||||
const ogTitle = document.querySelector('meta[property="og:title"]');
|
||||
const ogDesc = document.querySelector('meta[property="og:description"]');
|
||||
|
||||
if (ogTitle) ogTitle.content = title;
|
||||
if (ogDesc) ogDesc.content = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Twitter Card标签
|
||||
* @param {string} title - 标题
|
||||
* @param {string} description - 描述
|
||||
*/
|
||||
updateTwitterCardTags(title, description) {
|
||||
const twitterTitle = document.querySelector('meta[property="twitter:title"]');
|
||||
const twitterDesc = document.querySelector('meta[property="twitter:description"]');
|
||||
|
||||
if (twitterTitle) twitterTitle.content = title;
|
||||
if (twitterDesc) twitterDesc.content = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新页面标题
|
||||
* @param {string} title - 标题
|
||||
*/
|
||||
updatePageTitle(title) {
|
||||
document.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UI效果管理类
|
||||
*/
|
||||
class UIEffects {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化UI效果
|
||||
*/
|
||||
init() {
|
||||
this.initCardEffects();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化卡片悬停效果
|
||||
*/
|
||||
initCardEffects() {
|
||||
const cards = document.querySelectorAll('.feature-card');
|
||||
|
||||
cards.forEach(card => {
|
||||
card.addEventListener('mouseenter', () => {
|
||||
this.animateCardHover(card, true);
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', () => {
|
||||
this.animateCardHover(card, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡片悬停动画
|
||||
* @param {Element} card - 卡片元素
|
||||
* @param {boolean} isHover - 是否悬停
|
||||
*/
|
||||
animateCardHover(card, isHover) {
|
||||
if (isHover) {
|
||||
card.style.transform = 'translateY(-8px)';
|
||||
card.style.boxShadow = '7px 7px 0 var(--shadow-color)';
|
||||
} else {
|
||||
card.style.transform = 'translateY(0)';
|
||||
card.style.boxShadow = '5px 5px 0 var(--shadow-color)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* VoidRaft主应用类
|
||||
*/
|
||||
class VoidRaftApp {
|
||||
constructor() {
|
||||
this.themeManager = null;
|
||||
this.languageManager = null;
|
||||
this.seoManager = null;
|
||||
this.uiEffects = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化应用
|
||||
*/
|
||||
init() {
|
||||
this.initializeManagers();
|
||||
this.showConsoleBranding();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化各个管理器
|
||||
*/
|
||||
initializeManagers() {
|
||||
this.themeManager = new ThemeManager();
|
||||
this.languageManager = new LanguageManager();
|
||||
this.seoManager = new SEOManager(this.languageManager);
|
||||
this.uiEffects = new UIEffects();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示控制台品牌信息
|
||||
*/
|
||||
showConsoleBranding() {
|
||||
console.log('%c VoidRaft', 'color: #ff006e; font-size: 20px; font-family: "Space Mono", monospace;');
|
||||
console.log('%c An elegant text snippet recording tool designed for developers.', 'color: #073B4C; font-family: "Space Mono", monospace;');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主题管理器
|
||||
*/
|
||||
getThemeManager() {
|
||||
return this.themeManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取语言管理器
|
||||
*/
|
||||
getLanguageManager() {
|
||||
return this.languageManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SEO管理器
|
||||
*/
|
||||
getSEOManager() {
|
||||
return this.seoManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取UI效果管理器
|
||||
*/
|
||||
getUIEffects() {
|
||||
return this.uiEffects;
|
||||
}
|
||||
}
|
||||
|
||||
// 当DOM加载完成时初始化应用
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.voidRaftApp = new VoidRaftApp();
|
||||
});
|
@@ -1,7 +1,4 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as GreetService from "./greetservice.js";
|
||||
export {
|
||||
GreetService
|
||||
};
|
||||
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);
|
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";
|
1511
frontend/bindings/voidraft/internal/models/models.ts
Normal file
1511
frontend/bindings/voidraft/internal/models/models.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* BackupService 提供基于Git的备份功能
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as models$0 from "../models/models.js";
|
||||
|
||||
/**
|
||||
* HandleConfigChange 处理备份配置变更
|
||||
*/
|
||||
export function HandleConfigChange(config: models$0.GitBackupConfig | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(395287784, config) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize 初始化备份服务
|
||||
*/
|
||||
export function Initialize(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1052437974) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* PushToRemote 推送本地更改到远程仓库
|
||||
*/
|
||||
export function PushToRemote(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(262644139) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinitialize 重新初始化备份服务,用于响应配置变更
|
||||
*/
|
||||
export function Reinitialize(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(301562543) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown 服务关闭时的清理工作
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(422131801) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* StartAutoBackup 启动自动备份定时器
|
||||
*/
|
||||
export function StartAutoBackup(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3035755449) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* StopAutoBackup 停止自动备份
|
||||
*/
|
||||
export function StopAutoBackup(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2641894021) as any;
|
||||
return $resultPromise;
|
||||
}
|
@@ -0,0 +1,87 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetBackupConfigChangeCallback 设置备份配置变更回调
|
||||
*/
|
||||
export function SetBackupConfigChangeCallback(callback: any): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3264871659, callback) 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,47 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* DatabaseService provides shared database functionality
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||
|
||||
/**
|
||||
* OnDataPathChanged handles data path changes
|
||||
*/
|
||||
export function OnDataPathChanged(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3652863491) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* RegisterModel 注册模型与表的映射关系
|
||||
*/
|
||||
export function RegisterModel(tableName: string, model: any): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(175397515, tableName, model) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown shuts down the service when the application closes
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3907893632) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup initializes the service when the application starts
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2067840771, options) as any;
|
||||
return $resultPromise;
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* SelectFile 打开文件选择对话框
|
||||
*/
|
||||
export function SelectFile(): Promise<string> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(37302920) 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;
|
||||
}
|
135
frontend/bindings/voidraft/internal/services/documentservice.ts
Normal file
135
frontend/bindings/voidraft/internal/services/documentservice.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* LockDocument 锁定文档,防止删除
|
||||
*/
|
||||
export function LockDocument(id: number): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1889494473, id) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* RestoreDocument restores a deleted document
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* UnlockDocument 解锁文档
|
||||
*/
|
||||
export function UnlockDocument(id: number): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(222307930, id) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateDocumentContent updates the content of a document
|
||||
*/
|
||||
export function UpdateDocumentContent(id: number, content: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3251897116, id, content) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateDocumentTitle updates the title of a document
|
||||
*/
|
||||
export function UpdateDocumentTitle(id: number, title: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2045530459, id, title) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.Document.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Array($$createType1);
|
@@ -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);
|
37
frontend/bindings/voidraft/internal/services/index.ts
Normal file
37
frontend/bindings/voidraft/internal/services/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as BackupService from "./backupservice.js";
|
||||
import * as ConfigService from "./configservice.js";
|
||||
import * as DatabaseService from "./databaseservice.js";
|
||||
import * as DialogService from "./dialogservice.js";
|
||||
import * as DocumentService from "./documentservice.js";
|
||||
import * as ExtensionService from "./extensionservice.js";
|
||||
import * as HotkeyService from "./hotkeyservice.js";
|
||||
import * as KeyBindingService from "./keybindingservice.js";
|
||||
import * as MigrationService from "./migrationservice.js";
|
||||
import * as SelfUpdateService from "./selfupdateservice.js";
|
||||
import * as StartupService from "./startupservice.js";
|
||||
import * as SystemService from "./systemservice.js";
|
||||
import * as TranslationService from "./translationservice.js";
|
||||
import * as TrayService from "./trayservice.js";
|
||||
import * as WindowService from "./windowservice.js";
|
||||
export {
|
||||
BackupService,
|
||||
ConfigService,
|
||||
DatabaseService,
|
||||
DialogService,
|
||||
DocumentService,
|
||||
ExtensionService,
|
||||
HotkeyService,
|
||||
KeyBindingService,
|
||||
MigrationService,
|
||||
SelfUpdateService,
|
||||
StartupService,
|
||||
SystemService,
|
||||
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 {
|
||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
||||
BlockLanguageSelector: typeof import('./src/components/toolbar/BlockLanguageSelector.vue')['default']
|
||||
DocumentSelector: typeof import('./src/components/toolbar/DocumentSelector.vue')['default']
|
||||
LinuxTitleBar: typeof import('./src/components/titlebar/LinuxTitleBar.vue')['default']
|
||||
LoadingScreen: typeof import('./src/components/loading/LoadingScreen.vue')['default']
|
||||
MacOSTitleBar: typeof import('./src/components/titlebar/MacOSTitleBar.vue')['default']
|
||||
MemoryMonitor: typeof import('./src/components/monitor/MemoryMonitor.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Toolbar: typeof import('./src/components/toolbar/Toolbar.vue')['default']
|
||||
WindowsTitleBar: typeof import('./src/components/titlebar/WindowsTitleBar.vue')['default']
|
||||
WindowTitleBar: typeof import('./src/components/titlebar/WindowTitleBar.vue')['default']
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/appicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Wails + Vue + TS</title>
|
||||
<title>voidraft</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
2186
frontend/package-lock.json
generated
2186
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,28 +12,70 @@
|
||||
"lint:fix": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@primeuix/themes": "^1.0.3",
|
||||
"pinia": "^3.0.2",
|
||||
"primevue": "^4.3.3",
|
||||
"sass": "^1.87.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"vue-router": "^4.5.0"
|
||||
"@codemirror/autocomplete": "^6.18.6",
|
||||
"@codemirror/commands": "^6.8.1",
|
||||
"@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.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.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.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.2",
|
||||
"@codemirror/language-data": "^6.5.1",
|
||||
"@codemirror/legacy-modes": "^6.5.1",
|
||||
"@codemirror/lint": "^6.8.5",
|
||||
"@codemirror/search": "^6.5.11",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.38.1",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"codemirror": "^6.0.2",
|
||||
"codemirror-lang-elixir": "^4.0.0",
|
||||
"colors-named": "^1.0.2",
|
||||
"colors-named-hex": "^1.0.2",
|
||||
"franc-min": "^6.2.0",
|
||||
"hsl-matcher": "^1.2.4",
|
||||
"lezer": "^0.13.5",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.4.1",
|
||||
"prettier": "^3.6.2",
|
||||
"remarkable": "^2.0.1",
|
||||
"sass": "^1.89.2",
|
||||
"vue": "^3.5.17",
|
||||
"vue-i18n": "^11.1.10",
|
||||
"vue-pick-colors": "^1.8.0",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.1",
|
||||
"@primevue/auto-import-resolver": "^4.3.3",
|
||||
"@types/node": "^22.14.1",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@lezer/generator": "^1.8.0",
|
||||
"@types/node": "^24.0.14",
|
||||
"@types/remarkable": "^2.0.8",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@wailsio/runtime": "latest",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint-plugin-vue": "^10.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"eslint": "^9.31.0",
|
||||
"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.37.0",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^7.0.4",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.0.1"
|
||||
}
|
||||
}
|
||||
|
28
frontend/public/guesslang.min.js
vendored
Normal file
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,31 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import HelloWorld from './components/HelloWorld.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 {useBackupStore} from '@/stores/backupStore';
|
||||
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const systemStore = useSystemStore();
|
||||
const keybindingStore = useKeybindingStore();
|
||||
const themeStore = useThemeStore();
|
||||
const updateStore = useUpdateStore();
|
||||
const backupStore = useBackupStore();
|
||||
|
||||
// 应用启动时加载配置和初始化系统信息
|
||||
onMounted(async () => {
|
||||
// 并行初始化配置、系统信息和快捷键配置
|
||||
await Promise.all([
|
||||
configStore.initConfig(),
|
||||
systemStore.initializeSystemInfo(),
|
||||
keybindingStore.loadKeyBindings(),
|
||||
]);
|
||||
|
||||
// 初始化语言和主题
|
||||
await configStore.initializeLanguage();
|
||||
themeStore.initializeTheme();
|
||||
|
||||
// 初始化备份服务
|
||||
await backupStore.initialize();
|
||||
|
||||
// 启动时检查更新
|
||||
await updateStore.checkOnStartup();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div>
|
||||
<a wml-openURL="https://wails.io">
|
||||
<img src="/appicon.png" class="logo" alt="Wails logo"/>
|
||||
</a>
|
||||
<a wml-openURL="https://vuejs.org/">
|
||||
<img src="/appicon.png" class="logo vue" alt="Vue logo"/>
|
||||
</a>
|
||||
<div class="app-container">
|
||||
<WindowTitleBar/>
|
||||
<div class="app-content">
|
||||
<router-view/>
|
||||
</div>
|
||||
<HelloWorld msg="Wails + Vue" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
<style scoped lang="scss">
|
||||
.app-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #e80000aa);
|
||||
}
|
||||
.logo.vue:hover {
|
||||
filter: drop-shadow(0 0 2em #42b883aa);
|
||||
|
||||
.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.
Binary file not shown.
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;
|
||||
}
|
5
frontend/src/assets/styles/index.css
Normal file
5
frontend/src/assets/styles/index.css
Normal file
@@ -0,0 +1,5 @@
|
||||
/* 导入所有CSS文件 */
|
||||
@import 'normalize.css';
|
||||
@import 'variables.css';
|
||||
@import "fonts.css";
|
||||
@import 'scrollbar.css';
|
15
frontend/src/assets/styles/normalize.css
vendored
15
frontend/src/assets/styles/normalize.css
vendored
@@ -346,3 +346,18 @@ template {
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 添加全局样式,确保编辑器可以全屏显示 */
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 确保所有容器都能继承高度 */
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
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,26 +0,0 @@
|
||||
html {
|
||||
background-color: rgba(27, 38, 54, 1);
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: white;
|
||||
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(""),
|
||||
url("../fonts/nunito-v16-latin-regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
}
|
248
frontend/src/assets/styles/variables.css
Normal file
248
frontend/src/assets/styles/variables.css
Normal file
@@ -0,0 +1,248 @@
|
||||
:root {
|
||||
/* 编辑器区域 */
|
||||
--text-primary: #9BB586; /* 内容区域字体颜色 */
|
||||
|
||||
/* 深色主题颜色变量 */
|
||||
--dark-toolbar-bg: #2d2d2d;
|
||||
--dark-toolbar-border: #404040;
|
||||
--dark-toolbar-text: #ffffff;
|
||||
--dark-toolbar-text-secondary: #cccccc;
|
||||
--dark-toolbar-button-hover: #404040;
|
||||
--dark-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);
|
||||
}
|
@@ -1,47 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import {GreetService} from "../../bindings/voidraft";
|
||||
import {Events} from "@wailsio/runtime";
|
||||
|
||||
defineProps<{ msg: string }>();
|
||||
|
||||
const name = ref('');
|
||||
const result = ref('Please enter your name below 👇');
|
||||
const time = ref('Listening for Time event...');
|
||||
|
||||
const doGreet = () => {
|
||||
let localName = name.value;
|
||||
if (!localName) {
|
||||
localName = 'anonymous';
|
||||
}
|
||||
GreetService.Greet(localName).then((resultValue: string) => {
|
||||
result.value = resultValue;
|
||||
}).catch((err: Error) => {
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
Events.On('time', (timeValue: { data: string }) => {
|
||||
time.value = timeValue.data;
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="result">{{ result }}</div>
|
||||
<div class="card">
|
||||
<div class="input-box">
|
||||
<input class="input" v-model="name" type="text" autocomplete="off"/>
|
||||
<button class="btn" @click="doGreet">Greet</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<div><p>Click on the Wails logo to learn more</p></div>
|
||||
<div><p>{{ time }}</p></div>
|
||||
</div>
|
||||
</template>
|
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>
|
774
frontend/src/components/toolbar/DocumentSelector.vue
Normal file
774
frontend/src/components/toolbar/DocumentSelector.vue
Normal file
@@ -0,0 +1,774 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, nextTick, onMounted, onUnmounted, ref} from 'vue';
|
||||
import {useDocumentStore} from '@/stores/documentStore';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import type {Document} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {useWindowStore} from "@/stores/windowStore";
|
||||
|
||||
const documentStore = useDocumentStore();
|
||||
const windowStore = useWindowStore();
|
||||
const {t} = useI18n();
|
||||
|
||||
// 组件状态
|
||||
const showMenu = ref(false);
|
||||
const inputValue = ref('');
|
||||
const inputRef = ref<HTMLInputElement>();
|
||||
const editingId = ref<number | null>(null);
|
||||
const editingTitle = ref('');
|
||||
const editInputRef = ref<HTMLInputElement>();
|
||||
const deleteConfirmId = ref<number | null>(null);
|
||||
// 添加错误提示状态
|
||||
const alreadyOpenDocId = ref<number | null>(null);
|
||||
const errorMessageTimer = ref<number | null>(null);
|
||||
|
||||
// 过滤后的文档列表 + 创建选项
|
||||
const filteredItems = computed(() => {
|
||||
const docs = documentStore.documentList;
|
||||
const query = inputValue.value.trim();
|
||||
|
||||
if (!query) {
|
||||
return docs;
|
||||
}
|
||||
|
||||
// 过滤匹配的文档
|
||||
const filtered = docs.filter(doc =>
|
||||
doc.title.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
|
||||
// 如果输入的不是已存在文档的完整标题,添加创建选项
|
||||
const exactMatch = docs.some(doc => doc.title.toLowerCase() === query.toLowerCase());
|
||||
if (!exactMatch && query.length > 0) {
|
||||
return [
|
||||
{id: -1, title: t('toolbar.createDocument') + ` "${query}"`, isCreateOption: true} as any,
|
||||
...filtered
|
||||
];
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
// 当前文档显示名称
|
||||
const currentDocName = computed(() => {
|
||||
if (!documentStore.currentDocument) return t('toolbar.selectDocument');
|
||||
const title = documentStore.currentDocument.title;
|
||||
return title.length > 12 ? title.substring(0, 12) + '...' : title;
|
||||
});
|
||||
|
||||
// 打开菜单
|
||||
const openMenu = async () => {
|
||||
showMenu.value = true;
|
||||
await documentStore.updateDocuments();
|
||||
nextTick(() => {
|
||||
inputRef.value?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭菜单
|
||||
const closeMenu = () => {
|
||||
showMenu.value = false;
|
||||
inputValue.value = '';
|
||||
editingId.value = null;
|
||||
editingTitle.value = '';
|
||||
deleteConfirmId.value = null;
|
||||
|
||||
// 清除错误状态和定时器
|
||||
clearErrorMessage();
|
||||
};
|
||||
|
||||
// 清除错误提示和定时器
|
||||
const clearErrorMessage = () => {
|
||||
if (errorMessageTimer.value) {
|
||||
clearTimeout(errorMessageTimer.value);
|
||||
errorMessageTimer.value = null;
|
||||
}
|
||||
alreadyOpenDocId.value = null;
|
||||
};
|
||||
|
||||
// 切换菜单
|
||||
const toggleMenu = () => {
|
||||
if (showMenu.value) {
|
||||
closeMenu();
|
||||
} else {
|
||||
openMenu();
|
||||
}
|
||||
};
|
||||
|
||||
// 选择文档或创建文档
|
||||
const selectItem = async (item: any) => {
|
||||
if (item.isCreateOption) {
|
||||
// 创建新文档
|
||||
await createDoc(inputValue.value.trim());
|
||||
} else {
|
||||
// 选择现有文档
|
||||
await selectDoc(item);
|
||||
}
|
||||
};
|
||||
|
||||
// 选择文档
|
||||
const selectDoc = async (doc: Document) => {
|
||||
try {
|
||||
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
||||
if (hasOpen) {
|
||||
// 设置错误状态并启动定时器
|
||||
alreadyOpenDocId.value = doc.id;
|
||||
|
||||
// 清除之前的定时器(如果存在)
|
||||
if (errorMessageTimer.value) {
|
||||
clearTimeout(errorMessageTimer.value);
|
||||
}
|
||||
|
||||
// 设置新的定时器,3秒后清除错误信息
|
||||
errorMessageTimer.value = window.setTimeout(() => {
|
||||
alreadyOpenDocId.value = null;
|
||||
errorMessageTimer.value = null;
|
||||
}, 3000);
|
||||
return;
|
||||
}
|
||||
const success = await documentStore.openDocument(doc.id);
|
||||
if (success) {
|
||||
closeMenu();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to switch documents:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 文档名称长度限制
|
||||
const MAX_TITLE_LENGTH = 50;
|
||||
|
||||
// 验证文档名称
|
||||
const validateTitle = (title: string): string | null => {
|
||||
if (!title.trim()) {
|
||||
return t('toolbar.documentNameRequired');
|
||||
}
|
||||
if (title.trim().length > MAX_TITLE_LENGTH) {
|
||||
return t('toolbar.documentNameTooLong', {max: MAX_TITLE_LENGTH});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 创建文档
|
||||
const createDoc = async (title: string) => {
|
||||
const trimmedTitle = title.trim();
|
||||
const error = validateTitle(trimmedTitle);
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newDoc = await documentStore.createNewDocument(trimmedTitle);
|
||||
if (newDoc) {
|
||||
await selectDoc(newDoc);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create document:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始重命名
|
||||
const startRename = (doc: Document, event: Event) => {
|
||||
event.stopPropagation();
|
||||
editingId.value = doc.id;
|
||||
editingTitle.value = doc.title;
|
||||
deleteConfirmId.value = null; // 清除删除确认状态
|
||||
nextTick(() => {
|
||||
editInputRef.value?.focus();
|
||||
editInputRef.value?.select();
|
||||
});
|
||||
};
|
||||
|
||||
// 保存编辑
|
||||
const saveEdit = async () => {
|
||||
if (editingId.value && editingTitle.value.trim()) {
|
||||
const trimmedTitle = editingTitle.value.trim();
|
||||
const error = validateTitle(trimmedTitle);
|
||||
if (error) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await documentStore.updateDocumentMetadata(editingId.value, trimmedTitle);
|
||||
await documentStore.updateDocuments();
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
editingId.value = null;
|
||||
editingTitle.value = '';
|
||||
};
|
||||
|
||||
// 在新窗口打开文档
|
||||
const openInNewWindow = async (doc: Document, event: Event) => {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
await documentStore.openDocumentInNewWindow(doc.id);
|
||||
} catch (error) {
|
||||
console.error('Failed to open document in new window:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理删除 - 简化确认机制
|
||||
const handleDelete = async (doc: Document, event: Event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (deleteConfirmId.value === doc.id) {
|
||||
// 确认删除前检查文档是否在其他窗口打开
|
||||
try {
|
||||
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
||||
if (hasOpen) {
|
||||
// 设置错误状态并启动定时器
|
||||
alreadyOpenDocId.value = doc.id;
|
||||
|
||||
// 清除之前的定时器(如果存在)
|
||||
if (errorMessageTimer.value) {
|
||||
clearTimeout(errorMessageTimer.value);
|
||||
}
|
||||
|
||||
// 设置新的定时器,3秒后清除错误信息
|
||||
errorMessageTimer.value = window.setTimeout(() => {
|
||||
alreadyOpenDocId.value = null;
|
||||
errorMessageTimer.value = null;
|
||||
}, 3000);
|
||||
|
||||
// 取消删除确认状态
|
||||
deleteConfirmId.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
await documentStore.deleteDocument(doc.id);
|
||||
await documentStore.updateDocuments();
|
||||
|
||||
// 如果删除的是当前文档,切换到第一个文档
|
||||
if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) {
|
||||
const firstDoc = documentStore.documentList[0];
|
||||
if (firstDoc) {
|
||||
await selectDoc(firstDoc);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('deleted failed:', error);
|
||||
}
|
||||
deleteConfirmId.value = null;
|
||||
} else {
|
||||
// 进入确认状态
|
||||
deleteConfirmId.value = doc.id;
|
||||
editingId.value = null; // 清除编辑状态
|
||||
|
||||
// 3秒后自动取消确认状态
|
||||
setTimeout(() => {
|
||||
if (deleteConfirmId.value === doc.id) {
|
||||
deleteConfirmId.value = null;
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (dateString: string | null) => {
|
||||
if (!dateString) return t('toolbar.unknownTime');
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) return t('toolbar.invalidDate');
|
||||
|
||||
// 根据当前语言显示时间格式
|
||||
const locale = t('locale') === 'zh-CN' ? 'zh-CN' : 'en-US';
|
||||
return date.toLocaleString(locale, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
} catch (error) {
|
||||
return t('toolbar.timeError');
|
||||
}
|
||||
};
|
||||
|
||||
// 键盘事件
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
if (editingId.value) {
|
||||
editingId.value = null;
|
||||
editingTitle.value = '';
|
||||
} else if (deleteConfirmId.value) {
|
||||
deleteConfirmId.value = null;
|
||||
} else {
|
||||
closeMenu();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 输入框键盘事件
|
||||
const handleInputKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
const query = inputValue.value.trim();
|
||||
if (query) {
|
||||
// 如果有匹配的项目,选择第一个
|
||||
if (filteredItems.value.length > 0) {
|
||||
selectItem(filteredItems.value[0]);
|
||||
}
|
||||
}
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
closeMenu();
|
||||
}
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
// 编辑键盘事件
|
||||
const handleEditKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
saveEdit();
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
editingId.value = null;
|
||||
editingTitle.value = '';
|
||||
}
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
// 点击外部关闭
|
||||
const handleClickOutside = (event: Event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('.document-selector')) {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
// 清理定时器
|
||||
if (errorMessageTimer.value) {
|
||||
clearTimeout(errorMessageTimer.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="document-selector">
|
||||
<!-- 选择器按钮 -->
|
||||
<button class="doc-btn" @click="toggleMenu">
|
||||
<span class="doc-name">{{ currentDocName }}</span>
|
||||
<span class="arrow" :class="{ open: showMenu }">▲</span>
|
||||
</button>
|
||||
|
||||
<!-- 菜单 -->
|
||||
<div v-if="showMenu" class="doc-menu">
|
||||
<!-- 输入框 -->
|
||||
<div class="input-box">
|
||||
<input
|
||||
ref="inputRef"
|
||||
v-model="inputValue"
|
||||
type="text"
|
||||
class="main-input"
|
||||
:placeholder="t('toolbar.searchOrCreateDocument')"
|
||||
:maxlength="MAX_TITLE_LENGTH"
|
||||
@keydown="handleInputKeydown"
|
||||
/>
|
||||
<svg class="input-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.35-4.35"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<div class="item-list">
|
||||
<div
|
||||
v-for="item in filteredItems"
|
||||
:key="item.id"
|
||||
class="list-item"
|
||||
:class="{
|
||||
'active': !item.isCreateOption && documentStore.currentDocument?.id === item.id,
|
||||
'create-item': item.isCreateOption
|
||||
}"
|
||||
@click="selectItem(item)"
|
||||
>
|
||||
<!-- 创建选项 -->
|
||||
<div v-if="item.isCreateOption" class="create-option">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14"></path>
|
||||
<path d="M12 5v14"></path>
|
||||
</svg>
|
||||
<span>{{ item.title }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 文档项 -->
|
||||
<div v-else class="doc-item-content">
|
||||
<!-- 普通显示 -->
|
||||
<div v-if="editingId !== item.id" class="doc-info">
|
||||
<div class="doc-title">{{ item.title }}</div>
|
||||
<!-- 根据状态显示错误信息或时间 -->
|
||||
<div v-if="alreadyOpenDocId === item.id" class="doc-error">
|
||||
{{ t('toolbar.alreadyOpenInNewWindow') }}
|
||||
</div>
|
||||
<div v-else class="doc-date">{{ formatTime(item.updatedAt) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑状态 -->
|
||||
<div v-else class="doc-edit">
|
||||
<input
|
||||
:ref="el => editInputRef = el as HTMLInputElement"
|
||||
v-model="editingTitle"
|
||||
type="text"
|
||||
class="edit-input"
|
||||
:maxlength="MAX_TITLE_LENGTH"
|
||||
@keydown="handleEditKeydown"
|
||||
@blur="saveEdit"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div v-if="editingId !== item.id" class="doc-actions">
|
||||
<!-- 只有非当前文档才显示在新窗口打开按钮 -->
|
||||
<button
|
||||
v-if="documentStore.currentDocument?.id !== item.id"
|
||||
class="action-btn"
|
||||
@click="openInNewWindow(item, $event)"
|
||||
:title="t('toolbar.openInNewWindow')"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor">
|
||||
<path
|
||||
d="M172.8 1017.6c-89.6 0-166.4-70.4-166.4-166.4V441.6c0-89.6 70.4-166.4 166.4-166.4h416c89.6 0 166.4 70.4 166.4 166.4v416c0 89.6-70.4 166.4-166.4 166.4l-416-6.4z m0-659.2c-51.2 0-89.6 38.4-89.6 89.6v416c0 51.2 38.4 89.6 89.6 89.6h416c51.2 0 89.6-38.4 89.6-89.6V441.6c0-51.2-38.4-89.6-89.6-89.6H172.8z"></path>
|
||||
<path
|
||||
d="M851.2 19.2H435.2C339.2 19.2 268.8 96 268.8 185.6v25.6h70.4v-25.6c0-51.2 38.4-89.6 89.6-89.6h409.6c51.2 0 89.6 38.4 89.6 89.6v409.6c0 51.2-38.4 89.6-89.6 89.6h-38.4V768h51.2c96 0 166.4-76.8 166.4-166.4V185.6c0-96-76.8-166.4-166.4-166.4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="action-btn" @click="startRename(item, $event)" :title="t('toolbar.rename')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="documentStore.documentList.length > 1 && item.id !== 1"
|
||||
class="action-btn delete-btn"
|
||||
:class="{ 'delete-confirm': deleteConfirmId === item.id }"
|
||||
@click="handleDelete(item, $event)"
|
||||
:title="deleteConfirmId === item.id ? t('toolbar.confirmDelete') : t('toolbar.delete')"
|
||||
>
|
||||
<svg v-if="deleteConfirmId !== item.id" xmlns="http://www.w3.org/2000/svg" width="12" height="12"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<polyline points="3,6 5,6 21,6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
<span v-else class="confirm-text">{{ t('toolbar.confirm') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="filteredItems.length === 0" class="empty">
|
||||
{{ t('toolbar.noDocumentFound') }}
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="documentStore.isLoading" class="loading">
|
||||
{{ t('toolbar.loading') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.document-selector {
|
||||
position: relative;
|
||||
|
||||
.doc-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--border-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.doc-name {
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 8px;
|
||||
margin-left: 2px;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.doc-menu {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
margin-bottom: 4px;
|
||||
width: 260px;
|
||||
max-height: 320px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
|
||||
.input-box {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
.main-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 2px;
|
||||
padding: 5px 8px 5px 26px;
|
||||
font-size: 11px;
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.item-list {
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
|
||||
.list-item {
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--selection-bg);
|
||||
|
||||
.doc-item-content .doc-info {
|
||||
.doc-title {
|
||||
color: var(--selection-text);
|
||||
}
|
||||
|
||||
.doc-date, .doc-error {
|
||||
color: var(--selection-text);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.create-item {
|
||||
.create-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.doc-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 8px;
|
||||
|
||||
.doc-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.doc-title {
|
||||
font-size: 12px;
|
||||
margin-bottom: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.doc-date {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.doc-error {
|
||||
font-size: 10px;
|
||||
color: var(--text-danger);
|
||||
font-weight: 500;
|
||||
animation: fadeInOut 3s forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.doc-edit {
|
||||
flex: 1;
|
||||
|
||||
.edit-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 2px;
|
||||
padding: 4px 6px;
|
||||
font-size: 11px;
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.doc-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
.action-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.delete-btn:hover {
|
||||
color: var(--text-danger);
|
||||
}
|
||||
|
||||
&.delete-confirm {
|
||||
background-color: var(--text-danger);
|
||||
color: white;
|
||||
|
||||
.confirm-text {
|
||||
font-size: 10px;
|
||||
padding: 0 4px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--text-danger);
|
||||
color: white !important; // 确保确认状态下文字始终为白色
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .doc-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.empty, .loading {
|
||||
padding: 12px 8px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义滚动条
|
||||
.item-list {
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--border-color);
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
70% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user