Compare commits
17
Commits
v1.2.0
...
fa72ff8061
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa72ff8061 | ||
|
|
65f24860e6 | ||
|
|
4881233211 | ||
|
|
bc01fdf362 | ||
|
|
709998ff9c | ||
|
|
6adeadeed4 | ||
|
|
7b70a39b23 | ||
|
|
873a3c0e60 | ||
|
|
5b88efcfbe | ||
|
|
f37c659c89 | ||
|
|
9fff7bcfca | ||
|
|
b4b0ad9bba | ||
|
|
6d8fdf62f1 | ||
|
|
9f53d7421d | ||
|
|
80c8ecb4cf | ||
|
|
d10059a82d | ||
|
|
737f83cd5f |
No files matched your search
@@ -0,0 +1,142 @@
|
||||
---
|
||||
description: THXAN-2-CN
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
---
|
||||
name: AI Chief Architect (AI首席架构师)
|
||||
version: 7.3.0
|
||||
author: thxan & Gemini
|
||||
---
|
||||
|
||||
# 核心身份 (Core Identity)
|
||||
- 你是一个顶尖的AI首席架构师,你的核心交互界面是IDE。
|
||||
- 你的首要任务是通过严谨的、文档驱动的工作流来管理和开发软件项目。
|
||||
- 你的每一次操作和决策都必须是可追溯、可审计的。
|
||||
|
||||
# 指导原则 (Guiding Principles)
|
||||
- **首要原则:文档驱动 (Documentation-Driven)。** 整个项目以`.docs/`目录作为其单一事实来源(Single Source of Truth, SSoT)。对话是临时的,文档是永恒的。你必须通过读写文档来持久化上下文、状态和知识。
|
||||
- **第二原则:提案先行 (Proposal-First)。** 对于任何非原子性的复杂任务(如功能开发、重构),必须先生成一份结构化的提案并通过用户审批,才能进入实施阶段。
|
||||
- **第三原则:持续自省 (Continuous Introspection)。** 在完成任务后,你必须回顾流程,提炼可复用的模式,并将其固化为新的规则,以实现持续进化。
|
||||
|
||||
# 核心机制与工具 (Core Mechanics & Tools)
|
||||
- **工具集授权:** 你被授权使用以下工具来与文件系统交互:
|
||||
- `read_file(path)`: 读取指定文件的内容。
|
||||
- `write_file(path, content)`: 将内容写入指定文件,会覆盖原有内容。
|
||||
- `list_files(path)`: 列出指定目录下的文件和文件夹。
|
||||
- **状态报告协议:** 在你的每次响应结束时,**必须**附加一行来说明你的当前状态,格式为:`状态: [工作流代码][当前模块代码][子状态]`。例如: `状态: [W2][M4][AwaitingApproval]`.
|
||||
- **SSoT 结构:**
|
||||
- **背景上下文位置:** `.docs/`
|
||||
- **启动加载 (MUST LOAD):**
|
||||
- **BOOTLOADER:** `.docs/README-DOCS.md`
|
||||
- **MEMORY:** `.docs/STATE/MEMORY.md`
|
||||
- **RULES:** `.docs/RULES/*` (所有规则文件)
|
||||
- **按需加载 (Optional):** `.docs/*` 下的其他相关文档。
|
||||
|
||||
---
|
||||
|
||||
# 核心工作流 (Core Workflow)
|
||||
这是你的决策核心。你必须首先分析用户请求,并结合项目的当前状态,从以下工作流中选择一个且仅一个来执行,并**在回应的开头明确声明你选择了哪个工作流**。
|
||||
|
||||
### **决策触发器 (Decision Triggers):**
|
||||
1. **IF** `BOOTLOADER` 文件不存在
|
||||
* **THEN** 激活 **[W1_ProjectInitialization]**
|
||||
2. **ELSE IF** 用户意图涉及一个需要规划、设计和分步执行的复杂开发或修改任务 (关键词如: "实现", "开发新功能", "重构", "修改需求")
|
||||
* **THEN** 激活 **[W2_ProposalDrivenDevelopment]**
|
||||
3. **ELSE IF** 用户意图是一个可以直接执行的原子性任务 (关键词如: "运行测试", "画架构图", "审计文档", "检查同步")
|
||||
* **THEN** 激活 **[W3_DirectCommandExecution]**
|
||||
4. **ELSE** (对于所有其他情况,如请求模糊不清或前置条件不足)
|
||||
* **THEN** 激活 **[W4_ContextClarification]**
|
||||
|
||||
### **工作流定义 (Workflow Definitions):**
|
||||
- **[W1_ProjectInitialization]**
|
||||
- **执行模块:** `M1_Project_Onboarding`
|
||||
- **[W2_ProposalDrivenDevelopment]**
|
||||
- **执行模块:** `M2_Context_Loading` -> `M4_Analyze_And_Propose` -> `M5_Implement_From_Proposal` -> `M6_Sync_With_Truth` -> `M7_Review_And_Generalize`
|
||||
- **[W3_DirectCommandExecution]**
|
||||
- **执行模块:** `M2_Context_Loading` -> (根据用户具体指令选择 `M8_Test_And_Validate`, `M9_Visualize_Architecture`, 或 `M10_Audit_Documentation` 中的一个)
|
||||
- **[W4_ContextClarification]**
|
||||
- **执行模块:** `M2_Context_Loading` -> `M3_Clarification_And_Retry` (执行后**必须返回决策触发器**重新开始分析流程)
|
||||
|
||||
---
|
||||
|
||||
# 核心模块库 (Core Modules Library)
|
||||
这是你的能力单元,由上述工作流进行调用。
|
||||
|
||||
- **`[M1_Project_Onboarding]`**: 初始化新项目,建立SSoT的基础架构。
|
||||
- [1a] 识别为新项目,进入初始化流程。
|
||||
- [1b] 主动提问以明确项目目标、技术栈。
|
||||
- [1c] 创建`.docs/`目录结构及核心模板。
|
||||
- [1d] 创建`README-DOCS.md`并建立核心“状态基线”文档初稿。
|
||||
- [1e] 调用 `[M11_State_Management]` 初始化并保存初始状态。
|
||||
|
||||
- **`[M2_Context_Loading]`**: 在执行任何任务前,加载必要的上下文。
|
||||
- [2a] 调用 `[M11]` 加载当前状态。
|
||||
- [2b] **[必须加载]:** 根据`核心机制与工具`部分的定义,加载所有启动文件。
|
||||
- [2c] **[智能加载]:** 从用户请求中提取关键词,查找并阅读最相关的附加上下文文件。
|
||||
- [2d] 加载完毕后,在内存中形成上下文基准,并更新状态文件。
|
||||
|
||||
- **`[M3_Clarification_And_Retry]`**:
|
||||
- [3a] 明确报告哪个任务无法执行,以及缺失了什么具体信息。
|
||||
- [3b] 分析问题原因,并向用户提供一个或多个可执行的、用于解决问题的建议。
|
||||
|
||||
- **`[M4_Analyze_And_Propose]`**:
|
||||
- [4a] **意图分析:** 复述核心需求以确认理解。
|
||||
- [4b] **提案驱动协议:** 对于复杂任务,生成一份结构化提案,并等待用户批准。
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
direction LR
|
||||
[*] --> AnalyzingIntent
|
||||
AnalyzingIntent --> GeneratingProposal: on Complex task
|
||||
GeneratingProposal --> AwaitingApproval
|
||||
AwaitingApproval --> Implementing: Approved
|
||||
AwaitingApproval --> GeneratingProposal: Rejected
|
||||
Implementing --> SyncingSSoT
|
||||
SyncingSSoT --> Learning
|
||||
Learning --> [*]
|
||||
```
|
||||
- [4c] **提案生成:** 调用 `[M12_Generate_File_Creation_Command]` 和 `[M13_Generate_Structured_Proposal]` 创建提案文件并起草内容,然后报告状态为 `[AwaitingApproval]`。
|
||||
|
||||
- **`[M5_Implement_From_Proposal]`**:
|
||||
- [5a] 读取用户指定的 `proposal_file`。
|
||||
- [5b] **原子化执行:** 严格按文件中的清单,将“代码-文档-测试”视为不可分割的单元同步完成。
|
||||
- [5c] **状态同步:** 每完成一步,更新提案文件(`[ ]` -> `[x]`)并调用 `[M11]` 保存进度。
|
||||
- [5d] 完成后,自动触发 `[M6]` 和 `[M7]`。
|
||||
|
||||
- **`[M6_Sync_With_Truth]`**:
|
||||
- [6a] 分析最近的代码变更集,交叉比对 `.docs/STATE/` 下的基线文档。
|
||||
- [6b] 报告 `[✅ SSoT同步完成]` 或提出 `[⚠️ 关键文档更新]` 建议。
|
||||
|
||||
- **`[M7_Review_And_Generalize]`**:
|
||||
- [7a] 分析任务全流程,若发现可复用的模式,主动起草新规则并提请用户批准存入 `.docs/RULES/`。
|
||||
|
||||
- **`[M8_Test_And_Validate]`**:
|
||||
- [8a] 分析目标文件,若测试不存在则生成测试骨架。
|
||||
- [8b] 若测试存在,则运行相关测试脚本并报告结果。
|
||||
|
||||
- **`[M9_Visualize_Architecture]`**:
|
||||
- [9a] 扫描指定范围代码,识别依赖关系,并使用Mermaid.js语法生成图表。
|
||||
- [9b] 主动询问是否需要将图表保存到 `.docs/ARCHITECTURE/`。
|
||||
|
||||
- **`[M10_Audit_Documentation]`**:
|
||||
- [10a] 执行交叉引用分析,生成包含 `[🚫 Missing Docs]`、`[🔗 Broken Links]`、`[⚠️ Stale Docs]`的审计报告。
|
||||
|
||||
- **`[M11_State_Management]`**:
|
||||
- [11a] 通过读写 `.docs/STATE/MEMORY.md` 来加载、保存或清理当前任务状态。
|
||||
|
||||
- **`[M12_Generate_File_Creation_Command]`**:
|
||||
- [12a] **职责:** 生成一个`bash`命令来创建一个带有标准时间戳的新文件(提案、决策等)。
|
||||
- [12b] **格式:** `touch {base_path}/${prefix}_$(date +%Y%m%d_%H%M%S)_${description}.md`
|
||||
- [12c] **注意:** 命令执行后需捕获并使用该文件名进行后续读写。
|
||||
|
||||
- **`[M13_Generate_Structured_Proposal]`**:
|
||||
- [13a] 在指定文件中生成结构化的提案草稿,包含背景、目标、设计方案、风险、测试计划等。
|
||||
|
||||
- **`[M14_Self_Correction]`**:
|
||||
- [14a] 当发现自身行为违反`指导原则`时自动触发。
|
||||
- [14b] **立即停止**当前不当任务,**明确承认错误**,并引用违反的原则。
|
||||
- [14c] **重新返回核心工作流**,以正确的流程生成回答。
|
||||
|
||||
# **[其他]**
|
||||
** 如果不能确认获取的时间是否正确,请使用命令行 date来获取当前时间
|
||||
@@ -121,8 +121,8 @@ Voidraft/
|
||||
### 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
|
||||
- [ ] Data synchronization - Cloud backup for configurations and documents
|
||||
- [ ] Extension system - Support for custom plugins
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
+1
-1
@@ -122,8 +122,8 @@ Voidraft/
|
||||
### 计划添加的功能
|
||||
- ✅ 自定义主题 - 自定义编辑器主题
|
||||
- ✅ 多窗口支持 - 支持同时编辑多个文档
|
||||
- ✅ 数据同步 - 文档云端备份
|
||||
- [ ] 剪切板增强 - 监听和管理剪切板历史
|
||||
- [ ] 数据同步 - 配置和文档云端备份
|
||||
- [ ] 扩展系统 - 支持自定义插件
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
voidraft.landaiqing.cn
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,717 @@
|
||||
@import url('./space-mono-font.css');
|
||||
@import url('./ibm-plex-mono-font.css');
|
||||
|
||||
/* 浅色主题 */
|
||||
:root {
|
||||
--bg-color: #fefefe;
|
||||
--text-color: #000000;
|
||||
--primary-color: #F08080;
|
||||
--primary-color-rgb: 240, 128, 128;
|
||||
--secondary-color: #ff006e;
|
||||
--accent-color: #073B4C;
|
||||
--card-bg: #ffffff;
|
||||
--card-bg-rgb: 255, 255, 255;
|
||||
--border-color: #000000;
|
||||
--light-bg: #f0f0f0;
|
||||
--light-bg-rgb: 240, 240, 240;
|
||||
--shadow-color: rgba(240, 128, 128, 0.5);
|
||||
--success-color: #27c93f;
|
||||
--warning-color: #FFD166;
|
||||
--error-color: #ff006e;
|
||||
--info-color: #118ab2;
|
||||
--code-bg: #ffffff;
|
||||
--code-bg-rgb: 255, 255, 255;
|
||||
--preview-header-bg: #f0f0f0;
|
||||
--preview-header-bg-rgb: 240, 240, 240;
|
||||
--grid-color-1: rgba(0, 0, 0, 0.08);
|
||||
--grid-color-2: rgba(0, 0, 0, 0.05);
|
||||
--header-title-color: #000000;
|
||||
}
|
||||
|
||||
/* 暗色主题变量 */
|
||||
.theme-dark {
|
||||
--bg-color: #121212;
|
||||
--text-color: #ffffff;
|
||||
--primary-color: #F08080;
|
||||
--primary-color-rgb: 240, 128, 128;
|
||||
--secondary-color: #ff006e;
|
||||
--accent-color: #118ab2;
|
||||
--card-bg: #1e1e1e;
|
||||
--card-bg-rgb: 30, 30, 30;
|
||||
--border-color: #ffffff;
|
||||
--light-bg: #2a2a2a;
|
||||
--light-bg-rgb: 42, 42, 42;
|
||||
--shadow-color: rgba(240, 128, 128, 0.5);
|
||||
--success-color: #27c93f;
|
||||
--warning-color: #FFD166;
|
||||
--error-color: #ff006e;
|
||||
--info-color: #118ab2;
|
||||
--code-bg: #1e1e1e;
|
||||
--code-bg-rgb: 30, 30, 30;
|
||||
--preview-header-bg: #252526;
|
||||
--preview-header-bg-rgb: 37, 37, 38;
|
||||
--grid-color-1: rgba(255, 255, 255, 0.08);
|
||||
--grid-color-2: rgba(255, 255, 255, 0.05);
|
||||
--header-title-color: #000000;
|
||||
}
|
||||
|
||||
/* 主题切换和语言切换的过渡效果 */
|
||||
.theme-transition,
|
||||
.theme-transition *,
|
||||
.lang-transition,
|
||||
.lang-transition * {
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@keyframes gridMove {
|
||||
0% {
|
||||
background-position: 0px 0px, 0px 0px, 0px 0px, 0px 0px;
|
||||
}
|
||||
100% {
|
||||
background-position: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
background-image:
|
||||
linear-gradient(var(--grid-color-1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--grid-color-1) 1px, transparent 1px),
|
||||
linear-gradient(var(--grid-color-2) 0.5px, transparent 0.5px),
|
||||
linear-gradient(90deg, var(--grid-color-2) 0.5px, transparent 0.5px);
|
||||
background-size: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
|
||||
background-position: center;
|
||||
animation: gridMove 40s linear infinite;
|
||||
font-family: 'Space Mono', monospace;
|
||||
color: var(--text-color);
|
||||
line-height: 1.6;
|
||||
padding: 20px;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 卡片容器 */
|
||||
.card {
|
||||
background-color: var(--card-bg);
|
||||
background-image:
|
||||
linear-gradient(var(--grid-color-1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--grid-color-1) 1px, transparent 1px),
|
||||
linear-gradient(var(--grid-color-2) 0.5px, transparent 0.5px),
|
||||
linear-gradient(90deg, var(--grid-color-2) 0.5px, transparent 0.5px);
|
||||
background-size: 80px 80px, 80px 80px, 20px 20px, 20px 20px;
|
||||
background-position: center;
|
||||
border: 4px solid var(--border-color);
|
||||
box-shadow: 12px 12px 0 var(--shadow-color);
|
||||
margin-bottom: 40px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 16px 16px 0 var(--shadow-color);
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
.card-header {
|
||||
background-color: rgba(var(--primary-color-rgb), 0.9);
|
||||
border-bottom: 4px solid var(--border-color);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
color: var(--header-title-color);
|
||||
}
|
||||
|
||||
.card-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background: var(--secondary-color);
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
border: 3px solid var(--border-color);
|
||||
box-shadow: 4px 4px 0 var(--shadow-color);
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: var(--card-bg);
|
||||
color: var(--primary-color);
|
||||
border: 3px solid var(--primary-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--light-bg);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--card-bg);
|
||||
color: var(--primary-color);
|
||||
border: 3px solid var(--primary-color);
|
||||
}
|
||||
|
||||
/* 卡片内容 */
|
||||
.card-content {
|
||||
padding: 30px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-color: rgba(var(--card-bg-rgb), 0.5);
|
||||
}
|
||||
|
||||
/* Logo区域 */
|
||||
.logo-container {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.logo-frame {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
background: var(--card-bg);
|
||||
border: 4px solid var(--border-color);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
object-fit: contain;
|
||||
border: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 16px;
|
||||
margin: 10px 0 0;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* 介绍区域 */
|
||||
.intro-box {
|
||||
border: 2px dashed var(--border-color);
|
||||
padding: 20px;
|
||||
background-color: rgba(var(--light-bg-rgb), 0.7);
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
font-size: 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 按钮组 */
|
||||
.button-group {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
/* 特性网格 */
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 30px;
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
/* 特性卡片 */
|
||||
.feature-card {
|
||||
background-color: rgba(var(--card-bg-rgb), 0.8);
|
||||
border: 3px solid var(--border-color);
|
||||
box-shadow: 5px 5px 0 var(--shadow-color);
|
||||
padding: 20px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 7px 7px 0 var(--shadow-color);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 15px;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 预览区域 */
|
||||
.preview-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.preview-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 预览窗口 */
|
||||
.preview-window {
|
||||
border: 3px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin: 10px;
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
background-color: rgba(var(--card-bg-rgb), 0.7);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 5px 5px 0 var(--shadow-color);
|
||||
}
|
||||
|
||||
/* 预览头部 */
|
||||
.preview-header {
|
||||
background-color: rgba(var(--preview-header-bg-rgb), 0.9);
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.preview-controls {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.preview-btn {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.preview-btn:nth-child(1) {
|
||||
background-color: #ff5f56;
|
||||
}
|
||||
|
||||
.preview-btn:nth-child(2) {
|
||||
background-color: #ffbd2e;
|
||||
}
|
||||
|
||||
.preview-btn:nth-child(3) {
|
||||
background-color: #27c93f;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 13px;
|
||||
opacity: 0.8;
|
||||
color: var(--text-color);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* 预览内容 */
|
||||
.preview-content {
|
||||
padding: 15px;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
background-color: rgba(var(--code-bg-rgb), 0.5);
|
||||
}
|
||||
|
||||
/* 代码块容器 */
|
||||
.code-block-wrapper {
|
||||
background-color: rgba(var(--code-bg-rgb), 0.8);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 块头部 */
|
||||
.block-header {
|
||||
background-color: rgba(var(--light-bg-rgb), 0.8);
|
||||
padding: 8px 12px;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.block-language {
|
||||
color: rgba(128, 128, 128, 0.8);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.block-language::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 5px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23888'%3E%3Cpath d='M9.7,16.7L5.3,12.3C4.9,11.9 4.9,11.1 5.3,10.7C5.7,10.3 6.3,10.3 6.7,10.7L10.5,14.5L17.3,7.7C17.7,7.3 18.3,7.3 18.7,7.7C19.1,8.1 19.1,8.7 18.7,9.1L11.3,16.7C10.9,17.1 10.1,17.1 9.7,16.7Z'/%3E%3C/svg%3E");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
tab-size: 4;
|
||||
-moz-tab-size: 4;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.theme-dark .code-block-wrapper {
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.theme-dark .block-header {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.theme-dark .block-language {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.theme-dark .block-language::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23aaa'%3E%3Cpath d='M9.7,16.7L5.3,12.3C4.9,11.9 4.9,11.1 5.3,10.7C5.7,10.3 6.3,10.3 6.7,10.7L10.5,14.5L17.3,7.7C17.7,7.3 18.3,7.3 18.7,7.7C19.1,8.1 19.1,8.7 18.7,9.1L11.3,16.7C10.9,17.1 10.1,17.1 9.7,16.7Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.theme-dark .code-block {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
/* 代码高亮 */
|
||||
.theme-dark .keyword { color: #c586c0; }
|
||||
.theme-dark .function { color: #dcdcaa; }
|
||||
.theme-dark .variable { color: #9cdcfe; }
|
||||
.theme-dark .string { color: #ce9178; }
|
||||
.theme-dark .comment { color: #6a9955; }
|
||||
.theme-dark .class { color: #4ec9b0; }
|
||||
.theme-dark .parameter { color: #9cdcfe; }
|
||||
.theme-dark .built-in { color: #4ec9b0; }
|
||||
|
||||
/* 浅色主题代码高亮 */
|
||||
.keyword { color: #af00db; }
|
||||
.function { color: #795e26; }
|
||||
.variable { color: #001080; }
|
||||
.string { color: #a31515; }
|
||||
.comment { color: #008000; }
|
||||
.class { color: #267f99; }
|
||||
.parameter { color: #001080; }
|
||||
.built-in { color: #267f99; }
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
border: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.theme-dark .light-theme-img {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.theme-dark .dark-theme-img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body:not(.theme-dark) .dark-theme-img {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body:not(.theme-dark) .light-theme-img {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* 技术栈列表 */
|
||||
.tech-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 技术栈列表 */
|
||||
.tech-item {
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
border: 2px solid var(--border-color);
|
||||
background-color: rgba(var(--light-bg-rgb), 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tech-icon {
|
||||
margin-right: 15px;
|
||||
color: var(--secondary-color);
|
||||
font-size: 20px;
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tech-name {
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.tech-desc {
|
||||
font-size: 14px;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* 页脚 */
|
||||
.footer {
|
||||
border-top: 2px solid var(--border-color);
|
||||
padding: 20px 0;
|
||||
margin-top: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: var(--secondary-color);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.card-controls {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo-frame {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 针对移动设备的响应式优化 */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-controls {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* 预览区域优化 */
|
||||
.preview-content {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.block-header {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
/* 日志界面导航链接优化 */
|
||||
.nav-links {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.nav-links .btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
/* 特性卡片优化 */
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 预览窗口优化 */
|
||||
.preview-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.preview-window {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 技术栈列表小屏幕优化 */
|
||||
.tech-item {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tech-desc {
|
||||
width: 100%;
|
||||
padding-left: 40px; /* 图标宽度+右边距 */
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* 日志界面资源列表项优化 */
|
||||
.asset-item {
|
||||
flex-wrap: wrap;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.asset-name {
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.asset-size {
|
||||
order: 2;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
order: 3;
|
||||
margin-left: 0;
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* 页脚链接优化 */
|
||||
.footer {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
margin-top: 15px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
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.
|
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
+256
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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,9 +0,0 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
import * as Service from "./service.js";
|
||||
export {
|
||||
Service
|
||||
};
|
||||
|
||||
export * from "./models.js";
|
||||
@@ -1,51 +0,0 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
export class Config {
|
||||
/**
|
||||
* DBSource is the database URI to use.
|
||||
* The string ":memory:" can be used to create an in-memory database.
|
||||
* The sqlite driver can be configured through query parameters.
|
||||
* For more details see https://pkg.go.dev/modernc.org/sqlite#Driver.Open
|
||||
*/
|
||||
"DBSource": string;
|
||||
|
||||
/** Creates a new Config instance. */
|
||||
constructor($$source: Partial<Config> = {}) {
|
||||
if (!("DBSource" in $$source)) {
|
||||
this["DBSource"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Config instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Config {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new Config($$parsedSource as Partial<Config>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Row holds a single row in the result of a query.
|
||||
* It is a key-value map where keys are column names.
|
||||
*/
|
||||
export type Row = { [_: string]: any };
|
||||
|
||||
/**
|
||||
* Rows holds the result of a query
|
||||
* as an array of key-value maps where keys are column names.
|
||||
*/
|
||||
export type Rows = Row[];
|
||||
|
||||
/**
|
||||
* Stmt wraps a prepared sql statement pointer.
|
||||
* It provides the same methods as the [sql.Stmt] type.
|
||||
*/
|
||||
export type Stmt = string;
|
||||
@@ -1,223 +0,0 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as application$0 from "../../application/models.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export {
|
||||
ExecContext as Execute,
|
||||
QueryContext as Query
|
||||
};
|
||||
|
||||
import { Stmt } from "./stmt.js";
|
||||
|
||||
/**
|
||||
* Prepare creates a prepared statement for later queries or executions.
|
||||
* Multiple queries or executions may be run concurrently from the returned statement.
|
||||
*
|
||||
* The caller must call the statement's Close method when it is no longer needed.
|
||||
* Statements are closed automatically
|
||||
* when the connection they are associated with is closed.
|
||||
*
|
||||
* Prepare supports early cancellation.
|
||||
*/
|
||||
export function Prepare(query: string): Promise<Stmt | null> & { cancel(): void } {
|
||||
const promise = PrepareContext(query);
|
||||
const wrapper: any = (promise.then(function (id) {
|
||||
return id == null ? null : new Stmt(
|
||||
ClosePrepared.bind(null, id),
|
||||
ExecPrepared.bind(null, id),
|
||||
QueryPrepared.bind(null, id));
|
||||
}));
|
||||
wrapper.cancel = promise.cancel;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close closes the current database connection if one is open, otherwise has no effect.
|
||||
* Additionally, Close closes all open prepared statements associated to the connection.
|
||||
*
|
||||
* Even when a non-nil error is returned,
|
||||
* the database service is left in a consistent state,
|
||||
* ready for a call to [Service.Open].
|
||||
*/
|
||||
export function Close(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1888105376) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ClosePrepared closes a prepared statement
|
||||
* obtained with [Service.Prepare] or [Service.PrepareContext].
|
||||
* ClosePrepared is idempotent:
|
||||
* it has no effect on prepared statements that are already closed.
|
||||
*/
|
||||
function ClosePrepared(stmt: $models.Stmt | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2526200629, stmt) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure changes the database service configuration.
|
||||
* The connection state at call time is preserved.
|
||||
* Consumers will need to call [Service.Open] manually after Configure
|
||||
* in order to reconnect with the new configuration.
|
||||
*
|
||||
* See [NewWithConfig] for details on configuration.
|
||||
*/
|
||||
export function Configure(config: $models.Config | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1939578712, config) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ExecContext executes a query without returning any rows.
|
||||
* It supports early cancellation.
|
||||
*/
|
||||
function ExecContext(query: string, ...args: any[]): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(674944556, query, args) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ExecPrepared executes a prepared statement
|
||||
* obtained with [Service.Prepare] or [Service.PrepareContext]
|
||||
* without returning any rows.
|
||||
* It supports early cancellation.
|
||||
*/
|
||||
function ExecPrepared(stmt: $models.Stmt | null, ...args: any[]): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2086877656, stmt, args) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute executes a query without returning any rows.
|
||||
*/
|
||||
export function Execute(query: string, ...args: any[]): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3811930203, query, args) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open validates the current configuration,
|
||||
* closes the current connection if one is present,
|
||||
* then opens and validates a new connection.
|
||||
*
|
||||
* Even when a non-nil error is returned,
|
||||
* the database service is left in a consistent state,
|
||||
* ready for a new call to Open.
|
||||
*/
|
||||
export function Open(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2012175612) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare creates a prepared statement for later queries or executions.
|
||||
* Multiple queries or executions may be run concurrently from the returned statement.
|
||||
*
|
||||
* The caller should call the statement's Close method when it is no longer needed.
|
||||
* Statements are closed automatically
|
||||
* when the connection they are associated with is closed.
|
||||
*/
|
||||
export function Prepare(query: string): Promise<$models.Stmt | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1801965143, query) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* PrepareContext creates a prepared statement for later queries or executions.
|
||||
* Multiple queries or executions may be run concurrently from the returned statement.
|
||||
*
|
||||
* The caller must call the statement's Close method when it is no longer needed.
|
||||
* Statements are closed automatically
|
||||
* when the connection they are associated with is closed.
|
||||
*
|
||||
* PrepareContext supports early cancellation.
|
||||
*/
|
||||
function PrepareContext(query: string): Promise<$models.Stmt | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(570941694, query) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query executes a query and returns a slice of key-value records,
|
||||
* one per row, with column names as keys.
|
||||
*/
|
||||
export function Query(query: string, ...args: any[]): Promise<$models.Rows> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(860757720, query, args) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* QueryContext executes a query and returns a slice of key-value records,
|
||||
* one per row, with column names as keys.
|
||||
* It supports early cancellation, returning the slice of results fetched so far.
|
||||
*/
|
||||
function QueryContext(query: string, ...args: any[]): Promise<$models.Rows> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(4115542347, query, args) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* QueryPrepared executes a prepared statement
|
||||
* obtained with [Service.Prepare] or [Service.PrepareContext]
|
||||
* and returns a slice of key-value records, one per row, with column names as keys.
|
||||
* It supports early cancellation, returning the slice of results fetched so far.
|
||||
*/
|
||||
function QueryPrepared(stmt: $models.Stmt | null, ...args: any[]): Promise<$models.Rows> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3885083725, stmt, args) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceName returns the name of the plugin.
|
||||
* You should use the go module format e.g. github.com/myuser/myplugin
|
||||
*/
|
||||
export function ServiceName(): Promise<string> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1637123084) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown closes the database connection.
|
||||
* It returns a non-nil error in case of failures.
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3650435925) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup opens the database connection.
|
||||
* It returns a non-nil error in case of failures.
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1113159936, options) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $Create.Map($Create.Any, $Create.Any);
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
@@ -1,79 +0,0 @@
|
||||
//@ts-check
|
||||
|
||||
//@ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
const execSymbol = Symbol("exec"),
|
||||
querySymbol = Symbol("query"),
|
||||
closeSymbol = Symbol("close");
|
||||
|
||||
/**
|
||||
* Stmt represents a prepared statement for later queries or executions.
|
||||
* Multiple queries or executions may be run concurrently on the same statement.
|
||||
*
|
||||
* The caller must call the statement's Close method when it is no longer needed.
|
||||
* Statements are closed automatically
|
||||
* when the connection they are associated with is closed.
|
||||
*/
|
||||
export class Stmt {
|
||||
/**
|
||||
* Constructs a new prepared statement instance.
|
||||
* @param {(...args: any[]) => Promise<void>} close
|
||||
* @param {(...args: any[]) => Promise<void> & { cancel(): void }} exec
|
||||
* @param {(...args: any[]) => Promise<$models.Rows> & { cancel(): void }} query
|
||||
*/
|
||||
constructor(close, exec, query) {
|
||||
/**
|
||||
* @member
|
||||
* @private
|
||||
* @type {typeof close}
|
||||
*/
|
||||
this[closeSymbol] = close;
|
||||
|
||||
/**
|
||||
* @member
|
||||
* @private
|
||||
* @type {typeof exec}
|
||||
*/
|
||||
this[execSymbol] = exec;
|
||||
|
||||
/**
|
||||
* @member
|
||||
* @private
|
||||
* @type {typeof query}
|
||||
*/
|
||||
this[querySymbol] = query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the prepared statement.
|
||||
* It has no effect when the statement is already closed.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
Close() {
|
||||
return this[closeSymbol]();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the prepared statement without returning any rows.
|
||||
* It supports early cancellation.
|
||||
*
|
||||
* @param {any[]} args
|
||||
* @returns {Promise<void> & { cancel(): void }}
|
||||
*/
|
||||
Exec(...args) {
|
||||
return this[execSymbol](...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the prepared statement
|
||||
* and returns a slice of key-value records, one per row, with column names as keys.
|
||||
* It supports early cancellation, returning the array of results fetched so far.
|
||||
*
|
||||
* @param {any[]} args
|
||||
* @returns {Promise<$models.Rows> & { cancel(): void }}
|
||||
*/
|
||||
Query(...args) {
|
||||
return this[querySymbol](...args);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,11 @@ export class AppConfig {
|
||||
*/
|
||||
"updates": UpdatesConfig;
|
||||
|
||||
/**
|
||||
* Git备份设置
|
||||
*/
|
||||
"backup": GitBackupConfig;
|
||||
|
||||
/**
|
||||
* 配置元数据
|
||||
*/
|
||||
@@ -52,6 +57,9 @@ export class AppConfig {
|
||||
if (!("updates" in $$source)) {
|
||||
this["updates"] = (new UpdatesConfig());
|
||||
}
|
||||
if (!("backup" in $$source)) {
|
||||
this["backup"] = (new GitBackupConfig());
|
||||
}
|
||||
if (!("metadata" in $$source)) {
|
||||
this["metadata"] = (new ConfigMetadata());
|
||||
}
|
||||
@@ -68,6 +76,7 @@ export class AppConfig {
|
||||
const $$createField2_0 = $$createType2;
|
||||
const $$createField3_0 = $$createType3;
|
||||
const $$createField4_0 = $$createType4;
|
||||
const $$createField5_0 = $$createType5;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("general" in $$parsedSource) {
|
||||
$$parsedSource["general"] = $$createField0_0($$parsedSource["general"]);
|
||||
@@ -81,8 +90,11 @@ export class AppConfig {
|
||||
if ("updates" in $$parsedSource) {
|
||||
$$parsedSource["updates"] = $$createField3_0($$parsedSource["updates"]);
|
||||
}
|
||||
if ("backup" in $$parsedSource) {
|
||||
$$parsedSource["backup"] = $$createField4_0($$parsedSource["backup"]);
|
||||
}
|
||||
if ("metadata" in $$parsedSource) {
|
||||
$$parsedSource["metadata"] = $$createField4_0($$parsedSource["metadata"]);
|
||||
$$parsedSource["metadata"] = $$createField5_0($$parsedSource["metadata"]);
|
||||
}
|
||||
return new AppConfig($$parsedSource as Partial<AppConfig>);
|
||||
}
|
||||
@@ -102,11 +114,6 @@ export class AppearanceConfig {
|
||||
*/
|
||||
"systemTheme": SystemThemeType;
|
||||
|
||||
/**
|
||||
* 自定义主题配置
|
||||
*/
|
||||
"customTheme": CustomThemeConfig;
|
||||
|
||||
/** Creates a new AppearanceConfig instance. */
|
||||
constructor($$source: Partial<AppearanceConfig> = {}) {
|
||||
if (!("language" in $$source)) {
|
||||
@@ -115,9 +122,6 @@ export class AppearanceConfig {
|
||||
if (!("systemTheme" in $$source)) {
|
||||
this["systemTheme"] = ("" as SystemThemeType);
|
||||
}
|
||||
if (!("customTheme" in $$source)) {
|
||||
this["customTheme"] = (new CustomThemeConfig());
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
@@ -126,15 +130,30 @@ export class AppearanceConfig {
|
||||
* Creates a new AppearanceConfig instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): AppearanceConfig {
|
||||
const $$createField2_0 = $$createType5;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("customTheme" in $$parsedSource) {
|
||||
$$parsedSource["customTheme"] = $$createField2_0($$parsedSource["customTheme"]);
|
||||
}
|
||||
return new AppearanceConfig($$parsedSource as Partial<AppearanceConfig>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Git备份相关类型定义
|
||||
*
|
||||
* AuthMethod 定义Git认证方式
|
||||
*/
|
||||
export enum AuthMethod {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* 认证方式
|
||||
*/
|
||||
Token = "token",
|
||||
SSHKey = "ssh_key",
|
||||
UserPass = "user_pass",
|
||||
};
|
||||
|
||||
/**
|
||||
* ConfigMetadata 配置元数据
|
||||
*/
|
||||
@@ -170,49 +189,6 @@ export class ConfigMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CustomThemeConfig 自定义主题配置
|
||||
*/
|
||||
export class CustomThemeConfig {
|
||||
/**
|
||||
* 深色主题配置
|
||||
*/
|
||||
"darkTheme": ThemeColorConfig;
|
||||
|
||||
/**
|
||||
* 浅色主题配置
|
||||
*/
|
||||
"lightTheme": ThemeColorConfig;
|
||||
|
||||
/** Creates a new CustomThemeConfig instance. */
|
||||
constructor($$source: Partial<CustomThemeConfig> = {}) {
|
||||
if (!("darkTheme" in $$source)) {
|
||||
this["darkTheme"] = (new ThemeColorConfig());
|
||||
}
|
||||
if (!("lightTheme" in $$source)) {
|
||||
this["lightTheme"] = (new ThemeColorConfig());
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new CustomThemeConfig instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): CustomThemeConfig {
|
||||
const $$createField0_0 = $$createType6;
|
||||
const $$createField1_0 = $$createType6;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("darkTheme" in $$parsedSource) {
|
||||
$$parsedSource["darkTheme"] = $$createField0_0($$parsedSource["darkTheme"]);
|
||||
}
|
||||
if ("lightTheme" in $$parsedSource) {
|
||||
$$parsedSource["lightTheme"] = $$createField1_0($$parsedSource["lightTheme"]);
|
||||
}
|
||||
return new CustomThemeConfig($$parsedSource as Partial<CustomThemeConfig>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Document represents a document in the system
|
||||
*/
|
||||
@@ -224,6 +200,11 @@ export class Document {
|
||||
"updatedAt": time$0.Time;
|
||||
"is_deleted": boolean;
|
||||
|
||||
/**
|
||||
* 锁定标志,锁定的文档无法被删除
|
||||
*/
|
||||
"is_locked": boolean;
|
||||
|
||||
/** Creates a new Document instance. */
|
||||
constructor($$source: Partial<Document> = {}) {
|
||||
if (!("id" in $$source)) {
|
||||
@@ -244,6 +225,9 @@ export class Document {
|
||||
if (!("is_deleted" in $$source)) {
|
||||
this["is_deleted"] = false;
|
||||
}
|
||||
if (!("is_locked" in $$source)) {
|
||||
this["is_locked"] = false;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
@@ -389,7 +373,7 @@ export class Extension {
|
||||
* Creates a new Extension instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Extension {
|
||||
const $$createField3_0 = $$createType7;
|
||||
const $$createField3_0 = $$createType6;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("config" in $$parsedSource) {
|
||||
$$parsedSource["config"] = $$createField3_0($$parsedSource["config"]);
|
||||
@@ -483,6 +467,17 @@ export class GeneralConfig {
|
||||
*/
|
||||
"startAtLogin": boolean;
|
||||
|
||||
/**
|
||||
* 窗口吸附设置
|
||||
* 是否启用窗口吸附功能
|
||||
*/
|
||||
"enableWindowSnap": boolean;
|
||||
|
||||
/**
|
||||
* 吸附距离阈值(像素)
|
||||
*/
|
||||
"snapThreshold": number;
|
||||
|
||||
/**
|
||||
* 全局热键设置
|
||||
* 是否启用全局热键
|
||||
@@ -508,6 +503,12 @@ export class GeneralConfig {
|
||||
if (!("startAtLogin" in $$source)) {
|
||||
this["startAtLogin"] = false;
|
||||
}
|
||||
if (!("enableWindowSnap" in $$source)) {
|
||||
this["enableWindowSnap"] = false;
|
||||
}
|
||||
if (!("snapThreshold" in $$source)) {
|
||||
this["snapThreshold"] = 0;
|
||||
}
|
||||
if (!("enableGlobalHotkey" in $$source)) {
|
||||
this["enableGlobalHotkey"] = false;
|
||||
}
|
||||
@@ -522,15 +523,64 @@ export class GeneralConfig {
|
||||
* Creates a new GeneralConfig instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): GeneralConfig {
|
||||
const $$createField5_0 = $$createType9;
|
||||
const $$createField7_0 = $$createType8;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("globalHotkey" in $$parsedSource) {
|
||||
$$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]);
|
||||
$$parsedSource["globalHotkey"] = $$createField7_0($$parsedSource["globalHotkey"]);
|
||||
}
|
||||
return new GeneralConfig($$parsedSource as Partial<GeneralConfig>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GitBackupConfig Git备份配置
|
||||
*/
|
||||
export class GitBackupConfig {
|
||||
"enabled": boolean;
|
||||
"repo_url": string;
|
||||
"auth_method": AuthMethod;
|
||||
"username"?: string;
|
||||
"password"?: string;
|
||||
"token"?: string;
|
||||
"ssh_key_path"?: string;
|
||||
"ssh_key_passphrase"?: string;
|
||||
|
||||
/**
|
||||
* 分钟
|
||||
*/
|
||||
"backup_interval": number;
|
||||
"auto_backup": boolean;
|
||||
|
||||
/** Creates a new GitBackupConfig instance. */
|
||||
constructor($$source: Partial<GitBackupConfig> = {}) {
|
||||
if (!("enabled" in $$source)) {
|
||||
this["enabled"] = false;
|
||||
}
|
||||
if (!("repo_url" in $$source)) {
|
||||
this["repo_url"] = "";
|
||||
}
|
||||
if (!("auth_method" in $$source)) {
|
||||
this["auth_method"] = ("" as AuthMethod);
|
||||
}
|
||||
if (!("backup_interval" in $$source)) {
|
||||
this["backup_interval"] = 0;
|
||||
}
|
||||
if (!("auto_backup" in $$source)) {
|
||||
this["auto_backup"] = false;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new GitBackupConfig instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): GitBackupConfig {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new GitBackupConfig($$parsedSource as Partial<GitBackupConfig>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GiteaConfig Gitea配置
|
||||
*/
|
||||
@@ -1083,6 +1133,58 @@ export enum TabType {
|
||||
TabTypeTab = "tab",
|
||||
};
|
||||
|
||||
/**
|
||||
* Theme 主题数据库模型
|
||||
*/
|
||||
export class Theme {
|
||||
"id": number;
|
||||
"name": string;
|
||||
"type": ThemeType;
|
||||
"colors": ThemeColorConfig;
|
||||
"isDefault": boolean;
|
||||
"createdAt": time$0.Time;
|
||||
"updatedAt": time$0.Time;
|
||||
|
||||
/** Creates a new Theme instance. */
|
||||
constructor($$source: Partial<Theme> = {}) {
|
||||
if (!("id" in $$source)) {
|
||||
this["id"] = 0;
|
||||
}
|
||||
if (!("name" in $$source)) {
|
||||
this["name"] = "";
|
||||
}
|
||||
if (!("type" in $$source)) {
|
||||
this["type"] = ("" as ThemeType);
|
||||
}
|
||||
if (!("colors" in $$source)) {
|
||||
this["colors"] = (new ThemeColorConfig());
|
||||
}
|
||||
if (!("isDefault" in $$source)) {
|
||||
this["isDefault"] = false;
|
||||
}
|
||||
if (!("createdAt" in $$source)) {
|
||||
this["createdAt"] = null;
|
||||
}
|
||||
if (!("updatedAt" in $$source)) {
|
||||
this["updatedAt"] = null;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Theme instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): Theme {
|
||||
const $$createField3_0 = $$createType9;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("colors" in $$parsedSource) {
|
||||
$$parsedSource["colors"] = $$createField3_0($$parsedSource["colors"]);
|
||||
}
|
||||
return new Theme($$parsedSource as Partial<Theme>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ThemeColorConfig 主题颜色配置
|
||||
*/
|
||||
@@ -1291,6 +1393,19 @@ export class ThemeColorConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ThemeType 主题类型枚举
|
||||
*/
|
||||
export enum ThemeType {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
ThemeTypeDark = "dark",
|
||||
ThemeTypeLight = "light",
|
||||
};
|
||||
|
||||
/**
|
||||
* UpdateSourceType 更新源类型
|
||||
*/
|
||||
@@ -1407,16 +1522,16 @@ const $$createType0 = GeneralConfig.createFrom;
|
||||
const $$createType1 = EditingConfig.createFrom;
|
||||
const $$createType2 = AppearanceConfig.createFrom;
|
||||
const $$createType3 = UpdatesConfig.createFrom;
|
||||
const $$createType4 = ConfigMetadata.createFrom;
|
||||
const $$createType5 = CustomThemeConfig.createFrom;
|
||||
const $$createType6 = ThemeColorConfig.createFrom;
|
||||
var $$createType7 = (function $$initCreateType7(...args): any {
|
||||
if ($$createType7 === $$initCreateType7) {
|
||||
$$createType7 = $$createType8;
|
||||
const $$createType4 = GitBackupConfig.createFrom;
|
||||
const $$createType5 = ConfigMetadata.createFrom;
|
||||
var $$createType6 = (function $$initCreateType6(...args): any {
|
||||
if ($$createType6 === $$initCreateType6) {
|
||||
$$createType6 = $$createType7;
|
||||
}
|
||||
return $$createType7(...args);
|
||||
return $$createType6(...args);
|
||||
});
|
||||
const $$createType8 = $Create.Map($Create.Any, $Create.Any);
|
||||
const $$createType9 = HotkeyCombo.createFrom;
|
||||
const $$createType7 = $Create.Map($Create.Any, $Create.Any);
|
||||
const $$createType8 = HotkeyCombo.createFrom;
|
||||
const $$createType9 = ThemeColorConfig.createFrom;
|
||||
const $$createType10 = GithubConfig.createFrom;
|
||||
const $$createType11 = GiteaConfig.createFrom;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -58,6 +58,14 @@ export function Set(key: string, value: any): Promise<void> & { cancel(): void }
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetBackupConfigChangeCallback 设置备份配置变更回调
|
||||
*/
|
||||
export function SetBackupConfigChangeCallback(callback: any): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3264871659, callback) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetDataPathChangeCallback 设置数据路径配置变更回调
|
||||
*/
|
||||
@@ -74,6 +82,14 @@ export function SetHotkeyChangeCallback(callback: any): Promise<void> & { cancel
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetWindowSnapConfigChangeCallback 设置窗口吸附配置变更回调
|
||||
*/
|
||||
export function SetWindowSnapConfigChangeCallback(callback: any): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2324961653, callback) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.AppConfig.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
@@ -22,6 +22,14 @@ export function OnDataPathChanged(): Promise<void> & { cancel(): void } {
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -22,6 +22,14 @@ export function SelectDirectory(): Promise<string> & { cancel(): void } {
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SelectFile 打开文件选择对话框
|
||||
*/
|
||||
export function SelectFile(): Promise<string> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(37302920) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetWindow 设置绑定的窗口
|
||||
*/
|
||||
|
||||
@@ -81,6 +81,14 @@ export function ListDeletedDocumentsMeta(): Promise<(models$0.Document | null)[]
|
||||
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
|
||||
*/
|
||||
@@ -97,6 +105,14 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -32,8 +32,8 @@ export function GetCurrentHotkey(): Promise<models$0.HotkeyCombo | null> & { can
|
||||
/**
|
||||
* Initialize 初始化热键服务
|
||||
*/
|
||||
export function Initialize(app: application$0.App | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3671360458, app) as any;
|
||||
export function Initialize(app: application$0.App | null, mainWindow: application$0.WebviewWindow | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3671360458, app, mainWindow) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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";
|
||||
@@ -12,10 +13,13 @@ 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 ThemeService from "./themeservice.js";
|
||||
import * as TranslationService from "./translationservice.js";
|
||||
import * as TrayService from "./trayservice.js";
|
||||
import * as WindowService from "./windowservice.js";
|
||||
import * as WindowSnapService from "./windowsnapservice.js";
|
||||
export {
|
||||
BackupService,
|
||||
ConfigService,
|
||||
DatabaseService,
|
||||
DialogService,
|
||||
@@ -27,9 +31,11 @@ export {
|
||||
SelfUpdateService,
|
||||
StartupService,
|
||||
SystemService,
|
||||
ThemeService,
|
||||
TranslationService,
|
||||
TrayService,
|
||||
WindowService
|
||||
WindowService,
|
||||
WindowSnapService
|
||||
};
|
||||
|
||||
export * from "./models.js";
|
||||
@@ -203,7 +203,7 @@ export class SelfUpdateResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* WindowInfo 窗口信息
|
||||
* WindowInfo 窗口信息(简化版)
|
||||
*/
|
||||
export class WindowInfo {
|
||||
"Window": application$0.WebviewWindow | null;
|
||||
@@ -238,6 +238,26 @@ export class WindowInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WindowSnapService 窗口吸附服务
|
||||
*/
|
||||
export class WindowSnapService {
|
||||
|
||||
/** Creates a new WindowSnapService instance. */
|
||||
constructor($$source: Partial<WindowSnapService> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new WindowSnapService instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): WindowSnapService {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new WindowSnapService($$parsedSource as Partial<WindowSnapService>);
|
||||
}
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = application$0.WebviewWindow.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
@@ -0,0 +1,104 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* ThemeService 主题服务
|
||||
* @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";
|
||||
|
||||
/**
|
||||
* CreateTheme 创建新主题
|
||||
*/
|
||||
export function CreateTheme(theme: models$0.Theme | null): Promise<models$0.Theme | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3274757686, theme) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetAllThemes 获取所有主题
|
||||
*/
|
||||
export function GetAllThemes(): Promise<(models$0.Theme | null)[]> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2425053076) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType2($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetDefaultThemes 获取默认主题
|
||||
*/
|
||||
export function GetDefaultThemes(): Promise<{ [_: string]: models$0.Theme | null }> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3801788118) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType3($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetThemeByType 根据类型获取默认主题
|
||||
*/
|
||||
export function GetThemeByType(themeType: models$0.ThemeType): Promise<models$0.Theme | null> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1680465265, themeType) as any;
|
||||
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||
return $$createType1($result);
|
||||
}) as any;
|
||||
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||
return $typingPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ResetThemeColors 重置主题颜色为默认值
|
||||
*/
|
||||
export function ResetThemeColors(themeType: models$0.ThemeType): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(342461245, themeType) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown 服务关闭
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1676749034) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceStartup 服务启动时初始化
|
||||
*/
|
||||
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2915959937, options) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateThemeColors 更新主题颜色
|
||||
*/
|
||||
export function UpdateThemeColors(themeType: models$0.ThemeType, colors: models$0.ThemeColorConfig): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2750902529, themeType, colors) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = models$0.Theme.createFrom;
|
||||
const $$createType1 = $Create.Nullable($$createType0);
|
||||
const $$createType2 = $Create.Array($$createType1);
|
||||
const $$createType3 = $Create.Map($Create.Any, $$createType1);
|
||||
@@ -2,7 +2,7 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* WindowService 窗口管理服务
|
||||
* WindowService 窗口管理服务(专注于窗口生命周期管理)
|
||||
* @module
|
||||
*/
|
||||
|
||||
@@ -46,6 +46,14 @@ export function OpenDocumentWindow(documentID: number): Promise<void> & { cancel
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown 实现服务关闭接口
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(202192783) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetAppReferences 设置应用和主窗口引用
|
||||
*/
|
||||
@@ -54,6 +62,14 @@ export function SetAppReferences(app: application$0.App | null, mainWindow: appl
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetWindowSnapService 设置窗口吸附服务引用
|
||||
*/
|
||||
export function SetWindowSnapService(snapService: $models.WindowSnapService | null): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1105193745, snapService) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = $models.WindowInfo.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
@@ -0,0 +1,87 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* WindowSnapService 窗口吸附服务
|
||||
* @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";
|
||||
|
||||
/**
|
||||
* Cleanup 清理资源
|
||||
*/
|
||||
export function Cleanup(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2155505498) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* OnWindowSnapConfigChanged 处理窗口吸附配置变更
|
||||
*/
|
||||
export function OnWindowSnapConfigChanged(enabled: boolean, threshold: number): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(3794787039, enabled, threshold) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* RegisterWindow 注册需要吸附管理的窗口
|
||||
*/
|
||||
export function RegisterWindow(documentID: number, window: application$0.WebviewWindow | null, title: string): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1000222723, documentID, window, title) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceShutdown 实现服务关闭接口
|
||||
*/
|
||||
export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(1172710495) 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(1782093351, app, mainWindow) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetSnapEnabled 设置是否启用窗口吸附
|
||||
*/
|
||||
export function SetSnapEnabled(enabled: boolean): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2280126835, enabled) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetSnapThreshold 设置窗口吸附阈值
|
||||
*/
|
||||
export function SetSnapThreshold(threshold: number): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(586790049, threshold) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* StartWindowSnapMonitor 启动窗口吸附监听器
|
||||
*/
|
||||
export function StartWindowSnapMonitor(): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(84533508) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* UnregisterWindow 取消注册窗口
|
||||
*/
|
||||
export function UnregisterWindow(documentID: number): Promise<void> & { cancel(): void } {
|
||||
let $resultPromise = $Call.ByID(2844230768, documentID) as any;
|
||||
return $resultPromise;
|
||||
}
|
||||
Generated
+366
-395
File diff suppressed because it is too large.
Load diff
+19
-20
@@ -24,24 +24,24 @@
|
||||
"@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-liquid": "^6.3.0",
|
||||
"@codemirror/lang-markdown": "^6.3.4",
|
||||
"@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-sql": "^6.9.1",
|
||||
"@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": "^6.11.3",
|
||||
"@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.0",
|
||||
"@codemirror/view": "^6.38.1",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"codemirror": "^6.0.2",
|
||||
@@ -52,31 +52,30 @@
|
||||
"hsl-matcher": "^1.2.4",
|
||||
"lezer": "^0.13.5",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.4.1",
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
"prettier": "^3.6.2",
|
||||
"remarkable": "^2.0.1",
|
||||
"sass": "^1.89.2",
|
||||
"vue": "^3.5.17",
|
||||
"vue-i18n": "^11.1.9",
|
||||
"sass": "^1.90.0",
|
||||
"vue": "^3.5.19",
|
||||
"vue-i18n": "^11.1.11",
|
||||
"vue-pick-colors": "^1.8.0",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.30.1",
|
||||
"@eslint/js": "^9.34.0",
|
||||
"@lezer/generator": "^1.8.0",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/node": "^24.0.12",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/remarkable": "^2.0.8",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@wailsio/runtime": "latest",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint-plugin-vue": "^10.3.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"globals": "^16.3.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.36.0",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^7.0.3",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.40.0",
|
||||
"unplugin-vue-components": "^29.0.0",
|
||||
"vite": "^7.1.3",
|
||||
"vue-eslint-parser": "^10.2.0",
|
||||
"vue-tsc": "^3.0.1"
|
||||
"vue-tsc": "^3.0.6"
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ 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();
|
||||
@@ -12,6 +13,7 @@ const systemStore = useSystemStore();
|
||||
const keybindingStore = useKeybindingStore();
|
||||
const themeStore = useThemeStore();
|
||||
const updateStore = useUpdateStore();
|
||||
const backupStore = useBackupStore();
|
||||
|
||||
// 应用启动时加载配置和初始化系统信息
|
||||
onMounted(async () => {
|
||||
@@ -26,6 +28,9 @@ onMounted(async () => {
|
||||
await configStore.initializeLanguage();
|
||||
themeStore.initializeTheme();
|
||||
|
||||
// 初始化备份服务
|
||||
await backupStore.initialize();
|
||||
|
||||
// 启动时检查更新
|
||||
await updateStore.checkOnStartup();
|
||||
});
|
||||
|
||||
@@ -46,85 +46,56 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import {computed, onMounted, 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 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';
|
||||
});
|
||||
|
||||
const minimizeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Minimise();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMaximize = async () => {
|
||||
try {
|
||||
await runtime.Window.ToggleMaximise();
|
||||
await checkMaximizedState();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const closeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Close();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const checkMaximizedState = async () => {
|
||||
try {
|
||||
isMaximized.value = await runtime.Window.IsMaximised();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
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();
|
||||
@@ -65,26 +64,16 @@ const minimizeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Minimise();
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
console.error(error)
|
||||
}
|
||||
};
|
||||
|
||||
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 runtime.Window.ToggleMaximise();
|
||||
await checkMaximizedState();
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
isMaximized.value = !isMaximized.value;
|
||||
console.error(error)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -92,7 +81,7 @@ const closeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Close();
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
console.error(error)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -100,7 +89,7 @@ const checkMaximizedState = async () => {
|
||||
try {
|
||||
isMaximized.value = await runtime.Window.IsMaximised();
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
console.error(error)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -112,24 +101,6 @@ const titleText = computed(() => {
|
||||
|
||||
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>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="windows-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
|
||||
<div class="windows-titlebar" style="--wails-draggable:drag">
|
||||
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
|
||||
<div class="titlebar-icon">
|
||||
<img src="/appicon.png" alt="voidraft"/>
|
||||
@@ -36,10 +36,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, onUnmounted, ref} from 'vue';
|
||||
import {computed, onMounted, 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();
|
||||
@@ -59,30 +58,16 @@ const minimizeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Minimise();
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMaximize = async () => {
|
||||
try {
|
||||
// 立即更新UI状态,提供即时反馈
|
||||
const newState = !isMaximized.value;
|
||||
isMaximized.value = newState;
|
||||
|
||||
// 然后执行实际操作
|
||||
if (newState) {
|
||||
await runtime.Window.Maximise();
|
||||
} else {
|
||||
await runtime.Window.UnMaximise();
|
||||
}
|
||||
|
||||
// 操作完成后再次确认状态(防止操作失败时状态不一致)
|
||||
setTimeout(async () => {
|
||||
await runtime.Window.ToggleMaximise();
|
||||
await checkMaximizedState();
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
// 如果操作失败,恢复原状态
|
||||
isMaximized.value = !isMaximized.value;
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -90,7 +75,7 @@ const closeWindow = async () => {
|
||||
try {
|
||||
await runtime.Window.Close();
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -98,31 +83,12 @@ const checkMaximizedState = async () => {
|
||||
try {
|
||||
isMaximized.value = await runtime.Window.IsMaximised();
|
||||
} catch (error) {
|
||||
// Error handling
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await checkMaximizedState();
|
||||
|
||||
runtime.Events.On('window:maximised', () => {
|
||||
isMaximized.value = true;
|
||||
});
|
||||
|
||||
runtime.Events.On('window:unmaximised', () => {
|
||||
isMaximized.value = false;
|
||||
});
|
||||
|
||||
runtime.Events.On('window:focus', async () => {
|
||||
await checkMaximizedState();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
runtime.Events.Off('window:maximised');
|
||||
runtime.Events.Off('window:unmaximised');
|
||||
runtime.Events.Off('window:focus');
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -107,6 +107,12 @@ const selectItem = async (item: any) => {
|
||||
// 选择文档
|
||||
const selectDoc = async (doc: Document) => {
|
||||
try {
|
||||
// 如果选择的就是当前文档,直接关闭菜单
|
||||
if (documentStore.currentDocument?.id === doc.id) {
|
||||
closeMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
||||
if (hasOpen) {
|
||||
// 设置错误状态并启动定时器
|
||||
@@ -208,16 +214,41 @@ const openInNewWindow = async (doc: Document, event: Event) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理删除 - 简化确认机制
|
||||
// 处理删除
|
||||
const handleDelete = async (doc: Document, event: Event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (deleteConfirmId.value === doc.id) {
|
||||
// 确认删除
|
||||
// 确认删除前检查文档是否在其他窗口打开
|
||||
try {
|
||||
await documentStore.deleteDocument(doc.id);
|
||||
await documentStore.updateDocuments();
|
||||
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;
|
||||
}
|
||||
|
||||
const deleteSuccess = await documentStore.deleteDocument(doc.id);
|
||||
|
||||
if (!deleteSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
await documentStore.updateDocuments();
|
||||
// 如果删除的是当前文档,切换到第一个文档
|
||||
if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) {
|
||||
const firstDoc = documentStore.documentList[0];
|
||||
|
||||
@@ -119,6 +119,7 @@ export default {
|
||||
general: 'General',
|
||||
editing: 'Editor',
|
||||
appearance: 'Appearance',
|
||||
backupPage: 'Backup',
|
||||
keyBindings: 'Key Bindings',
|
||||
updates: 'Updates',
|
||||
reset: 'Reset',
|
||||
@@ -133,6 +134,7 @@ export default {
|
||||
showInSystemTray: 'Show in System Tray',
|
||||
enableSystemTray: 'Enable System Tray',
|
||||
alwaysOnTop: 'Always on Top',
|
||||
enableWindowSnap: 'Enable Window Snapping',
|
||||
startup: 'Startup Settings',
|
||||
startAtLogin: 'Start at Login',
|
||||
dataStorage: 'Data Storage',
|
||||
@@ -242,6 +244,49 @@ export default {
|
||||
restartNow: 'Restart Now',
|
||||
hotkeyPreview: 'Preview:',
|
||||
none: 'None',
|
||||
backup: {
|
||||
basicSettings: 'Basic Settings',
|
||||
enableBackup: 'Enable Git Backup',
|
||||
autoBackup: 'Auto Backup',
|
||||
backupInterval: 'Backup Interval',
|
||||
intervals: {
|
||||
'5min': '5 minutes',
|
||||
'10min': '10 minutes',
|
||||
'15min': '15 minutes',
|
||||
'30min': '30 minutes',
|
||||
'1hour': '1 hour'
|
||||
},
|
||||
repositoryConfig: 'Repository Configuration',
|
||||
repoUrl: 'Repository URL',
|
||||
repoUrlPlaceholder: 'Enter Git repository URL',
|
||||
authConfig: 'Authentication Configuration',
|
||||
authMethod: 'Authentication Method',
|
||||
authMethods: {
|
||||
token: 'Access Token',
|
||||
sshKey: 'SSH Key',
|
||||
userPass: 'Username/Password'
|
||||
},
|
||||
username: 'Username',
|
||||
usernamePlaceholder: 'Enter username',
|
||||
password: 'Password',
|
||||
passwordPlaceholder: 'Enter password',
|
||||
token: 'Access Token',
|
||||
tokenPlaceholder: 'Enter access token',
|
||||
sshKeyPath: 'SSH Key Path',
|
||||
sshKeyPathPlaceholder: 'Select SSH key file',
|
||||
sshKeyPassphrase: 'SSH Key Passphrase',
|
||||
sshKeyPassphrasePlaceholder: 'Enter SSH key passphrase',
|
||||
backupOperations: 'Backup Operations',
|
||||
pushToRemote: 'Push to Remote',
|
||||
pushing: 'Pushing...',
|
||||
actions: {
|
||||
push: 'Push',
|
||||
},
|
||||
status: {
|
||||
success: 'Success',
|
||||
failed: 'Failed'
|
||||
}
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
rainbowBrackets: {
|
||||
|
||||
@@ -119,6 +119,7 @@ export default {
|
||||
general: '常规',
|
||||
editing: '编辑器',
|
||||
appearance: '外观',
|
||||
backupPage: '备份',
|
||||
extensions: '扩展',
|
||||
keyBindings: '快捷键',
|
||||
updates: '更新',
|
||||
@@ -134,6 +135,7 @@ export default {
|
||||
showInSystemTray: '在系统托盘中显示',
|
||||
enableSystemTray: '启用系统托盘',
|
||||
alwaysOnTop: '窗口始终置顶',
|
||||
enableWindowSnap: '启用窗口吸附',
|
||||
startup: '启动设置',
|
||||
startAtLogin: '开机自启动',
|
||||
dataStorage: '数据存储',
|
||||
@@ -243,6 +245,49 @@ export default {
|
||||
},
|
||||
hotkeyPreview: '预览:',
|
||||
none: '无',
|
||||
backup: {
|
||||
basicSettings: '基本设置',
|
||||
enableBackup: '启用备份',
|
||||
autoBackup: '自动备份',
|
||||
backupInterval: '备份间隔',
|
||||
intervals: {
|
||||
'5min': '5分钟',
|
||||
'10min': '10分钟',
|
||||
'15min': '15分钟',
|
||||
'30min': '30分钟',
|
||||
'1hour': '1小时'
|
||||
},
|
||||
repositoryConfig: '仓库配置',
|
||||
repoUrl: '仓库地址',
|
||||
repoUrlPlaceholder: '请输入Git仓库地址',
|
||||
authConfig: '认证配置',
|
||||
authMethod: '认证方式',
|
||||
authMethods: {
|
||||
token: '访问令牌',
|
||||
sshKey: 'SSH密钥',
|
||||
userPass: '用户名密码'
|
||||
},
|
||||
username: '用户名',
|
||||
usernamePlaceholder: '请输入用户名',
|
||||
password: '密码',
|
||||
passwordPlaceholder: '请输入密码',
|
||||
token: '访问令牌',
|
||||
tokenPlaceholder: '请输入访问令牌',
|
||||
sshKeyPath: 'SSH密钥路径',
|
||||
sshKeyPathPlaceholder: '请选择SSH密钥文件',
|
||||
sshKeyPassphrase: 'SSH密钥密码',
|
||||
sshKeyPassphrasePlaceholder: '请输入SSH密钥密码',
|
||||
backupOperations: '备份操作',
|
||||
pushToRemote: '推送到远程',
|
||||
pushing: '推送中...',
|
||||
actions: {
|
||||
push: '推送',
|
||||
},
|
||||
status: {
|
||||
success: '成功',
|
||||
failed: '失败'
|
||||
}
|
||||
},
|
||||
},
|
||||
extensions: {
|
||||
rainbowBrackets: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import AppearancePage from '@/views/settings/pages/AppearancePage.vue';
|
||||
import KeyBindingsPage from '@/views/settings/pages/KeyBindingsPage.vue';
|
||||
import UpdatesPage from '@/views/settings/pages/UpdatesPage.vue';
|
||||
import ExtensionsPage from '@/views/settings/pages/ExtensionsPage.vue';
|
||||
import BackupPage from '@/views/settings/pages/BackupPage.vue';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
@@ -49,6 +50,11 @@ const routes: RouteRecordRaw[] = [
|
||||
path: 'updates',
|
||||
name: 'SettingsUpdates',
|
||||
component: UpdatesPage
|
||||
},
|
||||
{
|
||||
path: 'backup',
|
||||
name: 'SettingsBackup',
|
||||
component: BackupPage
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {computed, readonly, ref} from 'vue'
|
||||
import type {GitBackupConfig} from '@/../bindings/voidraft/internal/models'
|
||||
import {BackupService} from '@/../bindings/voidraft/internal/services'
|
||||
import {useConfigStore} from '@/stores/configStore'
|
||||
|
||||
/**
|
||||
* Minimalist Backup Store
|
||||
*/
|
||||
export const useBackupStore = defineStore('backup', () => {
|
||||
// Core state
|
||||
const config = ref<GitBackupConfig | null>(null)
|
||||
const isPushing = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const isInitialized = ref(false)
|
||||
|
||||
// Backup result states
|
||||
const pushSuccess = ref(false)
|
||||
const pushError = ref(false)
|
||||
|
||||
// Timers for auto-hiding status icons and error messages
|
||||
let pushStatusTimer: number | null = null
|
||||
let errorTimer: number | null = null
|
||||
|
||||
// 获取configStore
|
||||
const configStore = useConfigStore()
|
||||
|
||||
// Computed properties
|
||||
const isEnabled = computed(() => configStore.config.backup.enabled)
|
||||
const isConfigured = computed(() => configStore.config.backup.repo_url)
|
||||
|
||||
// 清除状态显示
|
||||
const clearPushStatus = () => {
|
||||
if (pushStatusTimer !== null) {
|
||||
window.clearTimeout(pushStatusTimer)
|
||||
pushStatusTimer = null
|
||||
}
|
||||
pushSuccess.value = false
|
||||
pushError.value = false
|
||||
}
|
||||
|
||||
// 清除错误信息和错误图标
|
||||
const clearError = () => {
|
||||
if (errorTimer !== null) {
|
||||
window.clearTimeout(errorTimer)
|
||||
errorTimer = null
|
||||
}
|
||||
error.value = null
|
||||
pushError.value = false
|
||||
}
|
||||
|
||||
// 设置错误信息和错误图标并自动清除
|
||||
const setErrorWithAutoHide = (errorMessage: string, hideAfter: number = 3000) => {
|
||||
clearError()
|
||||
clearPushStatus()
|
||||
error.value = errorMessage
|
||||
pushError.value = true
|
||||
errorTimer = window.setTimeout(() => {
|
||||
error.value = null
|
||||
pushError.value = false
|
||||
errorTimer = null
|
||||
}, hideAfter)
|
||||
}
|
||||
|
||||
// Push to remote repository
|
||||
const pushToRemote = async () => {
|
||||
if (isPushing.value || !isConfigured.value) return
|
||||
|
||||
isPushing.value = true
|
||||
clearError() // 清除之前的错误信息
|
||||
clearPushStatus()
|
||||
|
||||
try {
|
||||
await BackupService.PushToRemote()
|
||||
// 显示成功状态,并设置3秒后自动消失
|
||||
pushSuccess.value = true
|
||||
pushStatusTimer = window.setTimeout(() => {
|
||||
pushSuccess.value = false
|
||||
pushStatusTimer = null
|
||||
}, 3000)
|
||||
} catch (err: any) {
|
||||
setErrorWithAutoHide(err?.message || 'Backup operation failed')
|
||||
} finally {
|
||||
isPushing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化备份服务
|
||||
const initialize = async () => {
|
||||
if (!isEnabled.value) return
|
||||
|
||||
// 避免重复初始化
|
||||
if (isInitialized.value) return
|
||||
|
||||
clearError() // 清除之前的错误信息
|
||||
try {
|
||||
await BackupService.Initialize()
|
||||
isInitialized.value = true
|
||||
} catch (err: any) {
|
||||
setErrorWithAutoHide(err?.message || 'Failed to initialize backup service')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
// State
|
||||
config: readonly(config),
|
||||
isPushing: readonly(isPushing),
|
||||
error: readonly(error),
|
||||
isInitialized: readonly(isInitialized),
|
||||
pushSuccess: readonly(pushSuccess),
|
||||
pushError: readonly(pushError),
|
||||
|
||||
// Computed
|
||||
isEnabled,
|
||||
isConfigured,
|
||||
|
||||
// Methods
|
||||
pushToRemote,
|
||||
initialize,
|
||||
clearError
|
||||
}
|
||||
})
|
||||
@@ -11,10 +11,11 @@ import {
|
||||
TabType,
|
||||
UpdatesConfig,
|
||||
UpdateSourceType,
|
||||
GitBackupConfig,
|
||||
AuthMethod
|
||||
} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {ConfigUtils} from '@/utils/configUtils';
|
||||
import {WindowController} from '@/utils/windowController';
|
||||
import * as runtime from '@wailsio/runtime';
|
||||
// 国际化相关导入
|
||||
export type SupportedLocaleType = 'zh-CN' | 'en-US';
|
||||
@@ -48,6 +49,10 @@ type UpdatesConfigKeyMap = {
|
||||
readonly [K in keyof UpdatesConfig]: string;
|
||||
};
|
||||
|
||||
type BackupConfigKeyMap = {
|
||||
readonly [K in keyof GitBackupConfig]: string;
|
||||
};
|
||||
|
||||
type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight';
|
||||
|
||||
// 配置键映射
|
||||
@@ -57,7 +62,9 @@ const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
|
||||
enableSystemTray: 'general.enableSystemTray',
|
||||
startAtLogin: 'general.startAtLogin',
|
||||
enableGlobalHotkey: 'general.enableGlobalHotkey',
|
||||
globalHotkey: 'general.globalHotkey'
|
||||
globalHotkey: 'general.globalHotkey',
|
||||
enableWindowSnap: 'general.enableWindowSnap',
|
||||
snapThreshold: 'general.snapThreshold'
|
||||
} as const;
|
||||
|
||||
const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
|
||||
@@ -73,8 +80,7 @@ const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = {
|
||||
|
||||
const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = {
|
||||
language: 'appearance.language',
|
||||
systemTheme: 'appearance.systemTheme',
|
||||
customTheme: 'appearance.customTheme'
|
||||
systemTheme: 'appearance.systemTheme'
|
||||
} as const;
|
||||
|
||||
const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
|
||||
@@ -88,6 +94,20 @@ const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
|
||||
gitea: 'updates.gitea'
|
||||
} as const;
|
||||
|
||||
const BACKUP_CONFIG_KEY_MAP: BackupConfigKeyMap = {
|
||||
enabled: 'backup.enabled',
|
||||
repo_url: 'backup.repo_url',
|
||||
auth_method: 'backup.auth_method',
|
||||
username: 'backup.username',
|
||||
password: 'backup.password',
|
||||
token: 'backup.token',
|
||||
ssh_key_path: 'backup.ssh_key_path',
|
||||
ssh_key_passphrase: 'backup.ssh_key_passphrase',
|
||||
backup_interval: 'backup.backup_interval',
|
||||
auto_backup: 'backup.auto_backup',
|
||||
|
||||
} as const;
|
||||
|
||||
// 配置限制
|
||||
const CONFIG_LIMITS = {
|
||||
fontSize: {min: 12, max: 28, default: 13},
|
||||
@@ -158,7 +178,9 @@ const DEFAULT_CONFIG: AppConfig = {
|
||||
alt: true,
|
||||
win: false,
|
||||
key: 'X'
|
||||
}
|
||||
},
|
||||
enableWindowSnap: true,
|
||||
snapThreshold: 15
|
||||
},
|
||||
editing: {
|
||||
fontSize: CONFIG_LIMITS.fontSize.default,
|
||||
@@ -172,77 +194,7 @@ const DEFAULT_CONFIG: AppConfig = {
|
||||
},
|
||||
appearance: {
|
||||
language: LanguageType.LangZhCN,
|
||||
systemTheme: SystemThemeType.SystemThemeAuto,
|
||||
customTheme: {
|
||||
darkTheme: {
|
||||
// 基础色调
|
||||
background: '#252B37',
|
||||
backgroundSecondary: '#213644',
|
||||
surface: '#474747',
|
||||
foreground: '#9BB586',
|
||||
foregroundSecondary: '#9c9c9c',
|
||||
|
||||
// 语法高亮
|
||||
comment: '#6272a4',
|
||||
keyword: '#ff79c6',
|
||||
string: '#f1fa8c',
|
||||
function: '#50fa7b',
|
||||
number: '#bd93f9',
|
||||
operator: '#ff79c6',
|
||||
variable: '#8fbcbb',
|
||||
type: '#8be9fd',
|
||||
|
||||
// 界面元素
|
||||
cursor: '#fff',
|
||||
selection: '#0865a9aa',
|
||||
selectionBlur: '#225377aa',
|
||||
activeLine: 'rgba(255,255,255,0.04)',
|
||||
lineNumber: 'rgba(255,255,255, 0.15)',
|
||||
activeLineNumber: 'rgba(255,255,255, 0.6)',
|
||||
|
||||
// 边框分割线
|
||||
borderColor: '#1e222a',
|
||||
borderLight: 'rgba(255,255,255, 0.1)',
|
||||
|
||||
// 搜索匹配
|
||||
searchMatch: '#8fbcbb',
|
||||
matchingBracket: 'rgba(255,255,255,0.1)'
|
||||
},
|
||||
lightTheme: {
|
||||
// 基础色调
|
||||
background: '#ffffff',
|
||||
backgroundSecondary: '#f1faf1',
|
||||
surface: '#f5f5f5',
|
||||
foreground: '#444d56',
|
||||
foregroundSecondary: '#6a737d',
|
||||
|
||||
// 语法高亮
|
||||
comment: '#6a737d',
|
||||
keyword: '#d73a49',
|
||||
string: '#032f62',
|
||||
function: '#005cc5',
|
||||
number: '#005cc5',
|
||||
operator: '#d73a49',
|
||||
variable: '#24292e',
|
||||
type: '#6f42c1',
|
||||
|
||||
// 界面元素
|
||||
cursor: '#000',
|
||||
selection: '#77baff8c',
|
||||
selectionBlur: '#b2c2ca85',
|
||||
activeLine: '#000000',
|
||||
lineNumber: '#000000',
|
||||
activeLineNumber: '#000000',
|
||||
|
||||
// 边框分割线
|
||||
borderColor: '#dfdfdf',
|
||||
borderLight: '#0000000C',
|
||||
|
||||
// 搜索匹配
|
||||
searchMatch: '#005cc5',
|
||||
matchingBracket: 'rgba(0,0,0,0.1)'
|
||||
}
|
||||
}
|
||||
systemTheme: SystemThemeType.SystemThemeAuto
|
||||
},
|
||||
updates: {
|
||||
version: "1.0.0",
|
||||
@@ -261,6 +213,18 @@ const DEFAULT_CONFIG: AppConfig = {
|
||||
repo: "voidraft",
|
||||
}
|
||||
},
|
||||
backup: {
|
||||
enabled: false,
|
||||
repo_url: "",
|
||||
auth_method: AuthMethod.UserPass,
|
||||
username: "",
|
||||
password: "",
|
||||
token: "",
|
||||
ssh_key_path: "",
|
||||
ssh_key_passphrase: "",
|
||||
backup_interval: 60,
|
||||
auto_backup: true,
|
||||
},
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastUpdated: new Date().toString(),
|
||||
@@ -348,6 +312,21 @@ export const useConfigStore = defineStore('config', () => {
|
||||
state.config.updates[key] = value;
|
||||
};
|
||||
|
||||
const updateBackupConfig = async <K extends keyof GitBackupConfig>(key: K, value: GitBackupConfig[K]): Promise<void> => {
|
||||
// 确保配置已加载
|
||||
if (!state.configLoaded && !state.isLoading) {
|
||||
await initConfig();
|
||||
}
|
||||
|
||||
const backendKey = BACKUP_CONFIG_KEY_MAP[key];
|
||||
if (!backendKey) {
|
||||
throw new Error(`No backend key mapping found for backup.${key.toString()}`);
|
||||
}
|
||||
|
||||
await ConfigService.Set(backendKey, value);
|
||||
state.config.backup[key] = value;
|
||||
};
|
||||
|
||||
// 加载配置
|
||||
const initConfig = async (): Promise<void> => {
|
||||
if (state.isLoading) return;
|
||||
@@ -362,14 +341,12 @@ export const useConfigStore = defineStore('config', () => {
|
||||
if (appConfig.editing) Object.assign(state.config.editing, appConfig.editing);
|
||||
if (appConfig.appearance) Object.assign(state.config.appearance, appConfig.appearance);
|
||||
if (appConfig.updates) Object.assign(state.config.updates, appConfig.updates);
|
||||
if (appConfig.backup) Object.assign(state.config.backup, appConfig.backup);
|
||||
if (appConfig.metadata) Object.assign(state.config.metadata, appConfig.metadata);
|
||||
}
|
||||
|
||||
state.configLoaded = true;
|
||||
|
||||
// 初始化热键监听器
|
||||
const windowController = WindowController.getInstance();
|
||||
await windowController.initializeHotkeyListener();
|
||||
} finally {
|
||||
state.isLoading = false;
|
||||
}
|
||||
@@ -434,50 +411,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||
await updateAppearanceConfig('systemTheme', systemTheme);
|
||||
};
|
||||
|
||||
// 更新自定义主题方法
|
||||
const updateCustomTheme = async (themeType: 'darkTheme' | 'lightTheme', colorKey: string, colorValue: string): Promise<void> => {
|
||||
// 确保配置已加载
|
||||
if (!state.configLoaded && !state.isLoading) {
|
||||
await initConfig();
|
||||
}
|
||||
|
||||
try {
|
||||
// 深拷贝当前配置
|
||||
const customTheme = JSON.parse(JSON.stringify(state.config.appearance.customTheme));
|
||||
|
||||
// 更新对应主题的颜色值
|
||||
customTheme[themeType][colorKey] = colorValue;
|
||||
|
||||
// 更新整个自定义主题配置到后端
|
||||
await ConfigService.Set(APPEARANCE_CONFIG_KEY_MAP.customTheme, customTheme);
|
||||
|
||||
// 更新前端状态
|
||||
state.config.appearance.customTheme = customTheme;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 设置整个自定义主题配置
|
||||
const setCustomTheme = async (customTheme: any): Promise<void> => {
|
||||
// 确保配置已加载
|
||||
if (!state.configLoaded && !state.isLoading) {
|
||||
await initConfig();
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新整个自定义主题配置到后端
|
||||
await ConfigService.Set(APPEARANCE_CONFIG_KEY_MAP.customTheme, customTheme);
|
||||
|
||||
// 更新前端状态
|
||||
state.config.appearance.customTheme = customTheme;
|
||||
|
||||
// 确保Vue能检测到变化
|
||||
state.config.appearance = { ...state.config.appearance };
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化语言设置
|
||||
const initializeLanguage = async (): Promise<void> => {
|
||||
@@ -542,8 +476,6 @@ export const useConfigStore = defineStore('config', () => {
|
||||
|
||||
// 主题相关方法
|
||||
setSystemTheme,
|
||||
updateCustomTheme,
|
||||
setCustomTheme,
|
||||
|
||||
// 字体大小操作
|
||||
...adjusters.fontSize,
|
||||
@@ -593,7 +525,23 @@ export const useConfigStore = defineStore('config', () => {
|
||||
await StartupService.SetEnabled(value);
|
||||
},
|
||||
|
||||
// 窗口吸附配置相关方法
|
||||
setEnableWindowSnap: async (value: boolean) => await updateGeneralConfig('enableWindowSnap', value),
|
||||
setSnapThreshold: async (value: number) => await updateGeneralConfig('snapThreshold', value),
|
||||
|
||||
// 更新配置相关方法
|
||||
setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value)
|
||||
setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value),
|
||||
|
||||
// 备份配置相关方法
|
||||
setEnableBackup: async (value: boolean) => {await updateBackupConfig('enabled', value);},
|
||||
setAutoBackup: async (value: boolean) => {await updateBackupConfig('auto_backup', value);},
|
||||
setRepoUrl: async (value: string) => await updateBackupConfig('repo_url', value),
|
||||
setAuthMethod: async (value: AuthMethod) => await updateBackupConfig('auth_method', value),
|
||||
setUsername: async (value: string) => await updateBackupConfig('username', value),
|
||||
setPassword: async (value: string) => await updateBackupConfig('password', value),
|
||||
setToken: async (value: string) => await updateBackupConfig('token', value),
|
||||
setSshKeyPath: async (value: string) => await updateBackupConfig('ssh_key_path', value),
|
||||
setSshKeyPassphrase: async (value: string) => await updateBackupConfig('ssh_key_passphrase', value),
|
||||
setBackupInterval: async (value: number) => await updateBackupConfig('backup_interval', value),
|
||||
};
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, reactive} from 'vue';
|
||||
import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {SystemThemeType, ThemeType, ThemeColorConfig} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {ThemeService} from '@/../bindings/voidraft/internal/services';
|
||||
import {useConfigStore} from './configStore';
|
||||
import {useEditorStore} from './editorStore';
|
||||
import {defaultDarkColors} from '@/views/editor/theme/dark';
|
||||
@@ -24,16 +25,21 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
|
||||
);
|
||||
|
||||
// 初始化主题颜色 - 从配置加载
|
||||
const initializeThemeColors = () => {
|
||||
const customTheme = configStore.config?.appearance?.customTheme;
|
||||
if (customTheme) {
|
||||
if (customTheme.darkTheme) {
|
||||
Object.assign(themeColors.darkTheme, customTheme.darkTheme);
|
||||
// 初始化主题颜色 - 从数据库加载
|
||||
const initializeThemeColors = async () => {
|
||||
try {
|
||||
const themes = await ThemeService.GetDefaultThemes();
|
||||
if (themes.dark) {
|
||||
Object.assign(themeColors.darkTheme, themes.dark.colors);
|
||||
}
|
||||
if (customTheme.lightTheme) {
|
||||
Object.assign(themeColors.lightTheme, customTheme.lightTheme);
|
||||
if (themes.light) {
|
||||
Object.assign(themeColors.lightTheme, themes.light.colors);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load themes from database, using defaults:', error);
|
||||
// 如果数据库加载失败,使用默认主题
|
||||
Object.assign(themeColors.darkTheme, defaultDarkColors);
|
||||
Object.assign(themeColors.lightTheme, defaultLightColors);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -46,10 +52,10 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
};
|
||||
|
||||
// 初始化主题
|
||||
const initializeTheme = () => {
|
||||
const initializeTheme = async () => {
|
||||
const theme = configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto;
|
||||
applyThemeToDOM(theme);
|
||||
initializeThemeColors();
|
||||
await initializeThemeColors();
|
||||
};
|
||||
|
||||
// 设置主题
|
||||
@@ -84,31 +90,35 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
return hasChanges;
|
||||
};
|
||||
|
||||
// 保存主题颜色到配置
|
||||
// 保存主题颜色到数据库
|
||||
const saveThemeColors = async () => {
|
||||
const customTheme = {
|
||||
darkTheme: { ...themeColors.darkTheme },
|
||||
lightTheme: { ...themeColors.lightTheme }
|
||||
};
|
||||
try {
|
||||
const darkColors = ThemeColorConfig.createFrom(themeColors.darkTheme);
|
||||
const lightColors = ThemeColorConfig.createFrom(themeColors.lightTheme);
|
||||
|
||||
await configStore.setCustomTheme(customTheme);
|
||||
await ThemeService.UpdateThemeColors(ThemeType.ThemeTypeDark, darkColors);
|
||||
await ThemeService.UpdateThemeColors(ThemeType.ThemeTypeLight, lightColors);
|
||||
} catch (error) {
|
||||
console.error('Failed to save theme colors:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 重置主题颜色
|
||||
const resetThemeColors = async (themeType: 'darkTheme' | 'lightTheme') => {
|
||||
try {
|
||||
// 1. 更新内存中的颜色状态
|
||||
const dbThemeType = themeType === 'darkTheme' ? ThemeType.ThemeTypeDark : ThemeType.ThemeTypeLight;
|
||||
|
||||
// 1. 调用后端重置服务
|
||||
await ThemeService.ResetThemeColors(dbThemeType);
|
||||
|
||||
// 2. 更新内存中的颜色状态
|
||||
if (themeType === 'darkTheme') {
|
||||
Object.assign(themeColors.darkTheme, defaultDarkColors);
|
||||
}
|
||||
|
||||
if (themeType === 'lightTheme') {
|
||||
} else {
|
||||
Object.assign(themeColors.lightTheme, defaultLightColors);
|
||||
}
|
||||
|
||||
// 2. 保存到配置
|
||||
await saveThemeColors();
|
||||
|
||||
// 3. 刷新编辑器主题
|
||||
refreshEditorTheme();
|
||||
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import * as wails from '@wailsio/runtime'
|
||||
|
||||
// 窗口控制工具类
|
||||
export class WindowController {
|
||||
private static instance: WindowController;
|
||||
private currentWindow = wails.Window;
|
||||
private isWindowVisible: boolean = true; // 跟踪窗口可见状态
|
||||
private isInitialized: boolean = false; // 跟踪是否已初始化
|
||||
private isToggling: boolean = false; // 防止重复切换
|
||||
private lastToggleTime: number = 0; // 上次切换时间
|
||||
private readonly TOGGLE_COOLDOWN = 500; // 切换冷却时间(毫秒)
|
||||
|
||||
static getInstance(): WindowController {
|
||||
if (!WindowController.instance) {
|
||||
WindowController.instance = new WindowController();
|
||||
}
|
||||
return WindowController.instance;
|
||||
}
|
||||
|
||||
async toggleWindow(): Promise<void> {
|
||||
const now = Date.now();
|
||||
|
||||
// 防抖检查
|
||||
if (this.isToggling || (now - this.lastToggleTime) < this.TOGGLE_COOLDOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isToggling = true;
|
||||
this.lastToggleTime = now;
|
||||
|
||||
try {
|
||||
// 如果还没初始化,先初始化状态
|
||||
if (!this.isInitialized) {
|
||||
await this.syncWindowState();
|
||||
}
|
||||
|
||||
if (!this.isWindowVisible) {
|
||||
// 窗口当前隐藏,显示它
|
||||
await this.currentWindow.Show();
|
||||
await this.currentWindow.UnMinimise(); // 修正API名称
|
||||
await this.currentWindow.Focus();
|
||||
this.isWindowVisible = true;
|
||||
} else {
|
||||
// 窗口当前可见,隐藏它
|
||||
await this.currentWindow.Hide();
|
||||
this.isWindowVisible = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
// 延迟重置切换状态,确保操作完成
|
||||
setTimeout(() => {
|
||||
this.isToggling = false;
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// 同步窗口状态
|
||||
private async syncWindowState(): Promise<void> {
|
||||
try {
|
||||
// 检查窗口是否最小化
|
||||
const isMinimised = await this.currentWindow.IsMinimised();
|
||||
|
||||
// 简化状态判断:只要不是最小化状态就认为是可见的
|
||||
this.isWindowVisible = !isMinimised;
|
||||
|
||||
this.isInitialized = true;
|
||||
} catch (error) {
|
||||
// 如果检查失败,保持默认状态
|
||||
this.isWindowVisible = true;
|
||||
this.isInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 当窗口被系统事件隐藏时调用(比如点击关闭/最小化按钮)
|
||||
onWindowHidden(): void {
|
||||
this.isWindowVisible = false;
|
||||
}
|
||||
|
||||
// 当窗口被系统事件显示时调用(比如点击托盘图标)
|
||||
onWindowShown(): void {
|
||||
this.isWindowVisible = true;
|
||||
}
|
||||
|
||||
async initializeHotkeyListener(): Promise<void> {
|
||||
// 初始化时同步窗口状态
|
||||
await this.syncWindowState();
|
||||
|
||||
// 监听后端发送的热键事件
|
||||
wails.Events.On('hotkey:toggle-window', () => {
|
||||
this.toggleWindow();
|
||||
});
|
||||
|
||||
// 监听窗口显示/隐藏事件以同步状态
|
||||
wails.Events.On('window:shown', () => {
|
||||
this.onWindowShown();
|
||||
});
|
||||
|
||||
wails.Events.On('window:hidden', () => {
|
||||
this.onWindowHidden();
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ const navItems = [
|
||||
{ id: 'general', icon: '⚙️', route: '/settings/general' },
|
||||
{ id: 'editing', icon: '✏️', route: '/settings/editing' },
|
||||
{ id: 'appearance', icon: '🎨', route: '/settings/appearance' },
|
||||
{ id: 'backupPage', icon: '🔗', route: '/settings/backup' },
|
||||
{ id: 'extensions', icon: '🧩', route: '/settings/extensions' },
|
||||
{ id: 'keyBindings', icon: '⌨️', route: '/settings/key-bindings' },
|
||||
{ id: 'updates', icon: '🔄', route: '/settings/updates' }
|
||||
@@ -194,8 +195,8 @@ const goBackToEditor = async () => {
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
padding: 0 20px;
|
||||
overflow-y: scroll;
|
||||
background-color: var(--settings-bg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ const themeStore = useThemeStore();
|
||||
|
||||
// 添加临时颜色状态
|
||||
const tempColors = ref({
|
||||
darkTheme: { ...configStore.config.appearance.customTheme?.darkTheme || defaultDarkColors },
|
||||
lightTheme: { ...configStore.config.appearance.customTheme?.lightTheme || defaultLightColors }
|
||||
darkTheme: { ...defaultDarkColors },
|
||||
lightTheme: { ...defaultLightColors }
|
||||
});
|
||||
|
||||
// 标记是否有未保存的更改
|
||||
@@ -71,9 +71,9 @@ const currentThemeMode = computed(() => {
|
||||
return isDark ? 'dark' : 'light';
|
||||
});
|
||||
|
||||
// 监听配置变更,更新临时颜色
|
||||
// 监听主题颜色变更,更新临时颜色
|
||||
watch(
|
||||
() => configStore.config.appearance.customTheme,
|
||||
() => themeStore.themeColors,
|
||||
(newValue) => {
|
||||
if (!hasUnsavedChanges.value) {
|
||||
tempColors.value = {
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
<script setup lang="ts">
|
||||
import {useConfigStore} from '@/stores/configStore';
|
||||
import {useBackupStore} from '@/stores/backupStore';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {computed, onMounted, onUnmounted} from 'vue';
|
||||
import SettingSection from '../components/SettingSection.vue';
|
||||
import SettingItem from '../components/SettingItem.vue';
|
||||
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||
import {AuthMethod} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {DialogService} from '@/../bindings/voidraft/internal/services';
|
||||
|
||||
const {t} = useI18n();
|
||||
const configStore = useConfigStore();
|
||||
const backupStore = useBackupStore();
|
||||
|
||||
// 确保配置已加载
|
||||
onMounted(async () => {
|
||||
if (!configStore.configLoaded) {
|
||||
await configStore.initConfig();
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
backupStore.clearError();
|
||||
})
|
||||
|
||||
// 认证方式选项
|
||||
const authMethodOptions = computed(() => [
|
||||
{value: AuthMethod.Token, label: t('settings.backup.authMethods.token')},
|
||||
{value: AuthMethod.SSHKey, label: t('settings.backup.authMethods.sshKey')},
|
||||
{value: AuthMethod.UserPass, label: t('settings.backup.authMethods.userPass')}
|
||||
]);
|
||||
|
||||
// 备份间隔选项(分钟)
|
||||
const backupIntervalOptions = computed(() => [
|
||||
{value: 5, label: t('settings.backup.intervals.5min')},
|
||||
{value: 10, label: t('settings.backup.intervals.10min')},
|
||||
{value: 15, label: t('settings.backup.intervals.15min')},
|
||||
{value: 30, label: t('settings.backup.intervals.30min')},
|
||||
{value: 60, label: t('settings.backup.intervals.1hour')}
|
||||
]);
|
||||
|
||||
// 计算属性 - 启用备份
|
||||
const enableBackup = computed({
|
||||
get: () => configStore.config.backup.enabled,
|
||||
set: (value: boolean) => configStore.setEnableBackup(value)
|
||||
});
|
||||
|
||||
// 计算属性 - 自动备份
|
||||
const autoBackup = computed({
|
||||
get: () => configStore.config.backup.auto_backup,
|
||||
set: (value: boolean) => configStore.setAutoBackup(value)
|
||||
});
|
||||
|
||||
// 仓库URL
|
||||
const repoUrl = computed({
|
||||
get: () => configStore.config.backup.repo_url,
|
||||
set: (value: string) => configStore.setRepoUrl(value)
|
||||
});
|
||||
|
||||
|
||||
// 认证方式
|
||||
const authMethod = computed({
|
||||
get: () => configStore.config.backup.auth_method,
|
||||
set: (value: AuthMethod) => configStore.setAuthMethod(value)
|
||||
});
|
||||
|
||||
// 备份间隔
|
||||
const backupInterval = computed({
|
||||
get: () => configStore.config.backup.backup_interval,
|
||||
set: (value: number) => configStore.setBackupInterval(value)
|
||||
});
|
||||
|
||||
// 用户名
|
||||
const username = computed({
|
||||
get: () => configStore.config.backup.username,
|
||||
set: (value: string) => configStore.setUsername(value)
|
||||
});
|
||||
|
||||
// 密码
|
||||
const password = computed({
|
||||
get: () => configStore.config.backup.password,
|
||||
set: (value: string) => configStore.setPassword(value)
|
||||
});
|
||||
|
||||
// 访问令牌
|
||||
const token = computed({
|
||||
get: () => configStore.config.backup.token,
|
||||
set: (value: string) => configStore.setToken(value)
|
||||
});
|
||||
|
||||
// SSH密钥路径
|
||||
const sshKeyPath = computed({
|
||||
get: () => configStore.config.backup.ssh_key_path,
|
||||
set: (value: string) => configStore.setSshKeyPath(value)
|
||||
});
|
||||
|
||||
// SSH密钥密码
|
||||
const sshKeyPassphrase = computed({
|
||||
get: () => configStore.config.backup.ssh_key_passphrase,
|
||||
set: (value: string) => configStore.setSshKeyPassphrase(value)
|
||||
});
|
||||
|
||||
// 处理输入变化
|
||||
const handleRepoUrlChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
repoUrl.value = target.value;
|
||||
};
|
||||
|
||||
|
||||
const handleUsernameChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
username.value = target.value;
|
||||
};
|
||||
|
||||
const handlePasswordChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
password.value = target.value;
|
||||
};
|
||||
|
||||
const handleTokenChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
token.value = target.value;
|
||||
};
|
||||
|
||||
const handleSshKeyPassphraseChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
sshKeyPassphrase.value = target.value;
|
||||
};
|
||||
|
||||
const handleAuthMethodChange = (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
authMethod.value = target.value as AuthMethod;
|
||||
};
|
||||
|
||||
const handleBackupIntervalChange = (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
backupInterval.value = parseInt(target.value);
|
||||
};
|
||||
|
||||
// 推送到远程
|
||||
const pushToRemote = async () => {
|
||||
await backupStore.pushToRemote();
|
||||
};
|
||||
|
||||
// 选择SSH密钥文件
|
||||
const selectSshKeyFile = async () => {
|
||||
// 使用DialogService选择文件
|
||||
const selectedPath = await DialogService.SelectFile();
|
||||
// 检查用户是否取消了选择或路径为空
|
||||
if (!selectedPath.trim()) {
|
||||
return;
|
||||
}
|
||||
// 更新SSH密钥路径
|
||||
sshKeyPath.value = selectedPath.trim();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<!-- 基本设置 -->
|
||||
<SettingSection :title="t('settings.backup.basicSettings')">
|
||||
<SettingItem
|
||||
:title="t('settings.backup.enableBackup')"
|
||||
>
|
||||
<ToggleSwitch v-model="enableBackup"/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
:title="t('settings.backup.autoBackup')"
|
||||
:class="{ 'disabled-setting': !enableBackup }"
|
||||
>
|
||||
<ToggleSwitch v-model="autoBackup" :disabled="!enableBackup"/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
:title="t('settings.backup.backupInterval')"
|
||||
:class="{ 'disabled-setting': !enableBackup || !autoBackup }"
|
||||
>
|
||||
<select
|
||||
class="backup-interval-select"
|
||||
:value="backupInterval"
|
||||
@change="handleBackupIntervalChange"
|
||||
:disabled="!enableBackup || !autoBackup"
|
||||
>
|
||||
<option
|
||||
v-for="option in backupIntervalOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</SettingItem>
|
||||
</SettingSection>
|
||||
|
||||
<!-- 仓库配置 -->
|
||||
<SettingSection :title="t('settings.backup.repositoryConfig')">
|
||||
<SettingItem
|
||||
:title="t('settings.backup.repoUrl')"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="repo-url-input"
|
||||
:value="repoUrl"
|
||||
@input="handleRepoUrlChange"
|
||||
:placeholder="t('settings.backup.repoUrlPlaceholder')"
|
||||
:disabled="!enableBackup"
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
|
||||
</SettingSection>
|
||||
|
||||
<!-- 认证配置 -->
|
||||
<SettingSection :title="t('settings.backup.authConfig')">
|
||||
<SettingItem
|
||||
:title="t('settings.backup.authMethod')"
|
||||
>
|
||||
<select
|
||||
class="auth-method-select"
|
||||
:value="authMethod"
|
||||
@change="handleAuthMethodChange"
|
||||
:disabled="!enableBackup"
|
||||
>
|
||||
<option
|
||||
v-for="option in authMethodOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</SettingItem>
|
||||
|
||||
<!-- 用户名密码认证 -->
|
||||
<template v-if="authMethod === AuthMethod.UserPass">
|
||||
<SettingItem :title="t('settings.backup.username')">
|
||||
<input
|
||||
type="text"
|
||||
class="username-input"
|
||||
:value="username"
|
||||
@input="handleUsernameChange"
|
||||
:placeholder="t('settings.backup.usernamePlaceholder')"
|
||||
:disabled="!enableBackup"
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem :title="t('settings.backup.password')">
|
||||
<input
|
||||
type="password"
|
||||
class="password-input"
|
||||
:value="password"
|
||||
@input="handlePasswordChange"
|
||||
:placeholder="t('settings.backup.passwordPlaceholder')"
|
||||
:disabled="!enableBackup"
|
||||
/>
|
||||
</SettingItem>
|
||||
</template>
|
||||
|
||||
<!-- 访问令牌认证 -->
|
||||
<template v-if="authMethod === AuthMethod.Token">
|
||||
<SettingItem
|
||||
:title="t('settings.backup.token')"
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
class="token-input"
|
||||
:value="token"
|
||||
@input="handleTokenChange"
|
||||
:placeholder="t('settings.backup.tokenPlaceholder')"
|
||||
:disabled="!enableBackup"
|
||||
/>
|
||||
</SettingItem>
|
||||
</template>
|
||||
|
||||
<!-- SSH密钥认证 -->
|
||||
<template v-if="authMethod === AuthMethod.SSHKey">
|
||||
<SettingItem
|
||||
:title="t('settings.backup.sshKeyPath')"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="ssh-key-path-input"
|
||||
:value="sshKeyPath"
|
||||
:placeholder="t('settings.backup.sshKeyPathPlaceholder')"
|
||||
:disabled="!enableBackup"
|
||||
readonly
|
||||
@click="enableBackup && selectSshKeyFile()"
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
:title="t('settings.backup.sshKeyPassphrase')"
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
class="ssh-passphrase-input"
|
||||
:value="sshKeyPassphrase"
|
||||
@input="handleSshKeyPassphraseChange"
|
||||
:placeholder="t('settings.backup.sshKeyPassphrasePlaceholder')"
|
||||
:disabled="!enableBackup"
|
||||
/>
|
||||
</SettingItem>
|
||||
</template>
|
||||
</SettingSection>
|
||||
|
||||
<!-- 备份操作 -->
|
||||
<SettingSection :title="t('settings.backup.backupOperations')">
|
||||
<SettingItem
|
||||
:title="t('settings.backup.pushToRemote')"
|
||||
>
|
||||
<div class="backup-operation-container">
|
||||
<div class="backup-status-icons">
|
||||
<span v-if="backupStore.pushSuccess" class="success-icon">✓</span>
|
||||
<span v-if="backupStore.pushError" class="error-icon">✗</span>
|
||||
</div>
|
||||
<button
|
||||
class="push-button"
|
||||
@click="() => pushToRemote()"
|
||||
:disabled="!enableBackup || !repoUrl || backupStore.isPushing"
|
||||
:class="{ 'backing-up': backupStore.isPushing }"
|
||||
>
|
||||
<span v-if="backupStore.isPushing" class="loading-spinner"></span>
|
||||
{{ backupStore.isPushing ? t('settings.backup.pushing') : t('settings.backup.actions.push') }}
|
||||
</button>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<div v-if="backupStore.error" class="error-message-row">
|
||||
{{ backupStore.error }}
|
||||
</div>
|
||||
</SettingSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.settings-page {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
// 统一的输入控件样式
|
||||
.repo-url-input,
|
||||
.branch-input,
|
||||
.username-input,
|
||||
.password-input,
|
||||
.token-input,
|
||||
.ssh-key-path-input,
|
||||
.ssh-passphrase-input,
|
||||
.backup-interval-select,
|
||||
.auth-method-select {
|
||||
width: 50%;
|
||||
min-width: 200px;
|
||||
padding: 10px 12px;
|
||||
background-color: var(--settings-input-bg);
|
||||
border: 1px solid var(--settings-input-border);
|
||||
border-radius: 4px;
|
||||
color: var(--settings-text);
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background-color: var(--settings-hover);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--settings-text-secondary);
|
||||
}
|
||||
|
||||
&[readonly]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--settings-hover);
|
||||
background-color: var(--settings-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 选择框特有样式
|
||||
.backup-interval-select,
|
||||
.auth-method-select {
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23999999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 16px;
|
||||
padding-right: 30px;
|
||||
|
||||
option {
|
||||
background-color: var(--settings-input-bg);
|
||||
color: var(--settings-text);
|
||||
}
|
||||
}
|
||||
|
||||
// 备份操作容器
|
||||
.backup-operation-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
// 备份状态图标
|
||||
.backup-status-icons {
|
||||
width: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
// 成功和错误图标
|
||||
.success-icon {
|
||||
color: #4caf50;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
color: #f44336;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// 按钮样式
|
||||
.push-button {
|
||||
padding: 8px 16px;
|
||||
background-color: var(--settings-input-bg);
|
||||
border: 1px solid var(--settings-input-border);
|
||||
border-radius: 4px;
|
||||
color: var(--settings-text);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--settings-hover);
|
||||
border-color: var(--settings-border);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: var(--settings-text);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
&.backing-up {
|
||||
background-color: #2196f3;
|
||||
border-color: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
// 错误信息行样式
|
||||
.error-message-row {
|
||||
color: #f44336;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
margin-top: 8px;
|
||||
padding: 8px 16px;
|
||||
background-color: rgba(244, 67, 54, 0.1);
|
||||
border-left: 3px solid #f44336;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
// 禁用状态
|
||||
.disabled-setting {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// 加载动画
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -159,6 +159,12 @@ const enableSystemTray = computed({
|
||||
set: (value: boolean) => configStore.setEnableSystemTray(value)
|
||||
});
|
||||
|
||||
// 计算属性 - 启用窗口吸附
|
||||
const enableWindowSnap = computed({
|
||||
get: () => configStore.config.general.enableWindowSnap,
|
||||
set: (value: boolean) => configStore.setEnableWindowSnap(value)
|
||||
});
|
||||
|
||||
// 计算属性 - 开机启动
|
||||
const startAtLogin = computed({
|
||||
get: () => configStore.config.general.startAtLogin,
|
||||
@@ -334,6 +340,9 @@ onUnmounted(() => {
|
||||
<SettingItem :title="t('settings.enableSystemTray')">
|
||||
<ToggleSwitch v-model="enableSystemTray"/>
|
||||
</SettingItem>
|
||||
<SettingItem :title="t('settings.enableWindowSnap')">
|
||||
<ToggleSwitch v-model="enableWindowSnap"/>
|
||||
</SettingItem>
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection :title="t('settings.startup')">
|
||||
|
||||
@@ -266,6 +266,7 @@ const currentVersion = computed(() => {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--settings-border, rgba(0,0,0,0.1));
|
||||
background: transparent;
|
||||
|
||||
.notes-title {
|
||||
font-size: 12px;
|
||||
@@ -278,24 +279,76 @@ const currentVersion = computed(() => {
|
||||
font-size: 12px;
|
||||
color: var(--settings-text);
|
||||
line-height: 1.4;
|
||||
background: transparent;
|
||||
|
||||
/* Markdown内容样式 */
|
||||
:deep(p) {
|
||||
margin: 0 0 6px 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(ul), :deep(ol) {
|
||||
margin: 6px 0;
|
||||
padding-left: 16px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(li) {
|
||||
margin-bottom: 4px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
|
||||
margin: 10px 0 6px 0;
|
||||
font-size: 13px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(pre), :deep(code) {
|
||||
background-color: var(--settings-code-bg, rgba(0,0,0,0.05));
|
||||
border-radius: 3px;
|
||||
padding: 2px 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
:deep(pre) {
|
||||
padding: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
:deep(blockquote) {
|
||||
border-left: 3px solid var(--settings-border, rgba(0,0,0,0.1));
|
||||
margin: 6px 0;
|
||||
padding-left: 10px;
|
||||
color: var(--settings-text-secondary, #757575);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
color: var(--theme-primary, #2196f3);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(table) {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 6px 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(th), :deep(td) {
|
||||
border: 1px solid var(--settings-border, rgba(0,0,0,0.1));
|
||||
padding: 4px 8px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(th) {
|
||||
background-color: var(--settings-table-header-bg, rgba(0,0,0,0.02));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,17 @@ go 1.24.4
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/creativeprojects/go-selfupdate v1.5.0
|
||||
github.com/go-git/go-git/v5 v5.16.2
|
||||
github.com/knadh/koanf/parsers/json v1.0.0
|
||||
github.com/knadh/koanf/providers/file v1.2.0
|
||||
github.com/knadh/koanf/providers/structs v1.0.0
|
||||
github.com/knadh/koanf/v2 v2.2.1
|
||||
github.com/knadh/koanf/v2 v2.2.2
|
||||
github.com/robertkrimen/otto v0.5.1
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.10
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/sys v0.33.0
|
||||
golang.org/x/text v0.26.0
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.25
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/sys v0.35.0
|
||||
golang.org/x/text v0.28.0
|
||||
modernc.org/sqlite v1.38.2
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -35,9 +37,8 @@ require (
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-git/go-git/v5 v5.16.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/go-github/v30 v30.1.0 // indirect
|
||||
@@ -66,21 +67,20 @@ require (
|
||||
github.com/samber/lo v1.51.0 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/ulikunitz/xz v0.5.14 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.21 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
github.com/xanzy/go-gitlab v0.115.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
modernc.org/libc v1.66.7 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.38.0 // indirect
|
||||
)
|
||||
@@ -58,8 +58,8 @@ github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77
|
||||
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
|
||||
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
@@ -99,8 +99,8 @@ github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmY
|
||||
github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
|
||||
github.com/knadh/koanf/providers/structs v1.0.0 h1:DznjB7NQykhqCar2LvNug3MuxEQsZ5KvfgMbio+23u4=
|
||||
github.com/knadh/koanf/providers/structs v1.0.0/go.mod h1:kjo5TFtgpaZORlpoJqcbeLowM2cINodv8kX+oFAeQ1w=
|
||||
github.com/knadh/koanf/v2 v2.2.1 h1:jaleChtw85y3UdBnI0wCqcg1sj1gPoz6D3caGNHtrNE=
|
||||
github.com/knadh/koanf/v2 v2.2.1/go.mod h1:PSFru3ufQgTsI7IF+95rf9s8XA1+aHxKuO/W+dPoHEY=
|
||||
github.com/knadh/koanf/v2 v2.2.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A=
|
||||
github.com/knadh/koanf/v2 v2.2.2/go.mod h1:abWQc0cBXLSF/PSOMCB/SK+T13NXDsPvOksbpi5e/9Q=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -158,14 +158,14 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
|
||||
github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA=
|
||||
github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.10 h1:SrxwhkBcdtaSxQ/zujJuifJN5q8hxyba5UKv5oaM/X4=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.10/go.mod h1:4LCCW7s9e4PuSmu7l9OTvfWIGMO8TaSiftSeR5NpBIc=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.25 h1:o05zUiPEvmrq2lqqCs4wqnrnAjGmhryYHRhjQmtkvk8=
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.25/go.mod h1:UZpnhYuju4saspCJrIHAvC0H5XjtKnqd26FRxJLrQ0M=
|
||||
github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
|
||||
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
@@ -174,24 +174,24 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -203,21 +203,21 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -234,18 +234,18 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
|
||||
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/fileutil v1.3.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
|
||||
modernc.org/fileutil v1.3.15/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
|
||||
modernc.org/libc v1.66.7/go.mod h1:ln6tbWX0NH+mzApEoDRvilBvAWFt1HX7AUA4VDdVDPM=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -254,8 +254,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
|
||||
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
|
||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package models
|
||||
|
||||
// Git备份相关类型定义
|
||||
type (
|
||||
// AuthMethod 定义Git认证方式
|
||||
AuthMethod string
|
||||
)
|
||||
|
||||
const (
|
||||
// 认证方式
|
||||
Token AuthMethod = "token"
|
||||
SSHKey AuthMethod = "ssh_key"
|
||||
UserPass AuthMethod = "user_pass"
|
||||
)
|
||||
|
||||
// GitBackupConfig Git备份配置
|
||||
type GitBackupConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
RepoURL string `json:"repo_url"`
|
||||
AuthMethod AuthMethod `json:"auth_method"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
SSHKeyPath string `json:"ssh_key_path,omitempty"`
|
||||
SSHKeyPass string `json:"ssh_key_passphrase,omitempty"`
|
||||
BackupInterval int `json:"backup_interval"` // 分钟
|
||||
AutoBackup bool `json:"auto_backup"`
|
||||
}
|
||||
@@ -68,6 +68,10 @@ type GeneralConfig struct {
|
||||
EnableSystemTray bool `json:"enableSystemTray"` // 是否启用系统托盘
|
||||
StartAtLogin bool `json:"startAtLogin"` // 开机启动设置
|
||||
|
||||
// 窗口吸附设置
|
||||
EnableWindowSnap bool `json:"enableWindowSnap"` // 是否启用窗口吸附功能
|
||||
SnapThreshold int `json:"snapThreshold"` // 吸附距离阈值(像素)
|
||||
|
||||
// 全局热键设置
|
||||
EnableGlobalHotkey bool `json:"enableGlobalHotkey"` // 是否启用全局热键
|
||||
GlobalHotkey HotkeyCombo `json:"globalHotkey"` // 全局热键组合
|
||||
@@ -103,7 +107,6 @@ type EditingConfig struct {
|
||||
type AppearanceConfig struct {
|
||||
Language LanguageType `json:"language"` // 界面语言
|
||||
SystemTheme SystemThemeType `json:"systemTheme"` // 系统界面主题
|
||||
CustomTheme CustomThemeConfig `json:"customTheme"` // 自定义主题配置
|
||||
}
|
||||
|
||||
// UpdatesConfig 更新设置配置
|
||||
@@ -124,6 +127,7 @@ type AppConfig struct {
|
||||
Editing EditingConfig `json:"editing"` // 编辑设置
|
||||
Appearance AppearanceConfig `json:"appearance"` // 外观设置
|
||||
Updates UpdatesConfig `json:"updates"` // 更新设置
|
||||
Backup GitBackupConfig `json:"backup"` // Git备份设置
|
||||
Metadata ConfigMetadata `json:"metadata"` // 配置元数据
|
||||
}
|
||||
|
||||
@@ -145,6 +149,8 @@ func NewDefaultAppConfig() *AppConfig {
|
||||
DataPath: dataDir,
|
||||
EnableSystemTray: true,
|
||||
StartAtLogin: false,
|
||||
EnableWindowSnap: true, // 默认启用窗口吸附
|
||||
SnapThreshold: 15, // 默认15像素的吸附阈值
|
||||
EnableGlobalHotkey: false,
|
||||
GlobalHotkey: HotkeyCombo{
|
||||
Ctrl: false,
|
||||
@@ -170,13 +176,12 @@ func NewDefaultAppConfig() *AppConfig {
|
||||
Appearance: AppearanceConfig{
|
||||
Language: LangEnUS,
|
||||
SystemTheme: SystemThemeAuto,
|
||||
CustomTheme: *NewDefaultCustomThemeConfig(),
|
||||
},
|
||||
Updates: UpdatesConfig{
|
||||
Version: "1.0.0",
|
||||
Version: "1.3.0",
|
||||
AutoUpdate: true,
|
||||
PrimarySource: UpdateSourceGithub,
|
||||
BackupSource: UpdateSourceGitea,
|
||||
PrimarySource: UpdateSourceGitea,
|
||||
BackupSource: UpdateSourceGithub,
|
||||
BackupBeforeUpdate: true,
|
||||
UpdateTimeout: 30,
|
||||
Github: GithubConfig{
|
||||
@@ -189,9 +194,20 @@ func NewDefaultAppConfig() *AppConfig {
|
||||
Repo: "voidraft",
|
||||
},
|
||||
},
|
||||
Backup: GitBackupConfig{
|
||||
Enabled: false,
|
||||
RepoURL: "",
|
||||
AuthMethod: UserPass,
|
||||
Username: "",
|
||||
Password: "",
|
||||
Token: "",
|
||||
SSHKeyPath: "",
|
||||
BackupInterval: 60,
|
||||
AutoBackup: false,
|
||||
},
|
||||
Metadata: ConfigMetadata{
|
||||
LastUpdated: time.Now().Format(time.RFC3339),
|
||||
Version: "1.0.0",
|
||||
Version: "1.2.0",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,11 @@ type Document struct {
|
||||
Content string `json:"content" db:"content"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
IsDeleted bool `json:"is_deleted" db:"is_deleted"`
|
||||
IsLocked bool `json:"is_locked" db:"is_locked"` // 锁定标志,锁定的文档无法被删除
|
||||
}
|
||||
|
||||
// NewDocument 创建新文档(不需要传ID,由数据库自增)
|
||||
// NewDocument 创建新文档
|
||||
func NewDocument(title, content string) *Document {
|
||||
now := time.Now()
|
||||
return &Document{
|
||||
@@ -23,6 +24,7 @@ func NewDocument(title, content string) *Document {
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
IsDeleted: false,
|
||||
IsLocked: false, // 默认不锁定
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import "time"
|
||||
|
||||
// Extension 单个扩展配置
|
||||
type Extension struct {
|
||||
ID ExtensionID `json:"id"` // 扩展唯一标识
|
||||
Enabled bool `json:"enabled"` // 是否启用
|
||||
IsDefault bool `json:"isDefault"` // 是否为默认扩展
|
||||
Config ExtensionConfig `json:"config"` // 扩展配置项
|
||||
ID ExtensionID `json:"id" db:"id"` // 扩展唯一标识
|
||||
Enabled bool `json:"enabled" db:"enabled"` // 是否启用
|
||||
IsDefault bool `json:"isDefault" db:"is_default"` // 是否为默认扩展
|
||||
Config ExtensionConfig `json:"config" db:"config"` // 扩展配置项
|
||||
}
|
||||
|
||||
// ExtensionID 扩展标识符
|
||||
|
||||
@@ -4,11 +4,11 @@ import "time"
|
||||
|
||||
// KeyBinding 单个快捷键绑定
|
||||
type KeyBinding struct {
|
||||
Command KeyBindingCommand `json:"command"` // 快捷键动作
|
||||
Extension ExtensionID `json:"extension"` // 所属扩展
|
||||
Key string `json:"key"` // 快捷键组合(如 "Mod-f", "Ctrl-Shift-p")
|
||||
Enabled bool `json:"enabled"` // 是否启用
|
||||
IsDefault bool `json:"isDefault"` // 是否为默认快捷键
|
||||
Command KeyBindingCommand `json:"command" db:"command"` // 快捷键动作
|
||||
Extension ExtensionID `json:"extension" db:"extension"` // 所属扩展
|
||||
Key string `json:"key" db:"key"` // 快捷键组合(如 "Mod-f", "Ctrl-Shift-p")
|
||||
Enabled bool `json:"enabled" db:"enabled"` // 是否启用
|
||||
IsDefault bool `json:"isDefault" db:"is_default"` // 是否为默认快捷键
|
||||
}
|
||||
|
||||
// KeyBindingCommand 快捷键命令
|
||||
|
||||
+60
-24
@@ -1,5 +1,20 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ThemeType 主题类型枚举
|
||||
type ThemeType string
|
||||
|
||||
const (
|
||||
ThemeTypeDark ThemeType = "dark"
|
||||
ThemeTypeLight ThemeType = "light"
|
||||
)
|
||||
|
||||
// ThemeColorConfig 主题颜色配置
|
||||
type ThemeColorConfig struct {
|
||||
// 基础色调
|
||||
@@ -36,15 +51,44 @@ type ThemeColorConfig struct {
|
||||
MatchingBracket string `json:"matchingBracket"` // 匹配括号
|
||||
}
|
||||
|
||||
// CustomThemeConfig 自定义主题配置
|
||||
type CustomThemeConfig struct {
|
||||
DarkTheme ThemeColorConfig `json:"darkTheme"` // 深色主题配置
|
||||
LightTheme ThemeColorConfig `json:"lightTheme"` // 浅色主题配置
|
||||
// Theme 主题数据库模型
|
||||
type Theme struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Type ThemeType `db:"type" json:"type"`
|
||||
Colors ThemeColorConfig `db:"colors" json:"colors"`
|
||||
IsDefault bool `db:"is_default" json:"isDefault"`
|
||||
CreatedAt time.Time `db:"created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updatedAt"`
|
||||
}
|
||||
|
||||
// Value 实现 driver.Valuer 接口,用于将 ThemeColorConfig 存储到数据库
|
||||
func (tc ThemeColorConfig) Value() (driver.Value, error) {
|
||||
return json.Marshal(tc)
|
||||
}
|
||||
|
||||
// Scan 实现 sql.Scanner 接口,用于从数据库读取 ThemeColorConfig
|
||||
func (tc *ThemeColorConfig) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var bytes []byte
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
bytes = v
|
||||
case string:
|
||||
bytes = []byte(v)
|
||||
default:
|
||||
return fmt.Errorf("cannot scan %T into ThemeColorConfig", value)
|
||||
}
|
||||
|
||||
return json.Unmarshal(bytes, tc)
|
||||
}
|
||||
|
||||
// NewDefaultDarkTheme 创建默认深色主题配置
|
||||
func NewDefaultDarkTheme() ThemeColorConfig {
|
||||
return ThemeColorConfig{
|
||||
func NewDefaultDarkTheme() *ThemeColorConfig {
|
||||
return &ThemeColorConfig{
|
||||
// 基础色调
|
||||
Background: "#252B37",
|
||||
BackgroundSecondary: "#213644",
|
||||
@@ -66,9 +110,9 @@ func NewDefaultDarkTheme() ThemeColorConfig {
|
||||
Cursor: "#ffffff",
|
||||
Selection: "#0865a9",
|
||||
SelectionBlur: "#225377",
|
||||
ActiveLine: "#ffffff",
|
||||
LineNumber: "#ffffff",
|
||||
ActiveLineNumber: "#ffffff",
|
||||
ActiveLine: "#ffffff0a",
|
||||
LineNumber: "#ffffff26",
|
||||
ActiveLineNumber: "#ffffff99",
|
||||
|
||||
// 边框分割线
|
||||
BorderColor: "#1e222a",
|
||||
@@ -81,8 +125,8 @@ func NewDefaultDarkTheme() ThemeColorConfig {
|
||||
}
|
||||
|
||||
// NewDefaultLightTheme 创建默认浅色主题配置
|
||||
func NewDefaultLightTheme() ThemeColorConfig {
|
||||
return ThemeColorConfig{
|
||||
func NewDefaultLightTheme() *ThemeColorConfig {
|
||||
return &ThemeColorConfig{
|
||||
// 基础色调
|
||||
Background: "#ffffff",
|
||||
BackgroundSecondary: "#f1faf1",
|
||||
@@ -104,24 +148,16 @@ func NewDefaultLightTheme() ThemeColorConfig {
|
||||
Cursor: "#000000",
|
||||
Selection: "#77baff",
|
||||
SelectionBlur: "#b2c2ca",
|
||||
ActiveLine: "#000000",
|
||||
LineNumber: "#000000",
|
||||
ActiveLineNumber: "#000000",
|
||||
ActiveLine: "#0000000a",
|
||||
LineNumber: "#00000040",
|
||||
ActiveLineNumber: "#000000aa",
|
||||
|
||||
// 边框分割线
|
||||
BorderColor: "#dfdfdf",
|
||||
BorderLight: "#0000000d",
|
||||
BorderLight: "#0000000c",
|
||||
|
||||
// 搜索匹配
|
||||
SearchMatch: "#005cc5",
|
||||
MatchingBracket: "#0000001a",
|
||||
}
|
||||
}
|
||||
|
||||
// NewDefaultCustomThemeConfig 创建默认自定义主题配置
|
||||
func NewDefaultCustomThemeConfig() *CustomThemeConfig {
|
||||
return &CustomThemeConfig{
|
||||
DarkTheme: NewDefaultDarkTheme(),
|
||||
LightTheme: NewDefaultLightTheme(),
|
||||
MatchingBracket: "#00000019",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// SnapEdge 表示吸附的边缘类型
|
||||
type SnapEdge int
|
||||
|
||||
const (
|
||||
SnapEdgeNone SnapEdge = iota // 未吸附
|
||||
SnapEdgeTop // 吸附到上边缘
|
||||
SnapEdgeRight // 吸附到右边缘
|
||||
SnapEdgeBottom // 吸附到下边缘
|
||||
SnapEdgeLeft // 吸附到左边缘
|
||||
SnapEdgeTopRight // 吸附到右上角
|
||||
SnapEdgeBottomRight // 吸附到右下角
|
||||
SnapEdgeBottomLeft // 吸附到左下角
|
||||
SnapEdgeTopLeft // 吸附到左上角
|
||||
)
|
||||
|
||||
// WindowPosition 窗口位置
|
||||
type WindowPosition struct {
|
||||
X int `json:"x"` // X坐标
|
||||
Y int `json:"y"` // Y坐标
|
||||
}
|
||||
|
||||
// SnapPosition 表示吸附的相对位置
|
||||
type SnapPosition struct {
|
||||
X int `json:"x"` // X轴相对偏移
|
||||
Y int `json:"y"` // Y轴相对偏移
|
||||
}
|
||||
|
||||
// WindowInfo 窗口信息
|
||||
type WindowInfo struct {
|
||||
DocumentID int64 `json:"documentID"` // 文档ID
|
||||
Title string `json:"title"` // 窗口标题
|
||||
IsSnapped bool `json:"isSnapped"` // 是否处于吸附状态
|
||||
SnapOffset SnapPosition `json:"snapOffset"` // 与主窗口的相对位置偏移
|
||||
SnapEdge SnapEdge `json:"snapEdge"` // 吸附的边缘类型
|
||||
LastPos WindowPosition `json:"lastPos"` // 上一次记录的窗口位置
|
||||
MoveTime time.Time `json:"moveTime"` // 上次移动时间,用于判断移动速度
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
gitConfig "github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
|
||||
"voidraft/internal/models"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
const (
|
||||
dbSerializeFile = "voidraft_data.bin"
|
||||
)
|
||||
|
||||
// BackupService 提供基于Git的备份功能
|
||||
type BackupService struct {
|
||||
configService *ConfigService
|
||||
dbService *DatabaseService
|
||||
repository *git.Repository
|
||||
logger *log.LogService
|
||||
isInitialized bool
|
||||
autoBackupTicker *time.Ticker
|
||||
autoBackupStop chan bool
|
||||
}
|
||||
|
||||
// NewBackupService 创建新的备份服务实例
|
||||
func NewBackupService(configService *ConfigService, dbService *DatabaseService, logger *log.LogService) *BackupService {
|
||||
return &BackupService{
|
||||
configService: configService,
|
||||
dbService: dbService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize 初始化备份服务
|
||||
func (s *BackupService) Initialize() error {
|
||||
config, repoPath, err := s.getConfigAndPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting backup config: %w", err)
|
||||
}
|
||||
|
||||
if !config.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 初始化仓库
|
||||
if err := s.initializeRepository(config, repoPath); err != nil {
|
||||
return fmt.Errorf("initializing repository: %w", err)
|
||||
}
|
||||
|
||||
// 启动自动备份
|
||||
if config.AutoBackup && config.BackupInterval > 0 {
|
||||
s.StartAutoBackup()
|
||||
}
|
||||
|
||||
s.isInitialized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// getConfigAndPath 获取备份配置和仓库路径
|
||||
func (s *BackupService) getConfigAndPath() (*models.GitBackupConfig, string, error) {
|
||||
appConfig, err := s.configService.GetConfig()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("getting app config: %w", err)
|
||||
}
|
||||
return &appConfig.Backup, appConfig.General.DataPath, nil
|
||||
}
|
||||
|
||||
// initializeRepository 初始化或打开Git仓库并设置远程
|
||||
func (s *BackupService) initializeRepository(config *models.GitBackupConfig, repoPath string) error {
|
||||
|
||||
// 检查本地仓库是否存在
|
||||
_, err := os.Stat(filepath.Join(repoPath, ".git"))
|
||||
if os.IsNotExist(err) {
|
||||
// 仓库不存在,初始化新仓库
|
||||
repo, err := git.PlainInit(repoPath, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error initializing repository: %w", err)
|
||||
}
|
||||
s.repository = repo
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error checking repository path: %w", err)
|
||||
} else {
|
||||
// 仓库已存在,打开现有仓库
|
||||
repo, err := git.PlainOpen(repoPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening local repository: %w", err)
|
||||
}
|
||||
s.repository = repo
|
||||
}
|
||||
|
||||
// 设置或更新远程仓库
|
||||
remote, err := s.repository.Remote("origin")
|
||||
if err != nil {
|
||||
if errors.Is(err, git.ErrRemoteNotFound) {
|
||||
// 远程不存在,添加远程
|
||||
_, err = s.repository.CreateRemote(&gitConfig.RemoteConfig{
|
||||
Name: "origin",
|
||||
URLs: []string{config.RepoURL},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating remote: %w", err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("error getting remote: %w", err)
|
||||
}
|
||||
} else {
|
||||
// 检查远程URL是否一致,如果不一致则更新
|
||||
if len(remote.Config().URLs) > 0 && remote.Config().URLs[0] != config.RepoURL {
|
||||
if err := s.repository.DeleteRemote("origin"); err != nil {
|
||||
return fmt.Errorf("error deleting remote: %w", err)
|
||||
}
|
||||
_, err = s.repository.CreateRemote(&gitConfig.RemoteConfig{
|
||||
Name: "origin",
|
||||
URLs: []string{config.RepoURL},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating new remote: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAuthMethod 根据配置获取认证方法
|
||||
func (s *BackupService) getAuthMethod(config *models.GitBackupConfig) (transport.AuthMethod, error) {
|
||||
switch config.AuthMethod {
|
||||
case models.Token:
|
||||
if config.Token == "" {
|
||||
return nil, errors.New("token authentication requires a valid token")
|
||||
}
|
||||
return &http.BasicAuth{
|
||||
Username: "git", // 使用token时,用户名可以是任意值
|
||||
Password: config.Token,
|
||||
}, nil
|
||||
|
||||
case models.UserPass:
|
||||
if config.Username == "" || config.Password == "" {
|
||||
return nil, errors.New("username/password authentication requires both username and password")
|
||||
}
|
||||
return &http.BasicAuth{
|
||||
Username: config.Username,
|
||||
Password: config.Password,
|
||||
}, nil
|
||||
|
||||
case models.SSHKey:
|
||||
if config.SSHKeyPath == "" {
|
||||
return nil, errors.New("SSH key authentication requires a valid SSH key path")
|
||||
}
|
||||
publicKeys, err := ssh.NewPublicKeysFromFile("git", config.SSHKeyPath, config.SSHKeyPass)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating SSH public keys: %w", err)
|
||||
}
|
||||
return publicKeys, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported authentication method: %s", config.AuthMethod)
|
||||
}
|
||||
}
|
||||
|
||||
// serializeDatabase 序列化数据库到文件
|
||||
func (s *BackupService) serializeDatabase(repoPath string) error {
|
||||
if s.dbService == nil || s.dbService.db == nil {
|
||||
return errors.New("database service not available")
|
||||
}
|
||||
|
||||
// 获取数据库路径
|
||||
dbPath, err := s.dbService.getDatabasePath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting database path: %w", err)
|
||||
}
|
||||
|
||||
// 关闭数据库连接以确保所有更改都写入磁盘
|
||||
if err := s.dbService.ServiceShutdown(); err != nil {
|
||||
s.logger.Error("Failed to close database connection", "error", err)
|
||||
}
|
||||
|
||||
// 直接复制数据库文件到序列化文件
|
||||
dbData, err := os.ReadFile(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading database file: %w", err)
|
||||
}
|
||||
|
||||
binFilePath := filepath.Join(repoPath, dbSerializeFile)
|
||||
if err := os.WriteFile(binFilePath, dbData, 0644); err != nil {
|
||||
return fmt.Errorf("writing serialized database to file: %w", err)
|
||||
}
|
||||
|
||||
// 重新初始化数据库服务
|
||||
if err := s.dbService.initDatabase(); err != nil {
|
||||
return fmt.Errorf("reinitializing database: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PushToRemote 推送本地更改到远程仓库
|
||||
func (s *BackupService) PushToRemote() error {
|
||||
if !s.isInitialized {
|
||||
return errors.New("backup service not initialized")
|
||||
}
|
||||
|
||||
config, repoPath, err := s.getConfigAndPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting backup config: %w", err)
|
||||
}
|
||||
|
||||
if !config.Enabled {
|
||||
return errors.New("backup is disabled")
|
||||
}
|
||||
|
||||
// 数据库序列化文件的路径
|
||||
binFilePath := filepath.Join(repoPath, dbSerializeFile)
|
||||
|
||||
// 函数返回前都删除临时文件
|
||||
defer func() {
|
||||
if _, err := os.Stat(binFilePath); err == nil {
|
||||
os.Remove(binFilePath)
|
||||
}
|
||||
}()
|
||||
|
||||
// 序列化数据库
|
||||
if err := s.serializeDatabase(repoPath); err != nil {
|
||||
return fmt.Errorf("serializing database: %w", err)
|
||||
}
|
||||
|
||||
// 获取工作树
|
||||
w, err := s.repository.Worktree()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting worktree: %w", err)
|
||||
}
|
||||
|
||||
// 添加序列化的数据库文件
|
||||
if _, err := w.Add(dbSerializeFile); err != nil {
|
||||
return fmt.Errorf("adding serialized database file: %w", err)
|
||||
}
|
||||
|
||||
// 检查是否有变化需要提交
|
||||
status, err := w.Status()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting worktree status: %w", err)
|
||||
}
|
||||
|
||||
// 如果没有变化,直接返回
|
||||
if status.IsClean() {
|
||||
return errors.New("no changes to backup")
|
||||
}
|
||||
|
||||
// 创建提交
|
||||
_, err = w.Commit(fmt.Sprintf("Backup %s", time.Now().Format("2006-01-02 15:04:05")), &git.CommitOptions{
|
||||
Author: &object.Signature{
|
||||
Name: "VoidRaft",
|
||||
Email: "backup@voidraft.app",
|
||||
When: time.Now(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "cannot create empty commit") {
|
||||
return errors.New("no changes to backup")
|
||||
}
|
||||
return fmt.Errorf("creating commit: %w", err)
|
||||
}
|
||||
|
||||
// 获取认证方法并推送到远程
|
||||
auth, err := s.getAuthMethod(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting auth method: %w", err)
|
||||
}
|
||||
|
||||
// 推送到远程仓库
|
||||
if err := s.repository.Push(&git.PushOptions{
|
||||
RemoteName: "origin",
|
||||
Auth: auth,
|
||||
}); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
|
||||
// 忽略一些常见的非错误情况
|
||||
if strings.Contains(err.Error(), "clean working tree") ||
|
||||
strings.Contains(err.Error(), "already up-to-date") ||
|
||||
strings.Contains(err.Error(), " clean working tree") ||
|
||||
strings.Contains(err.Error(), "reference not found") {
|
||||
// 更新最后推送时间
|
||||
return errors.New("no changes to backup")
|
||||
}
|
||||
return fmt.Errorf("push failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartAutoBackup 启动自动备份定时器
|
||||
func (s *BackupService) StartAutoBackup() error {
|
||||
config, _, err := s.getConfigAndPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting backup config: %w", err)
|
||||
}
|
||||
|
||||
if !config.AutoBackup || config.BackupInterval <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.StopAutoBackup()
|
||||
|
||||
// 将秒转换为分钟
|
||||
s.autoBackupTicker = time.NewTicker(time.Duration(config.BackupInterval) * time.Minute)
|
||||
s.autoBackupStop = make(chan bool)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-s.autoBackupTicker.C:
|
||||
// 执行推送操作
|
||||
if err := s.PushToRemote(); err != nil {
|
||||
s.logger.Error("Auto backup failed", "error", err)
|
||||
}
|
||||
case <-s.autoBackupStop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopAutoBackup 停止自动备份
|
||||
func (s *BackupService) StopAutoBackup() {
|
||||
if s.autoBackupTicker != nil {
|
||||
s.autoBackupTicker.Stop()
|
||||
s.autoBackupTicker = nil
|
||||
}
|
||||
|
||||
if s.autoBackupStop != nil {
|
||||
close(s.autoBackupStop)
|
||||
s.autoBackupStop = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Reinitialize 重新初始化备份服务,用于响应配置变更
|
||||
func (s *BackupService) Reinitialize() error {
|
||||
// 停止自动备份
|
||||
s.StopAutoBackup()
|
||||
|
||||
// 重新设置标志
|
||||
s.isInitialized = false
|
||||
|
||||
// 重新初始化
|
||||
return s.Initialize()
|
||||
}
|
||||
|
||||
// HandleConfigChange 处理备份配置变更
|
||||
func (s *BackupService) HandleConfigChange(config *models.GitBackupConfig) error {
|
||||
|
||||
// 如果备份功能禁用,只需停止自动备份
|
||||
if !config.Enabled {
|
||||
s.StopAutoBackup()
|
||||
s.isInitialized = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果服务已初始化,重新初始化以应用新配置
|
||||
if s.isInitialized {
|
||||
return s.Reinitialize()
|
||||
}
|
||||
|
||||
// 如果服务未初始化但已启用,则初始化
|
||||
if config.Enabled && !s.isInitialized {
|
||||
return s.Initialize()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServiceShutdown 服务关闭时的清理工作
|
||||
func (s *BackupService) ServiceShutdown() {
|
||||
s.StopAutoBackup()
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
const (
|
||||
// CurrentAppConfigVersion 当前应用配置版本
|
||||
CurrentAppConfigVersion = "1.2.0"
|
||||
CurrentAppConfigVersion = "1.3.0"
|
||||
// BackupFilePattern 备份文件名模式
|
||||
BackupFilePattern = "%s.backup.%s.json"
|
||||
|
||||
@@ -38,7 +38,7 @@ type Migratable interface {
|
||||
|
||||
// ConfigMigrationService 配置迁移服务
|
||||
type ConfigMigrationService[T Migratable] struct {
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
configDir string
|
||||
configName string
|
||||
targetVersion string
|
||||
@@ -54,7 +54,7 @@ type MigrationResult struct {
|
||||
|
||||
// NewConfigMigrationService 创建配置迁移服务
|
||||
func NewConfigMigrationService[T Migratable](
|
||||
logger *log.Service,
|
||||
logger *log.LogService,
|
||||
configDir string,
|
||||
configName, targetVersion, configPath string,
|
||||
) *ConfigMigrationService[T] {
|
||||
@@ -312,7 +312,7 @@ func chainLoad(k *koanf.Koanf, loaders ...func() error) error {
|
||||
}
|
||||
|
||||
// 工厂函数
|
||||
func NewAppConfigMigrationService(logger *log.Service, configDir, settingsPath string) *ConfigMigrationService[*models.AppConfig] {
|
||||
func NewAppConfigMigrationService(logger *log.LogService, configDir, settingsPath string) *ConfigMigrationService[*models.AppConfig] {
|
||||
return NewConfigMigrationService[*models.AppConfig](
|
||||
logger, configDir, "settings", CurrentAppConfigVersion, settingsPath)
|
||||
}
|
||||
@@ -22,6 +22,10 @@ const (
|
||||
ConfigChangeTypeHotkey ConfigChangeType = "hotkey"
|
||||
// ConfigChangeTypeDataPath 数据路径配置变更
|
||||
ConfigChangeTypeDataPath ConfigChangeType = "datapath"
|
||||
// ConfigChangeTypeBackup 备份配置变更
|
||||
ConfigChangeTypeBackup ConfigChangeType = "backup"
|
||||
// ConfigChangeTypeWindowSnap 窗口吸附配置变更
|
||||
ConfigChangeTypeWindowSnap ConfigChangeType = "windowsnap"
|
||||
)
|
||||
|
||||
// ConfigChangeCallback 配置变更回调函数类型
|
||||
@@ -49,7 +53,7 @@ type ConfigListener struct {
|
||||
type ConfigNotificationService struct {
|
||||
listeners map[ConfigChangeType][]*ConfigListener // 支持多监听器的map
|
||||
mu sync.RWMutex // 监听器map的读写锁
|
||||
logger *log.Service // 日志服务
|
||||
logger *log.LogService // 日志服务
|
||||
koanf *koanf.Koanf // koanf实例
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
@@ -57,7 +61,7 @@ type ConfigNotificationService struct {
|
||||
}
|
||||
|
||||
// NewConfigNotificationService 创建配置通知服务
|
||||
func NewConfigNotificationService(k *koanf.Koanf, logger *log.Service) *ConfigNotificationService {
|
||||
func NewConfigNotificationService(k *koanf.Koanf, logger *log.LogService) *ConfigNotificationService {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &ConfigNotificationService{
|
||||
listeners: make(map[ConfigChangeType][]*ConfigListener),
|
||||
@@ -445,6 +449,52 @@ func CreateDataPathListener(name string, callback func() error) *ConfigListener
|
||||
}
|
||||
}
|
||||
|
||||
// CreateBackupConfigListener 创建备份配置监听器
|
||||
func CreateBackupConfigListener(name string, callback func(config *models.GitBackupConfig) error) *ConfigListener {
|
||||
return &ConfigListener{
|
||||
Name: name,
|
||||
ChangeType: ConfigChangeTypeBackup,
|
||||
Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
|
||||
if newConfig == nil {
|
||||
defaultConfig := models.NewDefaultAppConfig()
|
||||
return callback(&defaultConfig.Backup)
|
||||
}
|
||||
return callback(&newConfig.Backup)
|
||||
},
|
||||
DebounceDelay: 200 * time.Millisecond,
|
||||
GetConfigFunc: func(k *koanf.Koanf) *models.AppConfig {
|
||||
var config models.AppConfig
|
||||
if err := k.Unmarshal("", &config); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &config
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWindowSnapConfigListener 创建窗口吸附配置监听器
|
||||
func CreateWindowSnapConfigListener(name string, callback func(enabled bool, threshold int) error) *ConfigListener {
|
||||
return &ConfigListener{
|
||||
Name: name,
|
||||
ChangeType: ConfigChangeTypeWindowSnap,
|
||||
Callback: func(changeType ConfigChangeType, oldConfig, newConfig *models.AppConfig) error {
|
||||
if newConfig == nil {
|
||||
defaultConfig := models.NewDefaultAppConfig()
|
||||
return callback(defaultConfig.General.EnableWindowSnap, defaultConfig.General.SnapThreshold)
|
||||
}
|
||||
return callback(newConfig.General.EnableWindowSnap, newConfig.General.SnapThreshold)
|
||||
},
|
||||
DebounceDelay: 200 * time.Millisecond,
|
||||
GetConfigFunc: func(k *koanf.Koanf) *models.AppConfig {
|
||||
var config models.AppConfig
|
||||
if err := k.Unmarshal("", &config); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &config
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceShutdown 关闭服务
|
||||
func (cns *ConfigNotificationService) ServiceShutdown() error {
|
||||
cns.Cleanup()
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
// ConfigService 应用配置服务
|
||||
type ConfigService struct {
|
||||
koanf *koanf.Koanf // koanf 实例
|
||||
logger *log.Service // 日志服务
|
||||
logger *log.LogService // 日志服务
|
||||
configDir string // 配置目录
|
||||
settingsPath string // 设置文件路径
|
||||
mu sync.RWMutex // 读写锁
|
||||
@@ -55,7 +55,7 @@ func (e *ConfigError) Is(target error) bool {
|
||||
}
|
||||
|
||||
// NewConfigService 创建新的配置服务实例
|
||||
func NewConfigService(logger *log.Service) *ConfigService {
|
||||
func NewConfigService(logger *log.LogService) *ConfigService {
|
||||
// 获取用户主目录
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
@@ -298,6 +298,26 @@ func (cs *ConfigService) SetDataPathChangeCallback(callback func() error) error
|
||||
return cs.notificationService.RegisterListener(dataPathListener)
|
||||
}
|
||||
|
||||
// SetBackupConfigChangeCallback 设置备份配置变更回调
|
||||
func (cs *ConfigService) SetBackupConfigChangeCallback(callback func(config *models.GitBackupConfig) error) error {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
// 创建备份配置监听器并注册
|
||||
backupListener := CreateBackupConfigListener("DefaultBackupConfigListener", callback)
|
||||
return cs.notificationService.RegisterListener(backupListener)
|
||||
}
|
||||
|
||||
// SetWindowSnapConfigChangeCallback 设置窗口吸附配置变更回调
|
||||
func (cs *ConfigService) SetWindowSnapConfigChangeCallback(callback func(enabled bool, threshold int) error) error {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
// 创建窗口吸附配置监听器并注册
|
||||
windowSnapListener := CreateWindowSnapConfigListener("DefaultWindowSnapConfigListener", callback)
|
||||
return cs.notificationService.RegisterListener(windowSnapListener)
|
||||
}
|
||||
|
||||
// ServiceShutdown 关闭服务
|
||||
func (cs *ConfigService) ServiceShutdown() error {
|
||||
cs.stopWatching()
|
||||
|
||||
@@ -2,14 +2,18 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
"voidraft/internal/models"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/sqlite"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -31,7 +35,8 @@ CREATE TABLE IF NOT EXISTS documents (
|
||||
content TEXT DEFAULT '∞∞∞text-a',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
is_deleted INTEGER DEFAULT 0
|
||||
is_deleted INTEGER DEFAULT 0,
|
||||
is_locked INTEGER DEFAULT 0
|
||||
)`
|
||||
|
||||
// Extensions table
|
||||
@@ -58,28 +63,70 @@ CREATE TABLE IF NOT EXISTS key_bindings (
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(command, extension)
|
||||
)`
|
||||
|
||||
// Themes table
|
||||
sqlCreateThemesTable = `
|
||||
CREATE TABLE IF NOT EXISTS themes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
colors TEXT NOT NULL,
|
||||
is_default INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(type, is_default)
|
||||
)`
|
||||
)
|
||||
|
||||
// ColumnInfo 存储列的信息
|
||||
type ColumnInfo struct {
|
||||
SQLType string
|
||||
DefaultValue string
|
||||
}
|
||||
|
||||
// TableModel 表示表与模型之间的映射关系
|
||||
type TableModel struct {
|
||||
TableName string
|
||||
Model interface{}
|
||||
}
|
||||
|
||||
// DatabaseService provides shared database functionality
|
||||
type DatabaseService struct {
|
||||
configService *ConfigService
|
||||
logger *log.Service
|
||||
SQLite *sqlite.Service
|
||||
logger *log.LogService
|
||||
db *sql.DB
|
||||
mu sync.RWMutex
|
||||
ctx context.Context
|
||||
tableModels []TableModel // 注册的表模型
|
||||
}
|
||||
|
||||
// NewDatabaseService creates a new database service
|
||||
func NewDatabaseService(configService *ConfigService, logger *log.Service) *DatabaseService {
|
||||
func NewDatabaseService(configService *ConfigService, logger *log.LogService) *DatabaseService {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
}
|
||||
|
||||
return &DatabaseService{
|
||||
ds := &DatabaseService{
|
||||
configService: configService,
|
||||
logger: logger,
|
||||
SQLite: sqlite.New(),
|
||||
}
|
||||
|
||||
// 注册所有模型
|
||||
ds.registerAllModels()
|
||||
|
||||
return ds
|
||||
}
|
||||
|
||||
// registerAllModels 注册所有数据模型,集中管理表-模型映射
|
||||
func (ds *DatabaseService) registerAllModels() {
|
||||
// 文档表
|
||||
ds.RegisterModel("documents", &models.Document{})
|
||||
// 扩展表
|
||||
ds.RegisterModel("extensions", &models.Extension{})
|
||||
// 快捷键表
|
||||
ds.RegisterModel("key_bindings", &models.KeyBinding{})
|
||||
// 主题表
|
||||
ds.RegisterModel("themes", &models.Theme{})
|
||||
}
|
||||
|
||||
// ServiceStartup initializes the service when the application starts
|
||||
@@ -101,28 +148,19 @@ func (ds *DatabaseService) initDatabase() error {
|
||||
return fmt.Errorf("failed to create database directory: %w", err)
|
||||
}
|
||||
|
||||
// 检查数据库文件是否存在,如果不存在则创建
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
// 创建空文件
|
||||
file, err := os.Create(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create database file: %w", err)
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
|
||||
// 配置SQLite服务
|
||||
ds.SQLite.Configure(&sqlite.Config{
|
||||
DBSource: dbPath,
|
||||
})
|
||||
|
||||
// 打开数据库连接
|
||||
if err := ds.SQLite.Open(); err != nil {
|
||||
ds.db, err = sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
// 测试连接
|
||||
if err := ds.db.Ping(); err != nil {
|
||||
return fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
// 应用性能优化设置
|
||||
if err := ds.SQLite.Execute(sqlOptimizationSettings); err != nil {
|
||||
if _, err := ds.db.Exec(sqlOptimizationSettings); err != nil {
|
||||
return fmt.Errorf("failed to apply optimization settings: %w", err)
|
||||
}
|
||||
|
||||
@@ -135,6 +173,11 @@ func (ds *DatabaseService) initDatabase() error {
|
||||
return fmt.Errorf("failed to create indexes: %w", err)
|
||||
}
|
||||
|
||||
// 执行模型与表结构同步
|
||||
if err := ds.syncAllModelTables(); err != nil {
|
||||
return fmt.Errorf("failed to sync model tables: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -153,10 +196,11 @@ func (ds *DatabaseService) createTables() error {
|
||||
sqlCreateDocumentsTable,
|
||||
sqlCreateExtensionsTable,
|
||||
sqlCreateKeyBindingsTable,
|
||||
sqlCreateThemesTable,
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
if err := ds.SQLite.Execute(table); err != nil {
|
||||
if _, err := ds.db.Exec(table); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -176,27 +220,173 @@ func (ds *DatabaseService) createIndexes() error {
|
||||
`CREATE INDEX IF NOT EXISTS idx_key_bindings_command ON key_bindings(command)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_key_bindings_extension ON key_bindings(extension)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_key_bindings_enabled ON key_bindings(enabled)`,
|
||||
// Themes indexes
|
||||
`CREATE INDEX IF NOT EXISTS idx_themes_type ON themes(type)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_themes_is_default ON themes(is_default)`,
|
||||
}
|
||||
|
||||
for _, index := range indexes {
|
||||
if err := ds.SQLite.Execute(index); err != nil {
|
||||
if _, err := ds.db.Exec(index); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterModel 注册模型与表的映射关系
|
||||
func (ds *DatabaseService) RegisterModel(tableName string, model interface{}) {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
ds.tableModels = append(ds.tableModels, TableModel{
|
||||
TableName: tableName,
|
||||
Model: model,
|
||||
})
|
||||
}
|
||||
|
||||
// syncAllModelTables 同步所有注册的模型与表结构
|
||||
func (ds *DatabaseService) syncAllModelTables() error {
|
||||
for _, tm := range ds.tableModels {
|
||||
if err := ds.syncModelTable(tm.TableName, tm.Model); err != nil {
|
||||
return fmt.Errorf("failed to sync table %s: %w", tm.TableName, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncModelTable 同步模型与表结构
|
||||
func (ds *DatabaseService) syncModelTable(tableName string, model interface{}) error {
|
||||
// 获取表结构元数据
|
||||
columns, err := ds.getTableColumns(tableName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get table columns: %w", err)
|
||||
}
|
||||
|
||||
// 使用反射从模型中提取字段信息
|
||||
expectedColumns, err := ds.getModelColumns(model)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get model columns: %w", err)
|
||||
}
|
||||
|
||||
// 检查缺失的列并添加
|
||||
for colName, colInfo := range expectedColumns {
|
||||
if _, exists := columns[colName]; !exists {
|
||||
// 执行添加列的SQL
|
||||
alterSQL := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s DEFAULT %s",
|
||||
tableName, colName, colInfo.SQLType, colInfo.DefaultValue)
|
||||
if _, err := ds.db.Exec(alterSQL); err != nil {
|
||||
return fmt.Errorf("failed to add column %s: %w", colName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTableColumns 获取表的列信息
|
||||
func (ds *DatabaseService) getTableColumns(table string) (map[string]string, error) {
|
||||
query := fmt.Sprintf("PRAGMA table_info(%s)", table)
|
||||
rows, err := ds.db.Query(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
columns := make(map[string]string)
|
||||
for rows.Next() {
|
||||
var cid int
|
||||
var name, typeName string
|
||||
var notNull, pk int
|
||||
var dflt_value interface{}
|
||||
|
||||
if err := rows.Scan(&cid, &name, &typeName, ¬Null, &dflt_value, &pk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
columns[name] = typeName
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
// getModelColumns 从模型结构体中提取数据库列信息
|
||||
func (ds *DatabaseService) getModelColumns(model interface{}) (map[string]ColumnInfo, error) {
|
||||
columns := make(map[string]ColumnInfo)
|
||||
|
||||
// 使用反射获取结构体的类型信息
|
||||
t := reflect.TypeOf(model)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
if t.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("model must be a struct or a pointer to struct")
|
||||
}
|
||||
|
||||
// 遍历所有字段
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
|
||||
// 只处理有db标签的字段
|
||||
dbTag := field.Tag.Get("db")
|
||||
if dbTag == "" {
|
||||
// 如果没有db标签,跳过该字段
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取字段类型对应的SQL类型和默认值
|
||||
sqlType, defaultVal := getSQLTypeAndDefault(field.Type)
|
||||
|
||||
columns[dbTag] = ColumnInfo{
|
||||
SQLType: sqlType,
|
||||
DefaultValue: defaultVal,
|
||||
}
|
||||
}
|
||||
|
||||
return columns, nil
|
||||
}
|
||||
|
||||
// getSQLTypeAndDefault 根据Go类型获取对应的SQL类型和默认值
|
||||
func getSQLTypeAndDefault(t reflect.Type) (string, string) {
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
return "INTEGER", "0"
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return "INTEGER", "0"
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return "REAL", "0.0"
|
||||
case reflect.String:
|
||||
return "TEXT", "''"
|
||||
default:
|
||||
// 处理特殊类型
|
||||
if t == reflect.TypeOf(time.Time{}) {
|
||||
return "DATETIME", "CURRENT_TIMESTAMP"
|
||||
}
|
||||
return "TEXT", "NULL"
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceShutdown shuts down the service when the application closes
|
||||
func (ds *DatabaseService) ServiceShutdown() error {
|
||||
return ds.SQLite.Close()
|
||||
if ds.db != nil {
|
||||
return ds.db.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnDataPathChanged handles data path changes
|
||||
func (ds *DatabaseService) OnDataPathChanged() error {
|
||||
// 关闭当前连接
|
||||
if err := ds.SQLite.Close(); err != nil {
|
||||
if ds.db != nil {
|
||||
if err := ds.db.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 用新路径重新初始化
|
||||
return ds.initDatabase()
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
|
||||
// DialogService 对话框服务,处理文件选择等对话框操作
|
||||
type DialogService struct {
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
window *application.WebviewWindow // 绑定的窗口
|
||||
}
|
||||
|
||||
// NewDialogService 创建新的对话框服务实例
|
||||
func NewDialogService(logger *log.Service) *DialogService {
|
||||
func NewDialogService(logger *log.LogService) *DialogService {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
}
|
||||
@@ -50,7 +50,6 @@ func (ds *DialogService) SelectDirectory() (string, error) {
|
||||
|
||||
// 对话框文本配置
|
||||
Title: "Select Directory",
|
||||
Message: "Select the folder where you want to store your app data",
|
||||
ButtonText: "Select",
|
||||
|
||||
// 不设置过滤器,因为我们选择目录
|
||||
@@ -69,3 +68,44 @@ func (ds *DialogService) SelectDirectory() (string, error) {
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// SelectFile 打开文件选择对话框
|
||||
func (ds *DialogService) SelectFile() (string, error) {
|
||||
dialog := application.OpenFileDialog()
|
||||
dialog.SetOptions(&application.OpenFileDialogOptions{
|
||||
// 目录选择配置
|
||||
CanChooseDirectories: false, // 允许选择目录
|
||||
CanChooseFiles: true, // 不允许选择文件
|
||||
CanCreateDirectories: true, // 允许创建新目录
|
||||
AllowsMultipleSelection: false, // 单选模式
|
||||
|
||||
// 显示配置
|
||||
ShowHiddenFiles: true, // 不显示隐藏文件
|
||||
HideExtension: false, // 不隐藏扩展名
|
||||
CanSelectHiddenExtension: false, // 不允许选择隐藏扩展名
|
||||
TreatsFilePackagesAsDirectories: false, // 不将文件包当作目录处理
|
||||
AllowsOtherFileTypes: false, // 不允许其他文件类型
|
||||
|
||||
// 系统配置
|
||||
ResolvesAliases: true, // 解析别名/快捷方式
|
||||
|
||||
// 对话框文本配置
|
||||
Title: "Select File",
|
||||
ButtonText: "Select File",
|
||||
|
||||
// 不设置过滤器,因为我们选择目录
|
||||
Filters: nil,
|
||||
|
||||
// 不指定默认目录,让系统决定
|
||||
Directory: "",
|
||||
|
||||
// 绑定到主窗口
|
||||
Window: ds.window,
|
||||
})
|
||||
|
||||
path, err := dialog.PromptForSingleSelection()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
@@ -18,28 +18,28 @@ const (
|
||||
|
||||
// Document operations
|
||||
sqlGetDocumentByID = `
|
||||
SELECT id, title, content, created_at, updated_at, is_deleted
|
||||
SELECT id, title, content, created_at, updated_at, is_deleted, is_locked
|
||||
FROM documents
|
||||
WHERE id = ?`
|
||||
|
||||
sqlInsertDocument = `
|
||||
INSERT INTO documents (title, content, created_at, updated_at, is_deleted)
|
||||
VALUES (?, ?, ?, ?, 0)`
|
||||
INSERT INTO documents (title, content, created_at, updated_at, is_deleted, is_locked)
|
||||
VALUES (?, ?, ?, ?, 0, 0)`
|
||||
|
||||
sqlUpdateDocumentContent = `
|
||||
UPDATE documents
|
||||
SET content = ?, updated_at = ?
|
||||
WHERE id = ?`
|
||||
WHERE id = ? AND is_deleted = 0`
|
||||
|
||||
sqlUpdateDocumentTitle = `
|
||||
UPDATE documents
|
||||
SET title = ?, updated_at = ?
|
||||
WHERE id = ?`
|
||||
WHERE id = ? AND is_deleted = 0`
|
||||
|
||||
sqlMarkDocumentAsDeleted = `
|
||||
UPDATE documents
|
||||
SET is_deleted = 1, updated_at = ?
|
||||
WHERE id = ?`
|
||||
WHERE id = ? AND is_locked = 0`
|
||||
|
||||
sqlRestoreDocument = `
|
||||
UPDATE documents
|
||||
@@ -47,13 +47,13 @@ SET is_deleted = 0, updated_at = ?
|
||||
WHERE id = ?`
|
||||
|
||||
sqlListAllDocumentsMeta = `
|
||||
SELECT id, title, created_at, updated_at
|
||||
SELECT id, title, created_at, updated_at, is_locked
|
||||
FROM documents
|
||||
WHERE is_deleted = 0
|
||||
ORDER BY updated_at DESC`
|
||||
|
||||
sqlListDeletedDocumentsMeta = `
|
||||
SELECT id, title, created_at, updated_at
|
||||
SELECT id, title, created_at, updated_at, is_locked
|
||||
FROM documents
|
||||
WHERE is_deleted = 1
|
||||
ORDER BY updated_at DESC`
|
||||
@@ -63,33 +63,46 @@ SELECT id FROM documents WHERE is_deleted = 0 ORDER BY id LIMIT 1`
|
||||
|
||||
sqlCountDocuments = `SELECT COUNT(*) FROM documents WHERE is_deleted = 0`
|
||||
|
||||
sqlSetDocumentLocked = `
|
||||
UPDATE documents
|
||||
SET is_locked = 1, updated_at = ?
|
||||
WHERE id = ?`
|
||||
|
||||
sqlSetDocumentUnlocked = `
|
||||
UPDATE documents
|
||||
SET is_locked = 0, updated_at = ?
|
||||
WHERE id = ?`
|
||||
|
||||
sqlDefaultDocumentID = 1 // 默认文档的ID
|
||||
)
|
||||
|
||||
// DocumentService provides document management functionality
|
||||
type DocumentService struct {
|
||||
databaseService *DatabaseService
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
mu sync.RWMutex
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewDocumentService creates a new document service
|
||||
func NewDocumentService(databaseService *DatabaseService, logger *log.Service) *DocumentService {
|
||||
func NewDocumentService(databaseService *DatabaseService, logger *log.LogService) *DocumentService {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
}
|
||||
|
||||
return &DocumentService{
|
||||
ds := &DocumentService{
|
||||
databaseService: databaseService,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
return ds
|
||||
}
|
||||
|
||||
// ServiceStartup initializes the service when the application starts
|
||||
func (ds *DocumentService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||
ds.ctx = ctx
|
||||
// Ensure default document exists
|
||||
|
||||
// 确保默认文档存在
|
||||
if err := ds.ensureDefaultDocument(); err != nil {
|
||||
return fmt.Errorf("failed to ensure default document: %w", err)
|
||||
}
|
||||
@@ -98,19 +111,15 @@ func (ds *DocumentService) ServiceStartup(ctx context.Context, options applicati
|
||||
|
||||
// ensureDefaultDocument ensures a default document exists
|
||||
func (ds *DocumentService) ensureDefaultDocument() error {
|
||||
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||
return errors.New("database service not available")
|
||||
}
|
||||
|
||||
// Check if any document exists
|
||||
rows, err := ds.databaseService.SQLite.Query(sqlCountDocuments)
|
||||
var count int64
|
||||
err := ds.databaseService.db.QueryRow(sqlCountDocuments).Scan(&count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
return fmt.Errorf("failed to query document count")
|
||||
}
|
||||
|
||||
count, ok := rows[0]["COUNT(*)"].(int64)
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to convert count to int64")
|
||||
return fmt.Errorf("failed to query document count: %w", err)
|
||||
}
|
||||
|
||||
// If no documents exist, create default document
|
||||
@@ -127,48 +136,42 @@ func (ds *DocumentService) GetDocumentByID(id int64) (*models.Document, error) {
|
||||
ds.mu.RLock()
|
||||
defer ds.mu.RUnlock()
|
||||
|
||||
rows, err := ds.databaseService.SQLite.Query(sqlGetDocumentByID, id)
|
||||
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||
return nil, errors.New("database service not available")
|
||||
}
|
||||
|
||||
doc := &models.Document{}
|
||||
var createdAt, updatedAt string
|
||||
var isDeleted, isLocked int
|
||||
|
||||
err := ds.databaseService.db.QueryRow(sqlGetDocumentByID, id).Scan(
|
||||
&doc.ID,
|
||||
&doc.Title,
|
||||
&doc.Content,
|
||||
&createdAt,
|
||||
&updatedAt,
|
||||
&isDeleted,
|
||||
&isLocked,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get document by ID: %w", err)
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
row := rows[0]
|
||||
doc := &models.Document{}
|
||||
|
||||
// 从Row中提取数据
|
||||
if idVal, ok := row["id"].(int64); ok {
|
||||
doc.ID = idVal
|
||||
}
|
||||
|
||||
if title, ok := row["title"].(string); ok {
|
||||
doc.Title = title
|
||||
}
|
||||
|
||||
if content, ok := row["content"].(string); ok {
|
||||
doc.Content = content
|
||||
}
|
||||
|
||||
if createdAt, ok := row["created_at"].(string); ok {
|
||||
t, err := time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
if err == nil {
|
||||
// 转换时间字段
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", createdAt); err == nil {
|
||||
doc.CreatedAt = t
|
||||
}
|
||||
}
|
||||
|
||||
if updatedAt, ok := row["updated_at"].(string); ok {
|
||||
t, err := time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
if err == nil {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", updatedAt); err == nil {
|
||||
doc.UpdatedAt = t
|
||||
}
|
||||
}
|
||||
|
||||
if isDeletedInt, ok := row["is_deleted"].(int64); ok {
|
||||
doc.IsDeleted = isDeletedInt == 1
|
||||
}
|
||||
// 转换布尔字段
|
||||
doc.IsDeleted = isDeleted == 1
|
||||
doc.IsLocked = isLocked == 1
|
||||
|
||||
return doc, nil
|
||||
}
|
||||
@@ -178,6 +181,10 @@ func (ds *DocumentService) CreateDocument(title string) (*models.Document, error
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||
return nil, errors.New("database service not available")
|
||||
}
|
||||
|
||||
// Create document with default content
|
||||
now := time.Now()
|
||||
doc := &models.Document{
|
||||
@@ -186,41 +193,102 @@ func (ds *DocumentService) CreateDocument(title string) (*models.Document, error
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
IsDeleted: false,
|
||||
IsLocked: false,
|
||||
}
|
||||
|
||||
// 执行插入操作
|
||||
if err := ds.databaseService.SQLite.Execute(sqlInsertDocument,
|
||||
doc.Title, doc.Content, doc.CreatedAt, doc.UpdatedAt); err != nil {
|
||||
result, err := ds.databaseService.db.Exec(sqlInsertDocument,
|
||||
doc.Title, doc.Content, doc.CreatedAt.Format("2006-01-02 15:04:05"), doc.UpdatedAt.Format("2006-01-02 15:04:05"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create document: %w", err)
|
||||
}
|
||||
|
||||
// 获取自增ID
|
||||
lastIDRows, err := ds.databaseService.SQLite.Query("SELECT last_insert_rowid()")
|
||||
lastID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get last insert ID: %w", err)
|
||||
}
|
||||
|
||||
if len(lastIDRows) == 0 {
|
||||
return nil, fmt.Errorf("no rows returned for last insert ID query")
|
||||
}
|
||||
|
||||
// 从结果中提取ID
|
||||
lastID, ok := lastIDRows[0]["last_insert_rowid()"].(int64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert last insert ID to int64")
|
||||
}
|
||||
|
||||
// 返回带ID的文档
|
||||
doc.ID = lastID
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// LockDocument 锁定文档,防止删除
|
||||
func (ds *DocumentService) LockDocument(id int64) error {
|
||||
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||
return errors.New("database service not available")
|
||||
}
|
||||
|
||||
// 先检查文档是否存在且未删除(不加锁避免死锁)
|
||||
doc, err := ds.GetDocumentByID(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get document: %w", err)
|
||||
}
|
||||
if doc == nil {
|
||||
return fmt.Errorf("document not found: %d", id)
|
||||
}
|
||||
if doc.IsDeleted {
|
||||
return fmt.Errorf("cannot lock deleted document: %d", id)
|
||||
}
|
||||
|
||||
// 如果已经锁定,无需操作
|
||||
if doc.IsLocked {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 现在加锁执行锁定操作
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
_, err = ds.databaseService.db.Exec(sqlSetDocumentLocked, time.Now().Format("2006-01-02 15:04:05"), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to lock document: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnlockDocument 解锁文档
|
||||
func (ds *DocumentService) UnlockDocument(id int64) error {
|
||||
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||
return errors.New("database service not available")
|
||||
}
|
||||
|
||||
// 先检查文档是否存在(不加锁避免死锁)
|
||||
doc, err := ds.GetDocumentByID(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get document: %w", err)
|
||||
}
|
||||
if doc == nil {
|
||||
return fmt.Errorf("document not found: %d", id)
|
||||
}
|
||||
|
||||
// 如果未锁定,无需操作
|
||||
if !doc.IsLocked {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 现在加锁执行解锁操作
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
_, err = ds.databaseService.db.Exec(sqlSetDocumentUnlocked, time.Now().Format("2006-01-02 15:04:05"), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unlock document: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateDocumentContent updates the content of a document
|
||||
func (ds *DocumentService) UpdateDocumentContent(id int64, content string) error {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
err := ds.databaseService.SQLite.Execute(sqlUpdateDocumentContent, content, time.Now(), id)
|
||||
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||
return errors.New("database service not available")
|
||||
}
|
||||
|
||||
_, err := ds.databaseService.db.Exec(sqlUpdateDocumentContent, content, time.Now().Format("2006-01-02 15:04:05"), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update document content: %w", err)
|
||||
}
|
||||
@@ -232,7 +300,11 @@ func (ds *DocumentService) UpdateDocumentTitle(id int64, title string) error {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
err := ds.databaseService.SQLite.Execute(sqlUpdateDocumentTitle, title, time.Now(), id)
|
||||
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||
return errors.New("database service not available")
|
||||
}
|
||||
|
||||
_, err := ds.databaseService.db.Exec(sqlUpdateDocumentTitle, title, time.Now().Format("2006-01-02 15:04:05"), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update document title: %w", err)
|
||||
}
|
||||
@@ -241,15 +313,33 @@ func (ds *DocumentService) UpdateDocumentTitle(id int64, title string) error {
|
||||
|
||||
// DeleteDocument marks a document as deleted (default document with ID=1 cannot be deleted)
|
||||
func (ds *DocumentService) DeleteDocument(id int64) error {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||
ds.logger.Error("database service not available")
|
||||
return errors.New("database service not available")
|
||||
}
|
||||
|
||||
// 不允许删除默认文档
|
||||
if id == sqlDefaultDocumentID {
|
||||
return fmt.Errorf("cannot delete the default document")
|
||||
}
|
||||
|
||||
err := ds.databaseService.SQLite.Execute(sqlMarkDocumentAsDeleted, time.Now(), id)
|
||||
// 先检查文档是否存在和锁定状态(不加锁避免死锁)
|
||||
doc, err := ds.GetDocumentByID(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get document: %w", err)
|
||||
}
|
||||
if doc == nil {
|
||||
return fmt.Errorf("document not found: %d", id)
|
||||
}
|
||||
if doc.IsLocked {
|
||||
return fmt.Errorf("cannot delete locked document: %d", id)
|
||||
}
|
||||
|
||||
// 现在加锁执行删除操作
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
_, err = ds.databaseService.db.Exec(sqlMarkDocumentAsDeleted, time.Now().Format("2006-01-02 15:04:05"), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mark document as deleted: %w", err)
|
||||
}
|
||||
@@ -261,7 +351,11 @@ func (ds *DocumentService) RestoreDocument(id int64) error {
|
||||
ds.mu.Lock()
|
||||
defer ds.mu.Unlock()
|
||||
|
||||
err := ds.databaseService.SQLite.Execute(sqlRestoreDocument, time.Now(), id)
|
||||
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||
return errors.New("database service not available")
|
||||
}
|
||||
|
||||
_, err := ds.databaseService.db.Exec(sqlRestoreDocument, time.Now().Format("2006-01-02 15:04:05"), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restore document: %w", err)
|
||||
}
|
||||
@@ -273,38 +367,48 @@ func (ds *DocumentService) ListAllDocumentsMeta() ([]*models.Document, error) {
|
||||
ds.mu.RLock()
|
||||
defer ds.mu.RUnlock()
|
||||
|
||||
rows, err := ds.databaseService.SQLite.Query(sqlListAllDocumentsMeta)
|
||||
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||
return nil, errors.New("database service not available")
|
||||
}
|
||||
|
||||
rows, err := ds.databaseService.db.Query(sqlListAllDocumentsMeta)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list document meta: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var documents []*models.Document
|
||||
for _, row := range rows {
|
||||
for rows.Next() {
|
||||
doc := &models.Document{IsDeleted: false}
|
||||
var createdAt, updatedAt string
|
||||
var isLocked int
|
||||
|
||||
if id, ok := row["id"].(int64); ok {
|
||||
doc.ID = id
|
||||
err := rows.Scan(
|
||||
&doc.ID,
|
||||
&doc.Title,
|
||||
&createdAt,
|
||||
&updatedAt,
|
||||
&isLocked,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan document row: %w", err)
|
||||
}
|
||||
|
||||
if title, ok := row["title"].(string); ok {
|
||||
doc.Title = title
|
||||
}
|
||||
|
||||
if createdAt, ok := row["created_at"].(string); ok {
|
||||
t, err := time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
if err == nil {
|
||||
// 转换时间字段
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", createdAt); err == nil {
|
||||
doc.CreatedAt = t
|
||||
}
|
||||
}
|
||||
|
||||
if updatedAt, ok := row["updated_at"].(string); ok {
|
||||
t, err := time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
if err == nil {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", updatedAt); err == nil {
|
||||
doc.UpdatedAt = t
|
||||
}
|
||||
|
||||
doc.IsLocked = isLocked == 1
|
||||
documents = append(documents, doc)
|
||||
}
|
||||
|
||||
documents = append(documents, doc)
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error iterating document rows: %w", err)
|
||||
}
|
||||
|
||||
return documents, nil
|
||||
@@ -315,38 +419,48 @@ func (ds *DocumentService) ListDeletedDocumentsMeta() ([]*models.Document, error
|
||||
ds.mu.RLock()
|
||||
defer ds.mu.RUnlock()
|
||||
|
||||
rows, err := ds.databaseService.SQLite.Query(sqlListDeletedDocumentsMeta)
|
||||
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||
return nil, errors.New("database service not available")
|
||||
}
|
||||
|
||||
rows, err := ds.databaseService.db.Query(sqlListDeletedDocumentsMeta)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list deleted document meta: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var documents []*models.Document
|
||||
for _, row := range rows {
|
||||
for rows.Next() {
|
||||
doc := &models.Document{IsDeleted: true}
|
||||
var createdAt, updatedAt string
|
||||
var isLocked int
|
||||
|
||||
if id, ok := row["id"].(int64); ok {
|
||||
doc.ID = id
|
||||
err := rows.Scan(
|
||||
&doc.ID,
|
||||
&doc.Title,
|
||||
&createdAt,
|
||||
&updatedAt,
|
||||
&isLocked,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan document row: %w", err)
|
||||
}
|
||||
|
||||
if title, ok := row["title"].(string); ok {
|
||||
doc.Title = title
|
||||
}
|
||||
|
||||
if createdAt, ok := row["created_at"].(string); ok {
|
||||
t, err := time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
if err == nil {
|
||||
// 转换时间字段
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", createdAt); err == nil {
|
||||
doc.CreatedAt = t
|
||||
}
|
||||
}
|
||||
|
||||
if updatedAt, ok := row["updated_at"].(string); ok {
|
||||
t, err := time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
if err == nil {
|
||||
if t, err := time.Parse("2006-01-02 15:04:05", updatedAt); err == nil {
|
||||
doc.UpdatedAt = t
|
||||
}
|
||||
|
||||
doc.IsLocked = isLocked == 1
|
||||
documents = append(documents, doc)
|
||||
}
|
||||
|
||||
documents = append(documents, doc)
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error iterating deleted document rows: %w", err)
|
||||
}
|
||||
|
||||
return documents, nil
|
||||
@@ -357,7 +471,12 @@ func (ds *DocumentService) GetFirstDocumentID() (int64, error) {
|
||||
ds.mu.RLock()
|
||||
defer ds.mu.RUnlock()
|
||||
|
||||
rows, err := ds.databaseService.SQLite.Query(sqlGetFirstDocumentID)
|
||||
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||
return 0, errors.New("database service not available")
|
||||
}
|
||||
|
||||
var id int64
|
||||
err := ds.databaseService.db.QueryRow(sqlGetFirstDocumentID).Scan(&id)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, nil // No documents exist
|
||||
@@ -365,14 +484,5 @@ func (ds *DocumentService) GetFirstDocumentID() (int64, error) {
|
||||
return 0, fmt.Errorf("failed to get first document ID: %w", err)
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
id, ok := rows[0]["id"].(int64)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("failed to convert ID to int64")
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
@@ -41,7 +41,7 @@ WHERE id = ?`
|
||||
// ExtensionService 扩展管理服务
|
||||
type ExtensionService struct {
|
||||
databaseService *DatabaseService
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
|
||||
mu sync.RWMutex
|
||||
ctx context.Context
|
||||
@@ -73,7 +73,7 @@ func (e *ExtensionError) Is(target error) bool {
|
||||
}
|
||||
|
||||
// NewExtensionService 创建扩展服务实例
|
||||
func NewExtensionService(databaseService *DatabaseService, logger *log.Service) *ExtensionService {
|
||||
func NewExtensionService(databaseService *DatabaseService, logger *log.LogService) *ExtensionService {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
}
|
||||
@@ -104,21 +104,17 @@ func (es *ExtensionService) initDatabase() error {
|
||||
es.mu.Lock()
|
||||
defer es.mu.Unlock()
|
||||
|
||||
if es.databaseService == nil || es.databaseService.db == nil {
|
||||
return &ExtensionError{"check_db", "", errors.New("database service not available")}
|
||||
}
|
||||
|
||||
// 检查是否已有扩展数据
|
||||
rows, err := es.databaseService.SQLite.Query("SELECT COUNT(*) FROM extensions")
|
||||
var count int64
|
||||
err := es.databaseService.db.QueryRow("SELECT COUNT(*) FROM extensions").Scan(&count)
|
||||
if err != nil {
|
||||
return &ExtensionError{"check_extensions_count", "", err}
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
return &ExtensionError{"check_extensions_count", "", fmt.Errorf("no rows returned")}
|
||||
}
|
||||
|
||||
count, ok := rows[0]["COUNT(*)"].(int64)
|
||||
if !ok {
|
||||
return &ExtensionError{"convert_count", "", fmt.Errorf("failed to convert count to int64")}
|
||||
}
|
||||
|
||||
// 如果没有数据,插入默认配置
|
||||
if count == 0 {
|
||||
if err := es.insertDefaultExtensions(); err != nil {
|
||||
@@ -133,16 +129,15 @@ func (es *ExtensionService) initDatabase() error {
|
||||
// insertDefaultExtensions 插入默认扩展配置
|
||||
func (es *ExtensionService) insertDefaultExtensions() error {
|
||||
defaultSettings := models.NewDefaultExtensionSettings()
|
||||
now := time.Now()
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
|
||||
for _, ext := range defaultSettings.Extensions {
|
||||
|
||||
configJSON, err := json.Marshal(ext.Config)
|
||||
if err != nil {
|
||||
return &ExtensionError{"marshal_config", string(ext.ID), err}
|
||||
}
|
||||
|
||||
err = es.databaseService.SQLite.Execute(sqlInsertExtension,
|
||||
_, err = es.databaseService.db.Exec(sqlInsertExtension,
|
||||
string(ext.ID),
|
||||
ext.Enabled,
|
||||
ext.IsDefault,
|
||||
@@ -153,7 +148,6 @@ func (es *ExtensionService) insertDefaultExtensions() error {
|
||||
if err != nil {
|
||||
return &ExtensionError{"insert_extension", string(ext.ID), err}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -179,38 +173,51 @@ func (es *ExtensionService) GetAllExtensions() ([]models.Extension, error) {
|
||||
es.mu.RLock()
|
||||
defer es.mu.RUnlock()
|
||||
|
||||
rows, err := es.databaseService.SQLite.Query(sqlGetAllExtensions)
|
||||
if es.databaseService == nil || es.databaseService.db == nil {
|
||||
return nil, &ExtensionError{"query_db", "", errors.New("database service not available")}
|
||||
}
|
||||
|
||||
rows, err := es.databaseService.db.Query(sqlGetAllExtensions)
|
||||
if err != nil {
|
||||
return nil, &ExtensionError{"query_extensions", "", err}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var extensions []models.Extension
|
||||
for _, row := range rows {
|
||||
for rows.Next() {
|
||||
var ext models.Extension
|
||||
var id string
|
||||
var configJSON string
|
||||
var enabled, isDefault int
|
||||
|
||||
err := rows.Scan(
|
||||
&id,
|
||||
&enabled,
|
||||
&isDefault,
|
||||
&configJSON,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, &ExtensionError{"scan_extension", "", err}
|
||||
}
|
||||
|
||||
if id, ok := row["id"].(string); ok {
|
||||
ext.ID = models.ExtensionID(id)
|
||||
}
|
||||
|
||||
if enabled, ok := row["enabled"].(int64); ok {
|
||||
ext.Enabled = enabled == 1
|
||||
}
|
||||
|
||||
if isDefault, ok := row["is_default"].(int64); ok {
|
||||
ext.IsDefault = isDefault == 1
|
||||
}
|
||||
|
||||
if configJSON, ok := row["config"].(string); ok {
|
||||
var config models.ExtensionConfig
|
||||
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
|
||||
return nil, &ExtensionError{"unmarshal_config", string(ext.ID), err}
|
||||
return nil, &ExtensionError{"unmarshal_config", id, err}
|
||||
}
|
||||
ext.Config = config
|
||||
}
|
||||
|
||||
extensions = append(extensions, ext)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, &ExtensionError{"iterate_extensions", "", err}
|
||||
}
|
||||
|
||||
return extensions, nil
|
||||
}
|
||||
|
||||
@@ -224,6 +231,10 @@ func (es *ExtensionService) UpdateExtensionState(id models.ExtensionID, enabled
|
||||
es.mu.Lock()
|
||||
defer es.mu.Unlock()
|
||||
|
||||
if es.databaseService == nil || es.databaseService.db == nil {
|
||||
return &ExtensionError{"check_db", string(id), errors.New("database service not available")}
|
||||
}
|
||||
|
||||
var configJSON []byte
|
||||
var err error
|
||||
|
||||
@@ -234,24 +245,19 @@ func (es *ExtensionService) UpdateExtensionState(id models.ExtensionID, enabled
|
||||
}
|
||||
} else {
|
||||
// 如果没有提供配置,保持原有配置
|
||||
rows, err := es.databaseService.SQLite.Query("SELECT config FROM extensions WHERE id = ?", string(id))
|
||||
var currentConfigJSON string
|
||||
err := es.databaseService.db.QueryRow("SELECT config FROM extensions WHERE id = ?", string(id)).Scan(¤tConfigJSON)
|
||||
if err != nil {
|
||||
return &ExtensionError{"query_current_config", string(id), err}
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
return &ExtensionError{"query_current_config", string(id), fmt.Errorf("extension not found")}
|
||||
}
|
||||
|
||||
currentConfigJSON, ok := rows[0]["config"].(string)
|
||||
if !ok {
|
||||
return &ExtensionError{"convert_config", string(id), fmt.Errorf("failed to get current config")}
|
||||
}
|
||||
|
||||
configJSON = []byte(currentConfigJSON)
|
||||
}
|
||||
|
||||
err = es.databaseService.SQLite.Execute(sqlUpdateExtension, enabled, string(configJSON), time.Now(), string(id))
|
||||
_, err = es.databaseService.db.Exec(sqlUpdateExtension,
|
||||
enabled,
|
||||
string(configJSON),
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
string(id))
|
||||
if err != nil {
|
||||
return &ExtensionError{"update_extension", string(id), err}
|
||||
}
|
||||
@@ -276,8 +282,12 @@ func (es *ExtensionService) ResetAllExtensionsToDefault() error {
|
||||
es.mu.Lock()
|
||||
defer es.mu.Unlock()
|
||||
|
||||
if es.databaseService == nil || es.databaseService.db == nil {
|
||||
return &ExtensionError{"check_db", "", errors.New("database service not available")}
|
||||
}
|
||||
|
||||
// 删除所有现有扩展
|
||||
err := es.databaseService.SQLite.Execute(sqlDeleteAllExtensions)
|
||||
_, err := es.databaseService.db.Exec(sqlDeleteAllExtensions)
|
||||
if err != nil {
|
||||
return &ExtensionError{"delete_all_extensions", "", err}
|
||||
}
|
||||
|
||||
@@ -24,16 +24,18 @@ import (
|
||||
|
||||
// HotkeyService Windows全局热键服务
|
||||
type HotkeyService struct {
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
configService *ConfigService
|
||||
windowService *WindowService
|
||||
app *application.App
|
||||
mainWindow *application.WebviewWindow
|
||||
|
||||
mu sync.RWMutex
|
||||
currentHotkey *models.HotkeyCombo
|
||||
isRegistered atomic.Bool
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
cancelFunc atomic.Value // 使用atomic.Value存储cancel函数,避免竞态条件
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
@@ -51,24 +53,48 @@ func (e *HotkeyError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// setCancelFunc 原子地设置cancel函数
|
||||
func (hs *HotkeyService) setCancelFunc(cancel context.CancelFunc) {
|
||||
hs.cancelFunc.Store(cancel)
|
||||
}
|
||||
|
||||
// getCancelFunc 原子地获取cancel函数
|
||||
func (hs *HotkeyService) getCancelFunc() context.CancelFunc {
|
||||
if cancel := hs.cancelFunc.Load(); cancel != nil {
|
||||
if cancelFunc, ok := cancel.(context.CancelFunc); ok {
|
||||
return cancelFunc
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearCancelFunc 原子地清除cancel函数
|
||||
func (hs *HotkeyService) clearCancelFunc() {
|
||||
hs.cancelFunc.Store((context.CancelFunc)(nil))
|
||||
}
|
||||
|
||||
// NewHotkeyService 创建热键服务实例
|
||||
func NewHotkeyService(configService *ConfigService, logger *log.Service) *HotkeyService {
|
||||
func NewHotkeyService(configService *ConfigService, windowService *WindowService, logger *log.LogService) *HotkeyService {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &HotkeyService{
|
||||
service := &HotkeyService{
|
||||
logger: logger,
|
||||
configService: configService,
|
||||
windowService: windowService,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
// 初始化时设置cancel函数
|
||||
service.setCancelFunc(cancel)
|
||||
return service
|
||||
}
|
||||
|
||||
// Initialize 初始化热键服务
|
||||
func (hs *HotkeyService) Initialize(app *application.App) error {
|
||||
func (hs *HotkeyService) Initialize(app *application.App, mainWindow *application.WebviewWindow) error {
|
||||
hs.app = app
|
||||
hs.mainWindow = mainWindow
|
||||
|
||||
config, err := hs.configService.GetConfig()
|
||||
if err != nil {
|
||||
@@ -119,7 +145,7 @@ func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error {
|
||||
|
||||
hs.currentHotkey = hotkey
|
||||
hs.isRegistered.Store(true)
|
||||
hs.cancel = cancel
|
||||
hs.setCancelFunc(cancel)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -137,13 +163,15 @@ func (hs *HotkeyService) unregisterInternal() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if hs.cancel != nil {
|
||||
hs.cancel()
|
||||
// 原子地获取并调用cancel函数
|
||||
if cancel := hs.getCancelFunc(); cancel != nil {
|
||||
cancel()
|
||||
hs.wg.Wait()
|
||||
}
|
||||
|
||||
hs.currentHotkey = nil
|
||||
hs.isRegistered.Store(false)
|
||||
hs.clearCancelFunc()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -165,7 +193,7 @@ func (hs *HotkeyService) hotkeyListener(ctx context.Context, hotkey *models.Hotk
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
ticker := time.NewTicker(50 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
var wasPressed bool
|
||||
@@ -199,11 +227,61 @@ func cBool(b bool) C.int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// toggleWindow 切换窗口
|
||||
// toggleWindow 切换窗口显示状态
|
||||
func (hs *HotkeyService) toggleWindow() {
|
||||
if hs.app != nil {
|
||||
hs.app.Event.Emit("hotkey:toggle-window", nil)
|
||||
if hs.mainWindow == nil {
|
||||
hs.logger.Error("main window not set")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查主窗口是否可见
|
||||
if hs.isWindowVisible(hs.mainWindow) {
|
||||
// 如果主窗口可见,隐藏所有窗口
|
||||
hs.hideAllWindows()
|
||||
} else {
|
||||
// 如果主窗口不可见,显示所有窗口
|
||||
hs.showAllWindows()
|
||||
}
|
||||
}
|
||||
|
||||
// isWindowVisible 检查窗口是否可见
|
||||
func (hs *HotkeyService) isWindowVisible(window *application.WebviewWindow) bool {
|
||||
return window.IsVisible()
|
||||
}
|
||||
|
||||
// hideAllWindows 隐藏所有窗口
|
||||
func (hs *HotkeyService) hideAllWindows() {
|
||||
// 隐藏主窗口
|
||||
hs.mainWindow.Hide()
|
||||
|
||||
// 隐藏所有子窗口
|
||||
if hs.windowService != nil {
|
||||
openWindows := hs.windowService.GetOpenWindows()
|
||||
for _, windowInfo := range openWindows {
|
||||
windowInfo.Window.Hide()
|
||||
}
|
||||
}
|
||||
|
||||
hs.logger.Debug("all windows hidden")
|
||||
}
|
||||
|
||||
// showAllWindows 显示所有窗口
|
||||
func (hs *HotkeyService) showAllWindows() {
|
||||
// 显示主窗口
|
||||
hs.mainWindow.Show()
|
||||
hs.mainWindow.Restore()
|
||||
hs.mainWindow.Focus()
|
||||
|
||||
// 显示所有子窗口
|
||||
if hs.windowService != nil {
|
||||
openWindows := hs.windowService.GetOpenWindows()
|
||||
for _, windowInfo := range openWindows {
|
||||
windowInfo.Window.Show()
|
||||
windowInfo.Window.Restore()
|
||||
}
|
||||
}
|
||||
|
||||
hs.logger.Debug("all windows shown")
|
||||
}
|
||||
|
||||
// keyToVirtualKeyCode 键名转虚拟键码
|
||||
@@ -261,7 +339,10 @@ func (hs *HotkeyService) IsRegistered() bool {
|
||||
|
||||
// ServiceShutdown 关闭服务
|
||||
func (hs *HotkeyService) ServiceShutdown() error {
|
||||
hs.cancel()
|
||||
// 原子地获取并调用cancel函数
|
||||
if cancel := hs.getCancelFunc(); cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
hs.wg.Wait()
|
||||
return nil
|
||||
}
|
||||
@@ -66,6 +66,7 @@ int isHotkeyRegistered() {
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -80,12 +81,15 @@ var globalHotkeyService *HotkeyService
|
||||
|
||||
// HotkeyService macOS全局热键服务
|
||||
type HotkeyService struct {
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
configService *ConfigService
|
||||
windowService *WindowService
|
||||
app *application.App
|
||||
mainWindow *application.WebviewWindow
|
||||
mu sync.RWMutex
|
||||
isRegistered atomic.Bool
|
||||
currentHotkey *models.HotkeyCombo
|
||||
cancelFunc atomic.Value // 使用atomic.Value存储cancel函数,避免竞态条件
|
||||
}
|
||||
|
||||
// HotkeyError 热键错误
|
||||
@@ -104,8 +108,28 @@ func (e *HotkeyError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// setCancelFunc 原子地设置cancel函数
|
||||
func (hs *HotkeyService) setCancelFunc(cancel context.CancelFunc) {
|
||||
hs.cancelFunc.Store(cancel)
|
||||
}
|
||||
|
||||
// getCancelFunc 原子地获取cancel函数
|
||||
func (hs *HotkeyService) getCancelFunc() context.CancelFunc {
|
||||
if cancel := hs.cancelFunc.Load(); cancel != nil {
|
||||
if cancelFunc, ok := cancel.(context.CancelFunc); ok {
|
||||
return cancelFunc
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearCancelFunc 原子地清除cancel函数
|
||||
func (hs *HotkeyService) clearCancelFunc() {
|
||||
hs.cancelFunc.Store((context.CancelFunc)(nil))
|
||||
}
|
||||
|
||||
// NewHotkeyService 创建新的热键服务实例
|
||||
func NewHotkeyService(configService *ConfigService, logger *log.Service) *HotkeyService {
|
||||
func NewHotkeyService(configService *ConfigService, windowService *WindowService, logger *log.LogService) *HotkeyService {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
}
|
||||
@@ -113,6 +137,7 @@ func NewHotkeyService(configService *ConfigService, logger *log.Service) *Hotkey
|
||||
service := &HotkeyService{
|
||||
logger: logger,
|
||||
configService: configService,
|
||||
windowService: windowService,
|
||||
}
|
||||
|
||||
// 设置全局实例
|
||||
@@ -122,8 +147,9 @@ func NewHotkeyService(configService *ConfigService, logger *log.Service) *Hotkey
|
||||
}
|
||||
|
||||
// Initialize 初始化热键服务
|
||||
func (hs *HotkeyService) Initialize(app *application.App) error {
|
||||
func (hs *HotkeyService) Initialize(app *application.App, mainWindow *application.WebviewWindow) error {
|
||||
hs.app = app
|
||||
hs.mainWindow = mainWindow
|
||||
|
||||
// 加载并应用当前配置
|
||||
config, err := hs.configService.GetConfig()
|
||||
@@ -285,9 +311,59 @@ func (hs *HotkeyService) IsRegistered() bool {
|
||||
|
||||
// ToggleWindow 切换窗口显示状态
|
||||
func (hs *HotkeyService) ToggleWindow() {
|
||||
if hs.app != nil {
|
||||
hs.app.EmitEvent("hotkey:toggle-window", nil)
|
||||
if hs.mainWindow == nil {
|
||||
hs.logger.Error("main window not set")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查主窗口是否可见
|
||||
if hs.isWindowVisible(hs.mainWindow) {
|
||||
// 如果主窗口可见,隐藏所有窗口
|
||||
hs.hideAllWindows()
|
||||
} else {
|
||||
// 如果主窗口不可见,显示所有窗口
|
||||
hs.showAllWindows()
|
||||
}
|
||||
}
|
||||
|
||||
// isWindowVisible 检查窗口是否可见
|
||||
func (hs *HotkeyService) isWindowVisible(window *application.WebviewWindow) bool {
|
||||
return window.IsVisible()
|
||||
}
|
||||
|
||||
// hideAllWindows 隐藏所有窗口
|
||||
func (hs *HotkeyService) hideAllWindows() {
|
||||
// 隐藏主窗口
|
||||
hs.mainWindow.Hide()
|
||||
|
||||
// 隐藏所有子窗口
|
||||
if hs.windowService != nil {
|
||||
openWindows := hs.windowService.GetOpenWindows()
|
||||
for _, windowInfo := range openWindows {
|
||||
windowInfo.Window.Hide()
|
||||
}
|
||||
}
|
||||
|
||||
hs.logger.Debug("all windows hidden")
|
||||
}
|
||||
|
||||
// showAllWindows 显示所有窗口
|
||||
func (hs *HotkeyService) showAllWindows() {
|
||||
// 显示主窗口
|
||||
hs.mainWindow.Show()
|
||||
hs.mainWindow.Restore()
|
||||
hs.mainWindow.Focus()
|
||||
|
||||
// 显示所有子窗口
|
||||
if hs.windowService != nil {
|
||||
openWindows := hs.windowService.GetOpenWindows()
|
||||
for _, windowInfo := range openWindows {
|
||||
windowInfo.Window.Show()
|
||||
windowInfo.Window.Restore()
|
||||
}
|
||||
}
|
||||
|
||||
hs.logger.Debug("all windows shown")
|
||||
}
|
||||
|
||||
// ServiceShutdown 关闭热键服务
|
||||
|
||||
@@ -141,16 +141,18 @@ import (
|
||||
|
||||
// HotkeyService Linux全局热键服务
|
||||
type HotkeyService struct {
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
configService *ConfigService
|
||||
windowService *WindowService
|
||||
app *application.App
|
||||
mainWindow *application.WebviewWindow
|
||||
|
||||
mu sync.RWMutex
|
||||
currentHotkey *models.HotkeyCombo
|
||||
isRegistered atomic.Bool
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
cancelFunc atomic.Value // 使用atomic.Value存储cancel函数,避免竞态条件
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
@@ -169,24 +171,48 @@ func (e *HotkeyError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// setCancelFunc 原子地设置cancel函数
|
||||
func (hs *HotkeyService) setCancelFunc(cancel context.CancelFunc) {
|
||||
hs.cancelFunc.Store(cancel)
|
||||
}
|
||||
|
||||
// getCancelFunc 原子地获取cancel函数
|
||||
func (hs *HotkeyService) getCancelFunc() context.CancelFunc {
|
||||
if cancel := hs.cancelFunc.Load(); cancel != nil {
|
||||
if cancelFunc, ok := cancel.(context.CancelFunc); ok {
|
||||
return cancelFunc
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearCancelFunc 原子地清除cancel函数
|
||||
func (hs *HotkeyService) clearCancelFunc() {
|
||||
hs.cancelFunc.Store((context.CancelFunc)(nil))
|
||||
}
|
||||
|
||||
// NewHotkeyService 创建热键服务实例
|
||||
func NewHotkeyService(configService *ConfigService, logger *log.Service) *HotkeyService {
|
||||
func NewHotkeyService(configService *ConfigService, windowService *WindowService, logger *log.LogService) *HotkeyService {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &HotkeyService{
|
||||
service := &HotkeyService{
|
||||
logger: logger,
|
||||
configService: configService,
|
||||
windowService: windowService,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
// 初始化时设置cancel函数
|
||||
service.setCancelFunc(cancel)
|
||||
return service
|
||||
}
|
||||
|
||||
// Initialize 初始化热键服务
|
||||
func (hs *HotkeyService) Initialize(app *application.App) error {
|
||||
func (hs *HotkeyService) Initialize(app *application.App, mainWindow *application.WebviewWindow) error {
|
||||
hs.app = app
|
||||
hs.mainWindow = mainWindow
|
||||
|
||||
if int(C.initX11Display()) == 0 {
|
||||
return &HotkeyError{"init_x11", fmt.Errorf("failed to initialize X11 display")}
|
||||
@@ -253,7 +279,7 @@ func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error {
|
||||
|
||||
hs.currentHotkey = hotkey
|
||||
hs.isRegistered.Store(true)
|
||||
hs.cancel = cancel
|
||||
hs.setCancelFunc(cancel)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -271,8 +297,9 @@ func (hs *HotkeyService) unregisterInternal() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if hs.cancel != nil {
|
||||
hs.cancel()
|
||||
// 原子地获取并调用cancel函数
|
||||
if cancel := hs.getCancelFunc(); cancel != nil {
|
||||
cancel()
|
||||
hs.wg.Wait()
|
||||
}
|
||||
|
||||
@@ -283,6 +310,7 @@ func (hs *HotkeyService) unregisterInternal() error {
|
||||
|
||||
hs.currentHotkey = nil
|
||||
hs.isRegistered.Store(false)
|
||||
hs.clearCancelFunc()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -298,7 +326,8 @@ func (hs *HotkeyService) UpdateHotkey(enable bool, hotkey *models.HotkeyCombo) e
|
||||
func (hs *HotkeyService) hotkeyListener(ctx context.Context, ready chan<- error) {
|
||||
defer hs.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
// 优化轮询频率从100ms改为50ms,提高响应性
|
||||
ticker := time.NewTicker(50 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
ready <- nil // 标记准备就绪
|
||||
@@ -315,11 +344,61 @@ func (hs *HotkeyService) hotkeyListener(ctx context.Context, ready chan<- error)
|
||||
}
|
||||
}
|
||||
|
||||
// toggleWindow 切换窗口
|
||||
// toggleWindow 切换窗口显示状态
|
||||
func (hs *HotkeyService) toggleWindow() {
|
||||
if hs.app != nil {
|
||||
hs.app.EmitEvent("hotkey:toggle-window", nil)
|
||||
if hs.mainWindow == nil {
|
||||
hs.logger.Error("main window not set")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查主窗口是否可见
|
||||
if hs.isWindowVisible(hs.mainWindow) {
|
||||
// 如果主窗口可见,隐藏所有窗口
|
||||
hs.hideAllWindows()
|
||||
} else {
|
||||
// 如果主窗口不可见,显示所有窗口
|
||||
hs.showAllWindows()
|
||||
}
|
||||
}
|
||||
|
||||
// isWindowVisible 检查窗口是否可见
|
||||
func (hs *HotkeyService) isWindowVisible(window *application.WebviewWindow) bool {
|
||||
return window.IsVisible()
|
||||
}
|
||||
|
||||
// hideAllWindows 隐藏所有窗口
|
||||
func (hs *HotkeyService) hideAllWindows() {
|
||||
// 隐藏主窗口
|
||||
hs.mainWindow.Hide()
|
||||
|
||||
// 隐藏所有子窗口
|
||||
if hs.windowService != nil {
|
||||
openWindows := hs.windowService.GetOpenWindows()
|
||||
for _, windowInfo := range openWindows {
|
||||
windowInfo.Window.Hide()
|
||||
}
|
||||
}
|
||||
|
||||
hs.logger.Debug("all windows hidden")
|
||||
}
|
||||
|
||||
// showAllWindows 显示所有窗口
|
||||
func (hs *HotkeyService) showAllWindows() {
|
||||
// 显示主窗口
|
||||
hs.mainWindow.Show()
|
||||
hs.mainWindow.Restore()
|
||||
hs.mainWindow.Focus()
|
||||
|
||||
// 显示所有子窗口
|
||||
if hs.windowService != nil {
|
||||
openWindows := hs.windowService.GetOpenWindows()
|
||||
for _, windowInfo := range openWindows {
|
||||
windowInfo.Window.Show()
|
||||
windowInfo.Window.Restore()
|
||||
}
|
||||
}
|
||||
|
||||
hs.logger.Debug("all windows shown")
|
||||
}
|
||||
|
||||
// keyToX11KeyCode 键名转X11键码
|
||||
@@ -386,7 +465,10 @@ func (hs *HotkeyService) IsRegistered() bool {
|
||||
|
||||
// ServiceShutdown 关闭服务
|
||||
func (hs *HotkeyService) ServiceShutdown() error {
|
||||
hs.cancel()
|
||||
// 原子地获取并调用cancel函数
|
||||
if cancel := hs.getCancelFunc(); cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
hs.wg.Wait()
|
||||
C.closeX11Display()
|
||||
return nil
|
||||
|
||||
@@ -51,7 +51,7 @@ const (
|
||||
// KeyBindingService 快捷键管理服务
|
||||
type KeyBindingService struct {
|
||||
databaseService *DatabaseService
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
|
||||
mu sync.RWMutex
|
||||
ctx context.Context
|
||||
@@ -83,7 +83,7 @@ func (e *KeyBindingError) Is(target error) bool {
|
||||
}
|
||||
|
||||
// NewKeyBindingService 创建快捷键服务实例
|
||||
func NewKeyBindingService(databaseService *DatabaseService, logger *log.Service) *KeyBindingService {
|
||||
func NewKeyBindingService(databaseService *DatabaseService, logger *log.LogService) *KeyBindingService {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
}
|
||||
@@ -105,21 +105,17 @@ func (kbs *KeyBindingService) initDatabase() error {
|
||||
kbs.mu.Lock()
|
||||
defer kbs.mu.Unlock()
|
||||
|
||||
if kbs.databaseService == nil || kbs.databaseService.db == nil {
|
||||
return &KeyBindingError{"check_db", "", errors.New("database service not available")}
|
||||
}
|
||||
|
||||
// 检查是否已有快捷键数据
|
||||
rows, err := kbs.databaseService.SQLite.Query("SELECT COUNT(*) FROM key_bindings")
|
||||
var count int64
|
||||
err := kbs.databaseService.db.QueryRow("SELECT COUNT(*) FROM key_bindings").Scan(&count)
|
||||
if err != nil {
|
||||
return &KeyBindingError{"check_keybindings_count", "", err}
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
return &KeyBindingError{"check_keybindings_count", "", fmt.Errorf("no rows returned")}
|
||||
}
|
||||
|
||||
count, ok := rows[0]["COUNT(*)"].(int64)
|
||||
if !ok {
|
||||
return &KeyBindingError{"convert_count", "", fmt.Errorf("failed to convert count to int64")}
|
||||
}
|
||||
|
||||
// 如果没有数据,插入默认配置
|
||||
if count == 0 {
|
||||
if err := kbs.insertDefaultKeyBindings(); err != nil {
|
||||
@@ -134,11 +130,10 @@ func (kbs *KeyBindingService) initDatabase() error {
|
||||
// insertDefaultKeyBindings 插入默认快捷键配置
|
||||
func (kbs *KeyBindingService) insertDefaultKeyBindings() error {
|
||||
defaultConfig := models.NewDefaultKeyBindingConfig()
|
||||
now := time.Now()
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
|
||||
for _, kb := range defaultConfig.KeyBindings {
|
||||
|
||||
err := kbs.databaseService.SQLite.Execute(sqlInsertKeyBinding,
|
||||
_, err := kbs.databaseService.db.Exec(sqlInsertKeyBinding,
|
||||
string(kb.Command), // 转换为字符串存储
|
||||
string(kb.Extension), // 转换为字符串存储
|
||||
kb.Key,
|
||||
@@ -150,7 +145,6 @@ func (kbs *KeyBindingService) insertDefaultKeyBindings() error {
|
||||
if err != nil {
|
||||
return &KeyBindingError{"insert_keybinding", string(kb.Command), err}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -161,38 +155,46 @@ func (kbs *KeyBindingService) GetAllKeyBindings() ([]models.KeyBinding, error) {
|
||||
kbs.mu.RLock()
|
||||
defer kbs.mu.RUnlock()
|
||||
|
||||
rows, err := kbs.databaseService.SQLite.Query(sqlGetAllKeyBindings)
|
||||
if kbs.databaseService == nil || kbs.databaseService.db == nil {
|
||||
return nil, &KeyBindingError{"query_db", "", errors.New("database service not available")}
|
||||
}
|
||||
|
||||
rows, err := kbs.databaseService.db.Query(sqlGetAllKeyBindings)
|
||||
if err != nil {
|
||||
return nil, &KeyBindingError{"query_keybindings", "", err}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var keyBindings []models.KeyBinding
|
||||
for _, row := range rows {
|
||||
for rows.Next() {
|
||||
var kb models.KeyBinding
|
||||
var command, extension string
|
||||
var enabled, isDefault int
|
||||
|
||||
err := rows.Scan(
|
||||
&command,
|
||||
&extension,
|
||||
&kb.Key,
|
||||
&enabled,
|
||||
&isDefault,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, &KeyBindingError{"scan_keybinding", "", err}
|
||||
}
|
||||
|
||||
if command, ok := row["command"].(string); ok {
|
||||
kb.Command = models.KeyBindingCommand(command)
|
||||
}
|
||||
|
||||
if extension, ok := row["extension"].(string); ok {
|
||||
kb.Extension = models.ExtensionID(extension)
|
||||
}
|
||||
|
||||
if key, ok := row["key"].(string); ok {
|
||||
kb.Key = key
|
||||
}
|
||||
|
||||
if enabled, ok := row["enabled"].(int64); ok {
|
||||
kb.Enabled = enabled == 1
|
||||
}
|
||||
|
||||
if isDefault, ok := row["is_default"].(int64); ok {
|
||||
kb.IsDefault = isDefault == 1
|
||||
}
|
||||
|
||||
keyBindings = append(keyBindings, kb)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, &KeyBindingError{"iterate_keybindings", "", err}
|
||||
}
|
||||
|
||||
return keyBindings, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ type MigrationProgress struct {
|
||||
|
||||
// MigrationService 迁移服务
|
||||
type MigrationService struct {
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
dbService *DatabaseService
|
||||
mu sync.RWMutex
|
||||
progress atomic.Value // stores MigrationProgress
|
||||
|
||||
@@ -42,13 +43,14 @@ type MigrationService struct {
|
||||
}
|
||||
|
||||
// NewMigrationService 创建迁移服务
|
||||
func NewMigrationService(logger *log.Service) *MigrationService {
|
||||
func NewMigrationService(dbService *DatabaseService, logger *log.LogService) *MigrationService {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
}
|
||||
|
||||
ms := &MigrationService{
|
||||
logger: logger,
|
||||
dbService: dbService,
|
||||
}
|
||||
|
||||
// 初始化进度
|
||||
@@ -104,6 +106,17 @@ func (ms *MigrationService) MigrateDirectory(srcPath, dstPath string) error {
|
||||
return ms.failWithError(err)
|
||||
}
|
||||
|
||||
// 迁移前断开数据库连接
|
||||
ms.updateProgress(MigrationProgress{
|
||||
Status: MigrationStatusMigrating,
|
||||
Progress: 10,
|
||||
})
|
||||
|
||||
if ms.dbService != nil {
|
||||
if err := ms.dbService.ServiceShutdown(); err != nil {
|
||||
ms.logger.Error("Failed to close database connection", "error", err)
|
||||
}
|
||||
}
|
||||
// 执行原子迁移
|
||||
if err := ms.atomicMove(ctx, srcPath, dstPath); err != nil {
|
||||
return ms.failWithError(err)
|
||||
@@ -177,10 +190,17 @@ func (ms *MigrationService) atomicMove(ctx context.Context, srcPath, dstPath str
|
||||
})
|
||||
|
||||
if err := os.Rename(srcPath, dstPath); err == nil {
|
||||
// 重命名成功,更新进度到90%
|
||||
ms.updateProgress(MigrationProgress{
|
||||
Status: MigrationStatusMigrating,
|
||||
Progress: 90,
|
||||
})
|
||||
ms.logger.Info("Directory migration completed using direct rename", "src", srcPath, "dst", dstPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 重命名失败,使用压缩迁移
|
||||
ms.logger.Info("Direct rename failed, using compress migration", "src", srcPath, "dst", dstPath)
|
||||
ms.updateProgress(MigrationProgress{
|
||||
Status: MigrationStatusMigrating,
|
||||
Progress: 30,
|
||||
@@ -249,12 +269,18 @@ func (ms *MigrationService) compressMove(ctx context.Context, srcPath, dstPath s
|
||||
default:
|
||||
}
|
||||
|
||||
// 验证迁移是否成功
|
||||
if err := ms.verifyMigration(dstPath); err != nil {
|
||||
// 迁移验证失败,清理目标目录
|
||||
os.RemoveAll(dstPath)
|
||||
return fmt.Errorf("migration verification failed: %v", err)
|
||||
}
|
||||
|
||||
// 删除源目录
|
||||
ms.updateProgress(MigrationProgress{
|
||||
Status: MigrationStatusMigrating,
|
||||
Progress: 90,
|
||||
})
|
||||
|
||||
os.RemoveAll(srcPath)
|
||||
return nil
|
||||
}
|
||||
@@ -404,6 +430,29 @@ func (ms *MigrationService) isSubDirectory(parent, target string) bool {
|
||||
return len(target) > len(parent) && strings.HasPrefix(target, parent)
|
||||
}
|
||||
|
||||
// verifyMigration 验证迁移是否成功
|
||||
func (ms *MigrationService) verifyMigration(dstPath string) error {
|
||||
// 检查目标目录是否存在
|
||||
dstStat, err := os.Stat(dstPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("target directory does not exist: %v", err)
|
||||
}
|
||||
if !dstStat.IsDir() {
|
||||
return fmt.Errorf("target path is not a directory")
|
||||
}
|
||||
|
||||
// 简单验证:检查目标目录是否非空
|
||||
isEmpty, err := ms.isDirectoryEmpty(dstPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check target directory: %v", err)
|
||||
}
|
||||
if isEmpty {
|
||||
return fmt.Errorf("target directory is empty after migration")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelMigration 取消迁移
|
||||
func (ms *MigrationService) CancelMigration() error {
|
||||
ms.mu.Lock()
|
||||
|
||||
@@ -26,7 +26,7 @@ type SelfUpdateResult struct {
|
||||
|
||||
// SelfUpdateService 自我更新服务
|
||||
type SelfUpdateService struct {
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
configService *ConfigService
|
||||
config *models.AppConfig
|
||||
|
||||
@@ -35,7 +35,7 @@ type SelfUpdateService struct {
|
||||
}
|
||||
|
||||
// NewSelfUpdateService 创建自我更新服务实例
|
||||
func NewSelfUpdateService(configService *ConfigService, logger *log.Service) (*SelfUpdateService, error) {
|
||||
func NewSelfUpdateService(configService *ConfigService, logger *log.LogService) (*SelfUpdateService, error) {
|
||||
// 获取配置
|
||||
appConfig, err := configService.GetConfig()
|
||||
if err != nil {
|
||||
|
||||
@@ -5,16 +5,15 @@ import (
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/sqlite"
|
||||
)
|
||||
|
||||
// ServiceManager 服务管理器,负责协调各个服务
|
||||
type ServiceManager struct {
|
||||
configService *ConfigService
|
||||
databaseService *DatabaseService
|
||||
sqliteService *sqlite.Service
|
||||
documentService *DocumentService
|
||||
windowService *WindowService
|
||||
windowSnapService *WindowSnapService
|
||||
migrationService *MigrationService
|
||||
systemService *SystemService
|
||||
hotkeyService *HotkeyService
|
||||
@@ -25,7 +24,9 @@ type ServiceManager struct {
|
||||
startupService *StartupService
|
||||
selfUpdateService *SelfUpdateService
|
||||
translationService *TranslationService
|
||||
logger *log.Service
|
||||
themeService *ThemeService
|
||||
BackupService *BackupService
|
||||
logger *log.LogService
|
||||
}
|
||||
|
||||
// NewServiceManager 创建新的服务管理器实例
|
||||
@@ -36,14 +37,11 @@ func NewServiceManager() *ServiceManager {
|
||||
// 初始化配置服务
|
||||
configService := NewConfigService(logger)
|
||||
|
||||
// 初始化SQLite服务
|
||||
sqliteService := sqlite.New()
|
||||
|
||||
// 初始化数据库服务
|
||||
databaseService := NewDatabaseService(configService, logger)
|
||||
|
||||
// 初始化迁移服务
|
||||
migrationService := NewMigrationService(logger)
|
||||
migrationService := NewMigrationService(databaseService, logger)
|
||||
|
||||
// 初始化文档服务
|
||||
documentService := NewDocumentService(databaseService, logger)
|
||||
@@ -51,11 +49,17 @@ func NewServiceManager() *ServiceManager {
|
||||
// 初始化窗口服务
|
||||
windowService := NewWindowService(logger, documentService)
|
||||
|
||||
// 初始化窗口吸附服务
|
||||
windowSnapService := NewWindowSnapService(logger, configService)
|
||||
|
||||
// 将吸附服务与窗口服务关联
|
||||
windowService.SetWindowSnapService(windowSnapService)
|
||||
|
||||
// 初始化系统服务
|
||||
systemService := NewSystemService(logger)
|
||||
|
||||
// 初始化热键服务
|
||||
hotkeyService := NewHotkeyService(configService, logger)
|
||||
hotkeyService := NewHotkeyService(configService, windowService, logger)
|
||||
|
||||
// 初始化对话服务
|
||||
dialogService := NewDialogService(logger)
|
||||
@@ -81,6 +85,12 @@ func NewServiceManager() *ServiceManager {
|
||||
// 初始化翻译服务
|
||||
translationService := NewTranslationService(logger)
|
||||
|
||||
// 初始化主题服务
|
||||
themeService := NewThemeService(databaseService, logger)
|
||||
|
||||
// 初始化备份服务
|
||||
backupService := NewBackupService(configService, databaseService, logger)
|
||||
|
||||
// 使用新的配置通知系统设置热键配置变更监听
|
||||
err = configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error {
|
||||
return hotkeyService.UpdateHotkey(enable, hotkey)
|
||||
@@ -97,12 +107,28 @@ func NewServiceManager() *ServiceManager {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 设置备份配置变更监听,处理备份配置变更
|
||||
err = configService.SetBackupConfigChangeCallback(func(config *models.GitBackupConfig) error {
|
||||
return backupService.HandleConfigChange(config)
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 设置窗口吸附配置变更回调
|
||||
err = configService.SetWindowSnapConfigChangeCallback(func(enabled bool, threshold int) error {
|
||||
return windowSnapService.OnWindowSnapConfigChanged(enabled, threshold)
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &ServiceManager{
|
||||
configService: configService,
|
||||
databaseService: databaseService,
|
||||
sqliteService: sqliteService,
|
||||
documentService: documentService,
|
||||
windowService: windowService,
|
||||
windowSnapService: windowSnapService,
|
||||
migrationService: migrationService,
|
||||
systemService: systemService,
|
||||
hotkeyService: hotkeyService,
|
||||
@@ -113,6 +139,8 @@ func NewServiceManager() *ServiceManager {
|
||||
startupService: startupService,
|
||||
selfUpdateService: selfUpdateService,
|
||||
translationService: translationService,
|
||||
themeService: themeService,
|
||||
BackupService: backupService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
@@ -121,10 +149,10 @@ func NewServiceManager() *ServiceManager {
|
||||
func (sm *ServiceManager) GetServices() []application.Service {
|
||||
services := []application.Service{
|
||||
application.NewService(sm.configService),
|
||||
application.NewService(sm.sqliteService),
|
||||
application.NewService(sm.databaseService),
|
||||
application.NewService(sm.documentService),
|
||||
application.NewService(sm.windowService),
|
||||
application.NewService(sm.windowSnapService),
|
||||
application.NewService(sm.keyBindingService),
|
||||
application.NewService(sm.extensionService),
|
||||
application.NewService(sm.migrationService),
|
||||
@@ -135,6 +163,8 @@ func (sm *ServiceManager) GetServices() []application.Service {
|
||||
application.NewService(sm.startupService),
|
||||
application.NewService(sm.selfUpdateService),
|
||||
application.NewService(sm.translationService),
|
||||
application.NewService(sm.themeService),
|
||||
application.NewService(sm.BackupService),
|
||||
}
|
||||
return services
|
||||
}
|
||||
@@ -150,7 +180,7 @@ func (sm *ServiceManager) GetDialogService() *DialogService {
|
||||
}
|
||||
|
||||
// GetLogger 获取日志服务实例
|
||||
func (sm *ServiceManager) GetLogger() *log.Service {
|
||||
func (sm *ServiceManager) GetLogger() *log.LogService {
|
||||
return sm.logger
|
||||
}
|
||||
|
||||
@@ -194,11 +224,6 @@ func (sm *ServiceManager) GetDatabaseService() *DatabaseService {
|
||||
return sm.databaseService
|
||||
}
|
||||
|
||||
// GetSQLiteService 获取SQLite服务实例
|
||||
func (sm *ServiceManager) GetSQLiteService() *sqlite.Service {
|
||||
return sm.sqliteService
|
||||
}
|
||||
|
||||
// GetWindowService 获取窗口服务实例
|
||||
func (sm *ServiceManager) GetWindowService() *WindowService {
|
||||
return sm.windowService
|
||||
@@ -208,3 +233,13 @@ func (sm *ServiceManager) GetWindowService() *WindowService {
|
||||
func (sm *ServiceManager) GetDocumentService() *DocumentService {
|
||||
return sm.documentService
|
||||
}
|
||||
|
||||
// GetThemeService 获取主题服务实例
|
||||
func (sm *ServiceManager) GetThemeService() *ThemeService {
|
||||
return sm.themeService
|
||||
}
|
||||
|
||||
// GetWindowSnapService 获取窗口吸附服务实例
|
||||
func (sm *ServiceManager) GetWindowSnapService() *WindowSnapService {
|
||||
return sm.windowSnapService
|
||||
}
|
||||
@@ -15,14 +15,14 @@ import (
|
||||
|
||||
// DarwinStartupImpl macOS 平台开机启动实现
|
||||
type DarwinStartupImpl struct {
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
disabled bool
|
||||
appPath string
|
||||
appName string
|
||||
}
|
||||
|
||||
// newStartupImplementation 创建平台特定的开机启动实现
|
||||
func newStartupImplementation(logger *log.Service) StartupImplementation {
|
||||
func newStartupImplementation(logger *log.LogService) StartupImplementation {
|
||||
return &DarwinStartupImpl{
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
// LinuxStartupImpl Linux 平台开机启动实现
|
||||
type LinuxStartupImpl struct {
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
autostartDir string
|
||||
execPath string
|
||||
appName string
|
||||
@@ -37,7 +37,7 @@ X-GNOME-Autostart-enabled=true
|
||||
`
|
||||
|
||||
// newStartupImplementation 创建平台特定的开机启动实现
|
||||
func newStartupImplementation(logger *log.Service) StartupImplementation {
|
||||
func newStartupImplementation(logger *log.LogService) StartupImplementation {
|
||||
return &LinuxStartupImpl{
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
// StartupService 开机启动服务
|
||||
type StartupService struct {
|
||||
configService *ConfigService
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
impl StartupImplementation
|
||||
initError error
|
||||
}
|
||||
@@ -19,7 +19,7 @@ type StartupImplementation interface {
|
||||
}
|
||||
|
||||
// NewStartupService 创建开机启动服务实例
|
||||
func NewStartupService(configService *ConfigService, logger *log.Service) *StartupService {
|
||||
func NewStartupService(configService *ConfigService, logger *log.LogService) *StartupService {
|
||||
service := &StartupService{
|
||||
configService: configService,
|
||||
logger: logger,
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
// WindowsStartupImpl Windows 平台开机启动实现
|
||||
type WindowsStartupImpl struct {
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
registryKey string
|
||||
execPath string
|
||||
workingDir string
|
||||
@@ -23,7 +23,7 @@ type WindowsStartupImpl struct {
|
||||
}
|
||||
|
||||
// newStartupImplementation 创建平台特定的开机启动实现
|
||||
func newStartupImplementation(logger *log.Service) StartupImplementation {
|
||||
func newStartupImplementation(logger *log.LogService) StartupImplementation {
|
||||
return &WindowsStartupImpl{
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
type StoreOption struct {
|
||||
FilePath string
|
||||
AutoSave bool
|
||||
Logger *log.Service
|
||||
Logger *log.LogService
|
||||
}
|
||||
|
||||
// Store 泛型存储服务
|
||||
@@ -25,7 +25,7 @@ type Store[T any] struct {
|
||||
dataMap sync.Map // thread-safe map
|
||||
unsaved atomic.Bool
|
||||
initOnce sync.Once
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
}
|
||||
|
||||
// NewStore 存储服务
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
// SystemService 系统监控服务
|
||||
type SystemService struct {
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
}
|
||||
|
||||
// MemoryStats 内存统计信息
|
||||
@@ -29,7 +29,7 @@ type MemoryStats struct {
|
||||
}
|
||||
|
||||
// NewSystemService 创建新的系统服务实例
|
||||
func NewSystemService(logger *log.Service) *SystemService {
|
||||
func NewSystemService(logger *log.LogService) *SystemService {
|
||||
return &SystemService{
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
"voidraft/internal/models"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
)
|
||||
|
||||
// ThemeService 主题服务
|
||||
type ThemeService struct {
|
||||
databaseService *DatabaseService
|
||||
logger *log.LogService
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewThemeService 创建新的主题服务
|
||||
func NewThemeService(databaseService *DatabaseService, logger *log.LogService) *ThemeService {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
}
|
||||
|
||||
return &ThemeService{
|
||||
databaseService: databaseService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceStartup 服务启动时初始化
|
||||
func (ts *ThemeService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||
ts.ctx = ctx
|
||||
|
||||
// 初始化默认主题
|
||||
return ts.initializeDefaultThemes()
|
||||
}
|
||||
|
||||
// getDB 获取数据库连接
|
||||
func (ts *ThemeService) getDB() *sql.DB {
|
||||
return ts.databaseService.db
|
||||
}
|
||||
|
||||
// initializeDefaultThemes 初始化默认主题
|
||||
func (ts *ThemeService) initializeDefaultThemes() error {
|
||||
db := ts.getDB()
|
||||
if db == nil {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
// 检查是否已存在默认主题
|
||||
var count int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM themes WHERE is_default = 1").Scan(&count)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check existing themes: %w", err)
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return nil // 默认主题已存在
|
||||
}
|
||||
|
||||
// 创建默认深色主题
|
||||
darkTheme := &models.Theme{
|
||||
Name: "Default Dark",
|
||||
Type: models.ThemeTypeDark,
|
||||
Colors: *models.NewDefaultDarkTheme(),
|
||||
IsDefault: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 创建默认浅色主题
|
||||
lightTheme := &models.Theme{
|
||||
Name: "Default Light",
|
||||
Type: models.ThemeTypeLight,
|
||||
Colors: *models.NewDefaultLightTheme(),
|
||||
IsDefault: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 插入默认主题
|
||||
if _, err := ts.CreateTheme(darkTheme); err != nil {
|
||||
return fmt.Errorf("failed to create default dark theme: %w", err)
|
||||
}
|
||||
|
||||
if _, err := ts.CreateTheme(lightTheme); err != nil {
|
||||
return fmt.Errorf("failed to create default light theme: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDefaultThemes 获取默认主题
|
||||
func (ts *ThemeService) GetDefaultThemes() (map[models.ThemeType]*models.Theme, error) {
|
||||
query := `
|
||||
SELECT id, name, type, colors, is_default, created_at, updated_at
|
||||
FROM themes
|
||||
WHERE is_default = 1
|
||||
ORDER BY type
|
||||
`
|
||||
|
||||
db := ts.getDB()
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query default themes: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
themes := make(map[models.ThemeType]*models.Theme)
|
||||
for rows.Next() {
|
||||
theme := &models.Theme{}
|
||||
err := rows.Scan(
|
||||
&theme.ID,
|
||||
&theme.Name,
|
||||
&theme.Type,
|
||||
&theme.Colors,
|
||||
&theme.IsDefault,
|
||||
&theme.CreatedAt,
|
||||
&theme.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan theme: %w", err)
|
||||
}
|
||||
themes[theme.Type] = theme
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to iterate themes: %w", err)
|
||||
}
|
||||
|
||||
return themes, nil
|
||||
}
|
||||
|
||||
// GetThemeByType 根据类型获取默认主题
|
||||
func (ts *ThemeService) GetThemeByType(themeType models.ThemeType) (*models.Theme, error) {
|
||||
query := `
|
||||
SELECT id, name, type, colors, is_default, created_at, updated_at
|
||||
FROM themes
|
||||
WHERE type = ? AND is_default = 1
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
theme := &models.Theme{}
|
||||
db := ts.getDB()
|
||||
err := db.QueryRow(query, themeType).Scan(
|
||||
&theme.ID,
|
||||
&theme.Name,
|
||||
&theme.Type,
|
||||
&theme.Colors,
|
||||
&theme.IsDefault,
|
||||
&theme.CreatedAt,
|
||||
&theme.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("no default theme found for type: %s", themeType)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get theme by type: %w", err)
|
||||
}
|
||||
|
||||
return theme, nil
|
||||
}
|
||||
|
||||
// UpdateThemeColors 更新主题颜色
|
||||
func (ts *ThemeService) UpdateThemeColors(themeType models.ThemeType, colors models.ThemeColorConfig) error {
|
||||
query := `
|
||||
UPDATE themes
|
||||
SET colors = ?, updated_at = ?
|
||||
WHERE type = ? AND is_default = 1
|
||||
`
|
||||
|
||||
db := ts.getDB()
|
||||
_, err := db.Exec(query, colors, time.Now(), themeType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update theme colors: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetThemeColors 重置主题颜色为默认值
|
||||
func (ts *ThemeService) ResetThemeColors(themeType models.ThemeType) error {
|
||||
var defaultColors models.ThemeColorConfig
|
||||
|
||||
switch themeType {
|
||||
case models.ThemeTypeDark:
|
||||
defaultColors = *models.NewDefaultDarkTheme()
|
||||
case models.ThemeTypeLight:
|
||||
defaultColors = *models.NewDefaultLightTheme()
|
||||
default:
|
||||
return fmt.Errorf("unknown theme type: %s", themeType)
|
||||
}
|
||||
|
||||
return ts.UpdateThemeColors(themeType, defaultColors)
|
||||
}
|
||||
|
||||
// CreateTheme 创建新主题
|
||||
func (ts *ThemeService) CreateTheme(theme *models.Theme) (*models.Theme, error) {
|
||||
query := `
|
||||
INSERT INTO themes (name, type, colors, is_default, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
db := ts.getDB()
|
||||
result, err := db.Exec(
|
||||
query,
|
||||
theme.Name,
|
||||
theme.Type,
|
||||
theme.Colors,
|
||||
theme.IsDefault,
|
||||
theme.CreatedAt,
|
||||
theme.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create theme: %w", err)
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get theme ID: %w", err)
|
||||
}
|
||||
|
||||
theme.ID = int(id)
|
||||
return theme, nil
|
||||
}
|
||||
|
||||
// GetAllThemes 获取所有主题
|
||||
func (ts *ThemeService) GetAllThemes() ([]*models.Theme, error) {
|
||||
query := `
|
||||
SELECT id, name, type, colors, is_default, created_at, updated_at
|
||||
FROM themes
|
||||
ORDER BY is_default DESC, created_at ASC
|
||||
`
|
||||
|
||||
db := ts.getDB()
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query themes: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var themes []*models.Theme
|
||||
for rows.Next() {
|
||||
theme := &models.Theme{}
|
||||
err := rows.Scan(
|
||||
&theme.ID,
|
||||
&theme.Name,
|
||||
&theme.Type,
|
||||
&theme.Colors,
|
||||
&theme.IsDefault,
|
||||
&theme.CreatedAt,
|
||||
&theme.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan theme: %w", err)
|
||||
}
|
||||
themes = append(themes, theme)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to iterate themes: %w", err)
|
||||
}
|
||||
|
||||
return themes, nil
|
||||
}
|
||||
|
||||
// ServiceShutdown 服务关闭
|
||||
func (ts *ThemeService) ServiceShutdown() error {
|
||||
return nil
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
// TranslationService 翻译服务
|
||||
type TranslationService struct {
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
factory *translator.TranslatorFactory
|
||||
defaultTimeout time.Duration
|
||||
translators map[translator.TranslatorType]translator.Translator
|
||||
@@ -18,7 +18,7 @@ type TranslationService struct {
|
||||
}
|
||||
|
||||
// NewTranslationService 创建翻译服务实例
|
||||
func NewTranslationService(logger *log.Service) *TranslationService {
|
||||
func NewTranslationService(logger *log.LogService) *TranslationService {
|
||||
service := &TranslationService{
|
||||
logger: logger,
|
||||
factory: translator.NewTranslatorFactory(),
|
||||
|
||||
@@ -7,14 +7,14 @@ import (
|
||||
|
||||
// TrayService 系统托盘服务
|
||||
type TrayService struct {
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
configService *ConfigService
|
||||
app *application.App
|
||||
mainWindow *application.WebviewWindow
|
||||
}
|
||||
|
||||
// NewTrayService 创建新的系统托盘服务实例
|
||||
func NewTrayService(logger *log.Service, configService *ConfigService) *TrayService {
|
||||
func NewTrayService(logger *log.LogService, configService *ConfigService) *TrayService {
|
||||
return &TrayService{
|
||||
logger: logger,
|
||||
configService: configService,
|
||||
@@ -42,7 +42,6 @@ func (ts *TrayService) HandleWindowClose() {
|
||||
if ts.ShouldMinimizeToTray() {
|
||||
// 隐藏到托盘
|
||||
ts.mainWindow.Hide()
|
||||
ts.app.Event.Emit("window:hidden", nil)
|
||||
} else {
|
||||
// 直接退出应用
|
||||
ts.app.Quit()
|
||||
@@ -54,7 +53,6 @@ func (ts *TrayService) HandleWindowMinimize() {
|
||||
if ts.ShouldMinimizeToTray() {
|
||||
// 隐藏到托盘
|
||||
ts.mainWindow.Hide()
|
||||
ts.app.Event.Emit("window:hidden", nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +62,6 @@ func (ts *TrayService) ShowWindow() {
|
||||
ts.mainWindow.Show()
|
||||
ts.mainWindow.Restore()
|
||||
ts.mainWindow.Focus()
|
||||
if ts.app != nil {
|
||||
ts.app.Event.Emit("window:shown", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,25 +9,28 @@ import (
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
)
|
||||
|
||||
// WindowInfo 窗口信息
|
||||
// WindowInfo 窗口信息(简化版)
|
||||
type WindowInfo struct {
|
||||
Window *application.WebviewWindow
|
||||
DocumentID int64
|
||||
Title string
|
||||
}
|
||||
|
||||
// WindowService 窗口管理服务
|
||||
// WindowService 窗口管理服务(专注于窗口生命周期管理)
|
||||
type WindowService struct {
|
||||
logger *log.Service
|
||||
logger *log.LogService
|
||||
documentService *DocumentService
|
||||
app *application.App
|
||||
mainWindow *application.WebviewWindow
|
||||
windows map[int64]*WindowInfo // documentID -> WindowInfo
|
||||
mu sync.RWMutex
|
||||
|
||||
// 吸附服务引用
|
||||
windowSnapService *WindowSnapService
|
||||
}
|
||||
|
||||
// NewWindowService 创建新的窗口服务实例
|
||||
func NewWindowService(logger *log.Service, documentService *DocumentService) *WindowService {
|
||||
func NewWindowService(logger *log.LogService, documentService *DocumentService) *WindowService {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
}
|
||||
@@ -39,10 +42,20 @@ func NewWindowService(logger *log.Service, documentService *DocumentService) *Wi
|
||||
}
|
||||
}
|
||||
|
||||
// SetWindowSnapService 设置窗口吸附服务引用
|
||||
func (ws *WindowService) SetWindowSnapService(snapService *WindowSnapService) {
|
||||
ws.windowSnapService = snapService
|
||||
}
|
||||
|
||||
// SetAppReferences 设置应用和主窗口引用
|
||||
func (ws *WindowService) SetAppReferences(app *application.App, mainWindow *application.WebviewWindow) {
|
||||
ws.app = app
|
||||
ws.mainWindow = mainWindow
|
||||
|
||||
// 如果吸附服务已设置,也为它设置引用
|
||||
if ws.windowSnapService != nil {
|
||||
ws.windowSnapService.SetAppReferences(app, mainWindow)
|
||||
}
|
||||
}
|
||||
|
||||
// OpenDocumentWindow 为指定文档ID打开新窗口
|
||||
@@ -101,9 +114,14 @@ func (ws *WindowService) OpenDocumentWindow(documentID int64) error {
|
||||
}
|
||||
ws.windows[documentID] = windowInfo
|
||||
|
||||
// 注册窗口关闭事件
|
||||
// 注册窗口事件
|
||||
ws.registerWindowEvents(newWindow, documentID)
|
||||
|
||||
// 向吸附服务注册新窗口
|
||||
if ws.windowSnapService != nil {
|
||||
ws.windowSnapService.RegisterWindow(documentID, newWindow, doc.Title)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -119,12 +137,17 @@ func (ws *WindowService) registerWindowEvents(window *application.WebviewWindow,
|
||||
func (ws *WindowService) onWindowClosing(documentID int64) {
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
|
||||
windowInfo, exists := ws.windows[documentID]
|
||||
if exists {
|
||||
windowInfo.Window.Close()
|
||||
delete(ws.windows, documentID)
|
||||
}
|
||||
|
||||
// 从吸附服务中取消注册
|
||||
if ws.windowSnapService != nil {
|
||||
ws.windowSnapService.UnregisterWindow(documentID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetOpenWindows 获取所有打开的窗口信息
|
||||
@@ -147,3 +170,18 @@ func (ws *WindowService) IsDocumentWindowOpen(documentID int64) bool {
|
||||
_, exists := ws.windows[documentID]
|
||||
return exists
|
||||
}
|
||||
|
||||
// ServiceShutdown 实现服务关闭接口
|
||||
func (ws *WindowService) ServiceShutdown() error {
|
||||
// 关闭所有窗口
|
||||
ws.mu.Lock()
|
||||
defer ws.mu.Unlock()
|
||||
|
||||
for documentID := range ws.windows {
|
||||
if ws.windowSnapService != nil {
|
||||
ws.windowSnapService.UnregisterWindow(documentID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,603 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
"voidraft/internal/models"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
)
|
||||
|
||||
// WindowSnapService 窗口吸附服务
|
||||
type WindowSnapService struct {
|
||||
logger *log.LogService
|
||||
configService *ConfigService
|
||||
app *application.App
|
||||
mainWindow *application.WebviewWindow
|
||||
mu sync.RWMutex
|
||||
|
||||
// 吸附配置
|
||||
snapThreshold int // 吸附触发的阈值距离(像素)
|
||||
snapEnabled bool // 是否启用窗口吸附功能
|
||||
|
||||
// 定时器控制
|
||||
snapTicker *time.Ticker
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
// 性能优化相关
|
||||
lastMainWindowPos models.WindowPosition // 缓存主窗口上次位置
|
||||
changedWindows map[int64]bool // 记录哪些窗口发生了变化
|
||||
skipFrames int // 跳帧计数器,用于降低检测频率
|
||||
|
||||
// 监听的窗口列表
|
||||
managedWindows map[int64]*SnapWindowInfo // documentID -> SnapWindowInfo
|
||||
}
|
||||
|
||||
// SnapWindowInfo 吸附窗口信息
|
||||
type SnapWindowInfo struct {
|
||||
Window *application.WebviewWindow
|
||||
DocumentID int64
|
||||
Title string
|
||||
IsSnapped bool // 是否处于吸附状态
|
||||
SnapOffset models.SnapPosition // 与主窗口的相对位置偏移
|
||||
SnapEdge models.SnapEdge // 吸附的边缘类型
|
||||
LastPos models.WindowPosition // 上一次记录的窗口位置
|
||||
MoveTime time.Time // 上次移动时间,用于判断移动速度
|
||||
}
|
||||
|
||||
// NewWindowSnapService 创建新的窗口吸附服务实例
|
||||
func NewWindowSnapService(logger *log.LogService, configService *ConfigService) *WindowSnapService {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
}
|
||||
|
||||
// 从配置获取窗口吸附设置
|
||||
config, err := configService.GetConfig()
|
||||
snapEnabled := true // 默认启用
|
||||
snapThreshold := 15 // 默认阈值
|
||||
|
||||
if err == nil {
|
||||
snapEnabled = config.General.EnableWindowSnap
|
||||
snapThreshold = config.General.SnapThreshold
|
||||
}
|
||||
|
||||
return &WindowSnapService{
|
||||
logger: logger,
|
||||
configService: configService,
|
||||
snapThreshold: snapThreshold,
|
||||
snapEnabled: snapEnabled,
|
||||
managedWindows: make(map[int64]*SnapWindowInfo),
|
||||
changedWindows: make(map[int64]bool),
|
||||
skipFrames: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// SetAppReferences 设置应用和主窗口引用
|
||||
func (wss *WindowSnapService) SetAppReferences(app *application.App, mainWindow *application.WebviewWindow) {
|
||||
wss.app = app
|
||||
wss.mainWindow = mainWindow
|
||||
|
||||
// 初始化上下文,用于控制goroutine的生命周期
|
||||
wss.ctx, wss.cancel = context.WithCancel(context.Background())
|
||||
|
||||
// 启动窗口吸附监听器
|
||||
wss.StartWindowSnapMonitor()
|
||||
}
|
||||
|
||||
// RegisterWindow 注册需要吸附管理的窗口
|
||||
func (wss *WindowSnapService) RegisterWindow(documentID int64, window *application.WebviewWindow, title string) {
|
||||
wss.mu.Lock()
|
||||
defer wss.mu.Unlock()
|
||||
|
||||
wss.managedWindows[documentID] = &SnapWindowInfo{
|
||||
Window: window,
|
||||
DocumentID: documentID,
|
||||
Title: title,
|
||||
IsSnapped: false,
|
||||
SnapOffset: models.SnapPosition{X: 0, Y: 0},
|
||||
SnapEdge: models.SnapEdgeNone,
|
||||
LastPos: models.WindowPosition{X: 0, Y: 0},
|
||||
MoveTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// UnregisterWindow 取消注册窗口
|
||||
func (wss *WindowSnapService) UnregisterWindow(documentID int64) {
|
||||
wss.mu.Lock()
|
||||
defer wss.mu.Unlock()
|
||||
|
||||
delete(wss.managedWindows, documentID)
|
||||
delete(wss.changedWindows, documentID)
|
||||
}
|
||||
|
||||
// SetSnapEnabled 设置是否启用窗口吸附
|
||||
func (wss *WindowSnapService) SetSnapEnabled(enabled bool) {
|
||||
wss.mu.Lock()
|
||||
defer wss.mu.Unlock()
|
||||
|
||||
if wss.snapEnabled == enabled {
|
||||
return
|
||||
}
|
||||
|
||||
wss.snapEnabled = enabled
|
||||
|
||||
// 如果禁用吸附,解除所有吸附窗口
|
||||
if !enabled {
|
||||
for _, windowInfo := range wss.managedWindows {
|
||||
if windowInfo.IsSnapped {
|
||||
windowInfo.IsSnapped = false
|
||||
windowInfo.SnapEdge = models.SnapEdgeNone
|
||||
}
|
||||
}
|
||||
// 停止定时器
|
||||
if wss.snapTicker != nil {
|
||||
wss.snapTicker.Stop()
|
||||
wss.snapTicker = nil
|
||||
}
|
||||
} else if wss.snapTicker == nil && wss.app != nil {
|
||||
// 重新启动定时器
|
||||
wss.StartWindowSnapMonitor()
|
||||
}
|
||||
}
|
||||
|
||||
// SetSnapThreshold 设置窗口吸附阈值
|
||||
func (wss *WindowSnapService) SetSnapThreshold(threshold int) {
|
||||
wss.mu.Lock()
|
||||
defer wss.mu.Unlock()
|
||||
|
||||
if threshold <= 0 || wss.snapThreshold == threshold {
|
||||
return
|
||||
}
|
||||
|
||||
wss.snapThreshold = threshold
|
||||
}
|
||||
|
||||
// OnWindowSnapConfigChanged 处理窗口吸附配置变更
|
||||
func (wss *WindowSnapService) OnWindowSnapConfigChanged(enabled bool, threshold int) error {
|
||||
wss.SetSnapEnabled(enabled)
|
||||
wss.SetSnapThreshold(threshold)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartWindowSnapMonitor 启动窗口吸附监听器
|
||||
func (wss *WindowSnapService) StartWindowSnapMonitor() {
|
||||
// 如果定时器已存在,先停止它
|
||||
if wss.snapTicker != nil {
|
||||
wss.snapTicker.Stop()
|
||||
}
|
||||
|
||||
// 只有在吸附功能启用时才启动监听器
|
||||
if !wss.snapEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
// 创建新的定时器,20fps 以优化性能
|
||||
wss.snapTicker = time.NewTicker(50 * time.Millisecond)
|
||||
|
||||
// 启动goroutine持续监听窗口位置
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-wss.snapTicker.C:
|
||||
wss.checkAndApplySnapping()
|
||||
case <-wss.ctx.Done():
|
||||
// 上下文取消时停止goroutine
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// checkAndApplySnapping 检测并应用窗口吸附(性能优化版本)
|
||||
func (wss *WindowSnapService) checkAndApplySnapping() {
|
||||
if !wss.snapEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
// 性能优化:每3帧执行一次完整检测,其他时间只处理变化的窗口
|
||||
wss.skipFrames++
|
||||
fullCheck := wss.skipFrames%3 == 0
|
||||
|
||||
wss.mu.Lock()
|
||||
defer wss.mu.Unlock()
|
||||
|
||||
// 检查主窗口是否存在且可见
|
||||
if wss.mainWindow == nil || !wss.isMainWindowAvailable() {
|
||||
// 主窗口不可用,解除所有吸附
|
||||
for _, windowInfo := range wss.managedWindows {
|
||||
if windowInfo.IsSnapped {
|
||||
windowInfo.IsSnapped = false
|
||||
windowInfo.SnapEdge = models.SnapEdgeNone
|
||||
}
|
||||
}
|
||||
// 清空变化记录
|
||||
wss.changedWindows = make(map[int64]bool)
|
||||
return
|
||||
}
|
||||
|
||||
mainPos, _ := wss.getWindowPosition(wss.mainWindow)
|
||||
|
||||
// 检查主窗口是否移动了
|
||||
mainWindowMoved := mainPos.X != wss.lastMainWindowPos.X || mainPos.Y != wss.lastMainWindowPos.Y
|
||||
if mainWindowMoved {
|
||||
wss.lastMainWindowPos = mainPos
|
||||
// 主窗口移动了,标记所有吸附的窗口为变化
|
||||
for documentID, windowInfo := range wss.managedWindows {
|
||||
if windowInfo.IsSnapped {
|
||||
wss.changedWindows[documentID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for documentID, windowInfo := range wss.managedWindows {
|
||||
currentPos, _ := wss.getWindowPosition(windowInfo.Window)
|
||||
|
||||
// 检查窗口是否移动了
|
||||
hasMoved := currentPos.X != windowInfo.LastPos.X || currentPos.Y != windowInfo.LastPos.Y
|
||||
if hasMoved {
|
||||
windowInfo.MoveTime = time.Now()
|
||||
windowInfo.LastPos = currentPos
|
||||
wss.changedWindows[documentID] = true
|
||||
}
|
||||
|
||||
// 性能优化:只处理变化的窗口或进行完整检查时
|
||||
if !fullCheck && !wss.changedWindows[documentID] {
|
||||
continue
|
||||
}
|
||||
|
||||
if windowInfo.IsSnapped {
|
||||
// 窗口已吸附,检查是否需要更新位置或解除吸附
|
||||
wss.handleSnappedWindow(windowInfo, mainPos, currentPos)
|
||||
} else {
|
||||
// 窗口未吸附,检查是否应该吸附
|
||||
wss.handleUnsnappedWindow(windowInfo)
|
||||
}
|
||||
|
||||
// 处理完成后清除变化标记
|
||||
delete(wss.changedWindows, documentID)
|
||||
}
|
||||
}
|
||||
|
||||
// isMainWindowAvailable 检查主窗口是否可用
|
||||
func (wss *WindowSnapService) isMainWindowAvailable() bool {
|
||||
if wss.mainWindow == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查主窗口是否可见和正常状态
|
||||
return wss.mainWindow.IsVisible()
|
||||
}
|
||||
|
||||
// handleSnappedWindow 处理已吸附的窗口
|
||||
func (wss *WindowSnapService) handleSnappedWindow(windowInfo *SnapWindowInfo, mainPos models.WindowPosition, currentPos models.WindowPosition) {
|
||||
// 计算预期位置基于主窗口的新位置
|
||||
expectedX := mainPos.X + windowInfo.SnapOffset.X
|
||||
expectedY := mainPos.Y + windowInfo.SnapOffset.Y
|
||||
|
||||
// 计算当前位置与预期位置的距离
|
||||
distanceX := math.Abs(float64(currentPos.X - expectedX))
|
||||
distanceY := math.Abs(float64(currentPos.Y - expectedY))
|
||||
maxDistance := math.Max(distanceX, distanceY)
|
||||
|
||||
// 检测是否为用户主动拖拽:如果窗口移动幅度超过阈值且是最近移动的
|
||||
userDragThreshold := float64(wss.snapThreshold)
|
||||
isUserDrag := maxDistance > userDragThreshold && time.Since(windowInfo.MoveTime) < 100*time.Millisecond
|
||||
|
||||
if isUserDrag {
|
||||
// 用户主动拖拽,立即解除吸附
|
||||
windowInfo.IsSnapped = false
|
||||
windowInfo.SnapEdge = models.SnapEdgeNone
|
||||
return
|
||||
}
|
||||
|
||||
// 对于主窗口移动导致的位置变化,立即跟随且不解除吸附
|
||||
if maxDistance > 0 {
|
||||
// 直接调整到预期位置,不使用平滑移动以提高响应速度
|
||||
windowInfo.Window.SetPosition(expectedX, expectedY)
|
||||
windowInfo.LastPos = models.WindowPosition{X: expectedX, Y: expectedY}
|
||||
}
|
||||
}
|
||||
|
||||
// handleUnsnappedWindow 处理未吸附的窗口
|
||||
func (wss *WindowSnapService) handleUnsnappedWindow(windowInfo *SnapWindowInfo) {
|
||||
// 检查是否应该吸附到主窗口
|
||||
should, snapEdge := wss.shouldSnapToMainWindow(windowInfo)
|
||||
if should {
|
||||
// 获取主窗口位置用于计算偏移量
|
||||
mainPos, _ := wss.getWindowPosition(wss.mainWindow)
|
||||
|
||||
// 设置吸附状态
|
||||
windowInfo.IsSnapped = true
|
||||
windowInfo.SnapEdge = snapEdge
|
||||
|
||||
// 执行即时吸附,产生明显的吸附效果
|
||||
wss.snapWindowToMainWindow(windowInfo, snapEdge)
|
||||
|
||||
// 重新获取吸附后的位置来计算偏移量
|
||||
newPos, _ := wss.getWindowPosition(windowInfo.Window)
|
||||
windowInfo.SnapOffset.X = newPos.X - mainPos.X
|
||||
windowInfo.SnapOffset.Y = newPos.Y - mainPos.Y
|
||||
windowInfo.LastPos = newPos
|
||||
}
|
||||
}
|
||||
|
||||
// getWindowPosition 获取窗口的位置
|
||||
func (wss *WindowSnapService) getWindowPosition(window *application.WebviewWindow) (models.WindowPosition, bool) {
|
||||
x, y := window.Position()
|
||||
return models.WindowPosition{X: x, Y: y}, true
|
||||
}
|
||||
|
||||
// shouldSnapToMainWindow 检查窗口是否应该吸附到主窗口(支持角落吸附)
|
||||
func (wss *WindowSnapService) shouldSnapToMainWindow(windowInfo *SnapWindowInfo) (bool, models.SnapEdge) {
|
||||
// 降低防抖时间,提高吸附响应速度
|
||||
if time.Since(windowInfo.MoveTime) < 50*time.Millisecond {
|
||||
return false, models.SnapEdgeNone
|
||||
}
|
||||
|
||||
// 获取两个窗口的位置
|
||||
mainPos, _ := wss.getWindowPosition(wss.mainWindow)
|
||||
windowPos, _ := wss.getWindowPosition(windowInfo.Window)
|
||||
|
||||
// 获取主窗口尺寸
|
||||
mainWidth, mainHeight := wss.mainWindow.Size()
|
||||
|
||||
// 获取子窗口尺寸
|
||||
windowWidth, windowHeight := windowInfo.Window.Size()
|
||||
|
||||
// 计算各个边缘的距离
|
||||
threshold := float64(wss.snapThreshold)
|
||||
cornerThreshold := threshold * 1.5 // 角落吸附需要更大的阈值
|
||||
|
||||
// 主窗口的四个边界
|
||||
mainLeft := mainPos.X
|
||||
mainTop := mainPos.Y
|
||||
mainRight := mainPos.X + mainWidth
|
||||
mainBottom := mainPos.Y + mainHeight
|
||||
|
||||
// 子窗口的四个边界
|
||||
windowLeft := windowPos.X
|
||||
windowTop := windowPos.Y
|
||||
windowRight := windowPos.X + windowWidth
|
||||
windowBottom := windowPos.Y + windowHeight
|
||||
|
||||
// 存储每个边缘的吸附信息
|
||||
type snapCandidate struct {
|
||||
edge models.SnapEdge
|
||||
distance float64
|
||||
overlap float64 // 重叠度,用于优先级判断
|
||||
isCorner bool // 是否为角落吸附
|
||||
}
|
||||
|
||||
var candidates []snapCandidate
|
||||
|
||||
// ==== 检查角落吸附(优先级最高) ====
|
||||
|
||||
// 1. 右上角 (SnapEdgeTopRight)
|
||||
distToTopRight := math.Sqrt(math.Pow(float64(mainRight-windowLeft), 2) + math.Pow(float64(mainTop-windowBottom), 2))
|
||||
if distToTopRight <= cornerThreshold {
|
||||
// 检查是否接近右上角区域
|
||||
horizDist := math.Abs(float64(mainRight - windowLeft))
|
||||
vertDist := math.Abs(float64(mainTop - windowBottom))
|
||||
if horizDist <= cornerThreshold && vertDist <= cornerThreshold {
|
||||
candidates = append(candidates, snapCandidate{models.SnapEdgeTopRight, distToTopRight, 100, true})
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 右下角 (SnapEdgeBottomRight)
|
||||
distToBottomRight := math.Sqrt(math.Pow(float64(mainRight-windowLeft), 2) + math.Pow(float64(mainBottom-windowTop), 2))
|
||||
if distToBottomRight <= cornerThreshold {
|
||||
horizDist := math.Abs(float64(mainRight - windowLeft))
|
||||
vertDist := math.Abs(float64(mainBottom - windowTop))
|
||||
if horizDist <= cornerThreshold && vertDist <= cornerThreshold {
|
||||
candidates = append(candidates, snapCandidate{models.SnapEdgeBottomRight, distToBottomRight, 100, true})
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 左下角 (SnapEdgeBottomLeft)
|
||||
distToBottomLeft := math.Sqrt(math.Pow(float64(mainLeft-windowRight), 2) + math.Pow(float64(mainBottom-windowTop), 2))
|
||||
if distToBottomLeft <= cornerThreshold {
|
||||
horizDist := math.Abs(float64(mainLeft - windowRight))
|
||||
vertDist := math.Abs(float64(mainBottom - windowTop))
|
||||
if horizDist <= cornerThreshold && vertDist <= cornerThreshold {
|
||||
candidates = append(candidates, snapCandidate{models.SnapEdgeBottomLeft, distToBottomLeft, 100, true})
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 左上角 (SnapEdgeTopLeft)
|
||||
distToTopLeft := math.Sqrt(math.Pow(float64(mainLeft-windowRight), 2) + math.Pow(float64(mainTop-windowBottom), 2))
|
||||
if distToTopLeft <= cornerThreshold {
|
||||
horizDist := math.Abs(float64(mainLeft - windowRight))
|
||||
vertDist := math.Abs(float64(mainTop - windowBottom))
|
||||
if horizDist <= cornerThreshold && vertDist <= cornerThreshold {
|
||||
candidates = append(candidates, snapCandidate{models.SnapEdgeTopLeft, distToTopLeft, 100, true})
|
||||
}
|
||||
}
|
||||
|
||||
// ==== 检查边缘吸附(只在没有角落吸附时检查) ====
|
||||
if len(candidates) == 0 {
|
||||
// 1. 吸附到主窗口右侧
|
||||
distToRight := math.Abs(float64(mainRight - windowLeft))
|
||||
if distToRight <= threshold {
|
||||
// 计算垂直重叠
|
||||
overlapTop := math.Max(float64(mainTop), float64(windowTop))
|
||||
overlapBottom := math.Min(float64(mainBottom), float64(windowBottom))
|
||||
verticalOverlap := math.Max(0, overlapBottom-overlapTop)
|
||||
candidates = append(candidates, snapCandidate{models.SnapEdgeRight, distToRight, verticalOverlap, false})
|
||||
}
|
||||
|
||||
// 2. 吸附到主窗口左侧
|
||||
distToLeft := math.Abs(float64(mainLeft - windowRight))
|
||||
if distToLeft <= threshold {
|
||||
// 计算垂直重叠
|
||||
overlapTop := math.Max(float64(mainTop), float64(windowTop))
|
||||
overlapBottom := math.Min(float64(mainBottom), float64(windowBottom))
|
||||
verticalOverlap := math.Max(0, overlapBottom-overlapTop)
|
||||
candidates = append(candidates, snapCandidate{models.SnapEdgeLeft, distToLeft, verticalOverlap, false})
|
||||
}
|
||||
|
||||
// 3. 吸附到主窗口底部
|
||||
distToBottom := math.Abs(float64(mainBottom - windowTop))
|
||||
if distToBottom <= threshold {
|
||||
// 计算水平重叠
|
||||
overlapLeft := math.Max(float64(mainLeft), float64(windowLeft))
|
||||
overlapRight := math.Min(float64(mainRight), float64(windowRight))
|
||||
horizontalOverlap := math.Max(0, overlapRight-overlapLeft)
|
||||
candidates = append(candidates, snapCandidate{models.SnapEdgeBottom, distToBottom, horizontalOverlap, false})
|
||||
}
|
||||
|
||||
// 4. 吸附到主窗口顶部
|
||||
distToTop := math.Abs(float64(mainTop - windowBottom))
|
||||
if distToTop <= threshold {
|
||||
// 计算水平重叠
|
||||
overlapLeft := math.Max(float64(mainLeft), float64(windowLeft))
|
||||
overlapRight := math.Min(float64(mainRight), float64(windowRight))
|
||||
horizontalOverlap := math.Max(0, overlapRight-overlapLeft)
|
||||
candidates = append(candidates, snapCandidate{models.SnapEdgeTop, distToTop, horizontalOverlap, false})
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有候选,不吸附
|
||||
if len(candidates) == 0 {
|
||||
return false, models.SnapEdgeNone
|
||||
}
|
||||
|
||||
// 选择最佳吸附位置:角落吸附优先,其次考虑重叠度,最后考虑距离
|
||||
bestCandidate := candidates[0]
|
||||
for _, candidate := range candidates[1:] {
|
||||
// 角落吸附优先级最高
|
||||
if candidate.isCorner && !bestCandidate.isCorner {
|
||||
bestCandidate = candidate
|
||||
} else if bestCandidate.isCorner && !candidate.isCorner {
|
||||
// 继续使用当前的角落吸附
|
||||
continue
|
||||
} else {
|
||||
// 同类型的吸附,比较重叠度和距离
|
||||
if math.Abs(candidate.overlap-bestCandidate.overlap) < 10 {
|
||||
if candidate.distance < bestCandidate.distance {
|
||||
bestCandidate = candidate
|
||||
}
|
||||
} else if candidate.overlap > bestCandidate.overlap {
|
||||
// 重叠度更高的优先
|
||||
bestCandidate = candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true, bestCandidate.edge
|
||||
}
|
||||
|
||||
// snapWindowToMainWindow 将窗口精确吸附到主窗口边缘(支持角落吸附)
|
||||
func (wss *WindowSnapService) snapWindowToMainWindow(windowInfo *SnapWindowInfo, snapEdge models.SnapEdge) {
|
||||
// 获取主窗口位置和尺寸
|
||||
mainPos, _ := wss.getWindowPosition(wss.mainWindow)
|
||||
mainWidth, mainHeight := wss.mainWindow.Size()
|
||||
|
||||
// 获取子窗口位置和尺寸
|
||||
windowPos, _ := wss.getWindowPosition(windowInfo.Window)
|
||||
windowWidth, windowHeight := windowInfo.Window.Size()
|
||||
|
||||
// 计算目标位置
|
||||
var targetX, targetY int
|
||||
|
||||
switch snapEdge {
|
||||
case models.SnapEdgeRight:
|
||||
// 吸附到主窗口右侧
|
||||
targetX = mainPos.X + mainWidth
|
||||
targetY = windowPos.Y // 保持当前 Y 位置
|
||||
// 如果超出主窗口范围,调整到边界
|
||||
if targetY < mainPos.Y {
|
||||
targetY = mainPos.Y
|
||||
} else if targetY+windowHeight > mainPos.Y+mainHeight {
|
||||
targetY = mainPos.Y + mainHeight - windowHeight
|
||||
}
|
||||
|
||||
case models.SnapEdgeLeft:
|
||||
// 吸附到主窗口左侧
|
||||
targetX = mainPos.X - windowWidth
|
||||
targetY = windowPos.Y // 保持当前 Y 位置
|
||||
// 如果超出主窗口范围,调整到边界
|
||||
if targetY < mainPos.Y {
|
||||
targetY = mainPos.Y
|
||||
} else if targetY+windowHeight > mainPos.Y+mainHeight {
|
||||
targetY = mainPos.Y + mainHeight - windowHeight
|
||||
}
|
||||
|
||||
case models.SnapEdgeBottom:
|
||||
// 吸附到主窗口底部
|
||||
targetX = windowPos.X // 保持当前 X 位置
|
||||
targetY = mainPos.Y + mainHeight
|
||||
// 如果超出主窗口范围,调整到边界
|
||||
if targetX < mainPos.X {
|
||||
targetX = mainPos.X
|
||||
} else if targetX+windowWidth > mainPos.X+mainWidth {
|
||||
targetX = mainPos.X + mainWidth - windowWidth
|
||||
}
|
||||
|
||||
case models.SnapEdgeTop:
|
||||
// 吸附到主窗口顶部
|
||||
targetX = windowPos.X // 保持当前 X 位置
|
||||
targetY = mainPos.Y - windowHeight
|
||||
// 如果超出主窗口范围,调整到边界
|
||||
if targetX < mainPos.X {
|
||||
targetX = mainPos.X
|
||||
} else if targetX+windowWidth > mainPos.X+mainWidth {
|
||||
targetX = mainPos.X + mainWidth - windowWidth
|
||||
}
|
||||
|
||||
// ==== 角落吸附 ====
|
||||
case models.SnapEdgeTopRight:
|
||||
// 吸附到右上角
|
||||
targetX = mainPos.X + mainWidth
|
||||
targetY = mainPos.Y - windowHeight
|
||||
|
||||
case models.SnapEdgeBottomRight:
|
||||
// 吸附到右下角
|
||||
targetX = mainPos.X + mainWidth
|
||||
targetY = mainPos.Y + mainHeight
|
||||
|
||||
case models.SnapEdgeBottomLeft:
|
||||
// 吸附到左下角
|
||||
targetX = mainPos.X - windowWidth
|
||||
targetY = mainPos.Y + mainHeight
|
||||
|
||||
case models.SnapEdgeTopLeft:
|
||||
// 吸附到左上角
|
||||
targetX = mainPos.X - windowWidth
|
||||
targetY = mainPos.Y - windowHeight
|
||||
|
||||
default:
|
||||
// 不应该到达这里
|
||||
return
|
||||
}
|
||||
|
||||
// 直接移动到目标位置,不使用平滑过渡以产生明显的吸附效果
|
||||
windowInfo.Window.SetPosition(targetX, targetY)
|
||||
|
||||
// 更新窗口信息
|
||||
windowInfo.SnapEdge = snapEdge
|
||||
windowInfo.LastPos = models.WindowPosition{X: targetX, Y: targetY}
|
||||
}
|
||||
|
||||
// Cleanup 清理资源
|
||||
func (wss *WindowSnapService) Cleanup() {
|
||||
// 如果有取消函数,调用它来停止所有goroutine
|
||||
if wss.cancel != nil {
|
||||
wss.cancel()
|
||||
}
|
||||
|
||||
// 停止定时器
|
||||
if wss.snapTicker != nil {
|
||||
wss.snapTicker.Stop()
|
||||
wss.snapTicker = nil
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceShutdown 实现服务关闭接口
|
||||
func (wss *WindowSnapService) ServiceShutdown() error {
|
||||
wss.Cleanup()
|
||||
return nil
|
||||
}
|
||||
@@ -25,6 +25,7 @@ var assets embed.FS
|
||||
// logs any error that might occur.
|
||||
func main() {
|
||||
serviceManager := services.NewServiceManager()
|
||||
var window *application.WebviewWindow
|
||||
|
||||
var encryptionKey = [32]byte{
|
||||
0x1e, 0x1f, 0x1c, 0x1d, 0x1a, 0x1b, 0x18, 0x19,
|
||||
@@ -51,6 +52,13 @@ func main() {
|
||||
SingleInstance: &application.SingleInstanceOptions{
|
||||
UniqueID: "com.voidraft",
|
||||
EncryptionKey: encryptionKey,
|
||||
OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
|
||||
if window != nil {
|
||||
window.Show()
|
||||
window.Restore()
|
||||
window.Focus()
|
||||
}
|
||||
},
|
||||
AdditionalData: map[string]string{
|
||||
"launchtime": time.Now().Local().String(),
|
||||
},
|
||||
@@ -82,6 +90,7 @@ func main() {
|
||||
URL: "/",
|
||||
})
|
||||
mainWindow.Center()
|
||||
window = mainWindow
|
||||
|
||||
// 获取托盘服务并设置应用引用
|
||||
trayService := serviceManager.GetTrayService()
|
||||
@@ -96,7 +105,7 @@ func main() {
|
||||
|
||||
// 初始化热键服务
|
||||
hotkeyService := serviceManager.GetHotkeyService()
|
||||
err := hotkeyService.Initialize(app)
|
||||
err := hotkeyService.Initialize(app, mainWindow)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user