Compare commits
14
Commits
v1.1.0
...
709998ff9c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
709998ff9c | ||
|
|
6adeadeed4 | ||
|
|
7b70a39b23 | ||
|
|
873a3c0e60 | ||
|
|
5b88efcfbe | ||
|
|
f37c659c89 | ||
|
|
9fff7bcfca | ||
|
|
b4b0ad9bba | ||
|
|
6d8fdf62f1 | ||
|
|
9f53d7421d | ||
|
|
80c8ecb4cf | ||
|
|
d10059a82d | ||
|
|
737f83cd5f | ||
|
|
a720a4cfb8 |
No files matched your search
@@ -15,6 +15,7 @@ VoidRaft is a modern developer-focused text editor that allows you to record, or
|
|||||||
- Code formatting - Built-in Prettier support for one-click code beautification
|
- Code formatting - Built-in Prettier support for one-click code beautification
|
||||||
- Block editing mode - Split content into independent code blocks, each with different language settings
|
- Block editing mode - Split content into independent code blocks, each with different language settings
|
||||||
- Multi-window support - edit multiple documents at the same time
|
- Multi-window support - edit multiple documents at the same time
|
||||||
|
- Support for custom themes - Custom editor themes
|
||||||
|
|
||||||
### Modern Interface
|
### Modern Interface
|
||||||
|
|
||||||
@@ -118,10 +119,10 @@ Voidraft/
|
|||||||
| Linux | Planned | Future versions |
|
| Linux | Planned | Future versions |
|
||||||
|
|
||||||
### Planned Features
|
### Planned Features
|
||||||
- [ ] Custom themes - Customize editor themes
|
- ✅ Custom themes - Customize editor themes
|
||||||
- ✅ Multi-window support - Support editing multiple documents simultaneously
|
- ✅ Multi-window support - Support editing multiple documents simultaneously
|
||||||
|
- ✅ Data synchronization - Cloud backup for documents
|
||||||
- [ ] Enhanced clipboard - Monitor and manage clipboard history
|
- [ ] Enhanced clipboard - Monitor and manage clipboard history
|
||||||
- [ ] Data synchronization - Cloud backup for configurations and documents
|
|
||||||
- [ ] Extension system - Support for custom plugins
|
- [ ] Extension system - Support for custom plugins
|
||||||
|
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|||||||
+3
-2
@@ -15,6 +15,7 @@ Voidraft 是一个现代化的开发者专用文本编辑器,让你能够随
|
|||||||
- 代码格式化 - 内置 Prettier 支持,一键美化代码
|
- 代码格式化 - 内置 Prettier 支持,一键美化代码
|
||||||
- 块状编辑模式 - 将内容分割为独立的代码块,每个块可设置不同语言
|
- 块状编辑模式 - 将内容分割为独立的代码块,每个块可设置不同语言
|
||||||
- 支持多窗口 - 同时编辑多个文档
|
- 支持多窗口 - 同时编辑多个文档
|
||||||
|
- 支持自定义主题 - 自定义编辑器主题
|
||||||
|
|
||||||
### 现代化界面
|
### 现代化界面
|
||||||
|
|
||||||
@@ -119,10 +120,10 @@ Voidraft/
|
|||||||
| Linux | 计划中 | 后续版本 |
|
| Linux | 计划中 | 后续版本 |
|
||||||
|
|
||||||
### 计划添加的功能
|
### 计划添加的功能
|
||||||
- [ ] 自定义主题 - 自定义编辑器主题
|
- ✅ 自定义主题 - 自定义编辑器主题
|
||||||
- ✅ 多窗口支持 - 支持同时编辑多个文档
|
- ✅ 多窗口支持 - 支持同时编辑多个文档
|
||||||
|
- ✅ 数据同步 - 文档云端备份
|
||||||
- [ ] 剪切板增强 - 监听和管理剪切板历史
|
- [ ] 剪切板增强 - 监听和管理剪切板历史
|
||||||
- [ ] 数据同步 - 配置和文档云端备份
|
|
||||||
- [ ] 扩展系统 - 支持自定义插件
|
- [ ] 扩展系统 - 支持自定义插件
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
"updates": UpdatesConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Git备份设置
|
||||||
|
*/
|
||||||
|
"backup": GitBackupConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配置元数据
|
* 配置元数据
|
||||||
*/
|
*/
|
||||||
@@ -52,6 +57,9 @@ export class AppConfig {
|
|||||||
if (!("updates" in $$source)) {
|
if (!("updates" in $$source)) {
|
||||||
this["updates"] = (new UpdatesConfig());
|
this["updates"] = (new UpdatesConfig());
|
||||||
}
|
}
|
||||||
|
if (!("backup" in $$source)) {
|
||||||
|
this["backup"] = (new GitBackupConfig());
|
||||||
|
}
|
||||||
if (!("metadata" in $$source)) {
|
if (!("metadata" in $$source)) {
|
||||||
this["metadata"] = (new ConfigMetadata());
|
this["metadata"] = (new ConfigMetadata());
|
||||||
}
|
}
|
||||||
@@ -68,6 +76,7 @@ export class AppConfig {
|
|||||||
const $$createField2_0 = $$createType2;
|
const $$createField2_0 = $$createType2;
|
||||||
const $$createField3_0 = $$createType3;
|
const $$createField3_0 = $$createType3;
|
||||||
const $$createField4_0 = $$createType4;
|
const $$createField4_0 = $$createType4;
|
||||||
|
const $$createField5_0 = $$createType5;
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
if ("general" in $$parsedSource) {
|
if ("general" in $$parsedSource) {
|
||||||
$$parsedSource["general"] = $$createField0_0($$parsedSource["general"]);
|
$$parsedSource["general"] = $$createField0_0($$parsedSource["general"]);
|
||||||
@@ -81,8 +90,11 @@ export class AppConfig {
|
|||||||
if ("updates" in $$parsedSource) {
|
if ("updates" in $$parsedSource) {
|
||||||
$$parsedSource["updates"] = $$createField3_0($$parsedSource["updates"]);
|
$$parsedSource["updates"] = $$createField3_0($$parsedSource["updates"]);
|
||||||
}
|
}
|
||||||
|
if ("backup" in $$parsedSource) {
|
||||||
|
$$parsedSource["backup"] = $$createField4_0($$parsedSource["backup"]);
|
||||||
|
}
|
||||||
if ("metadata" in $$parsedSource) {
|
if ("metadata" in $$parsedSource) {
|
||||||
$$parsedSource["metadata"] = $$createField4_0($$parsedSource["metadata"]);
|
$$parsedSource["metadata"] = $$createField5_0($$parsedSource["metadata"]);
|
||||||
}
|
}
|
||||||
return new AppConfig($$parsedSource as Partial<AppConfig>);
|
return new AppConfig($$parsedSource as Partial<AppConfig>);
|
||||||
}
|
}
|
||||||
@@ -123,6 +135,25 @@ export class 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 配置元数据
|
* ConfigMetadata 配置元数据
|
||||||
*/
|
*/
|
||||||
@@ -169,6 +200,11 @@ export class Document {
|
|||||||
"updatedAt": time$0.Time;
|
"updatedAt": time$0.Time;
|
||||||
"is_deleted": boolean;
|
"is_deleted": boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 锁定标志,锁定的文档无法被删除
|
||||||
|
*/
|
||||||
|
"is_locked": boolean;
|
||||||
|
|
||||||
/** Creates a new Document instance. */
|
/** Creates a new Document instance. */
|
||||||
constructor($$source: Partial<Document> = {}) {
|
constructor($$source: Partial<Document> = {}) {
|
||||||
if (!("id" in $$source)) {
|
if (!("id" in $$source)) {
|
||||||
@@ -189,6 +225,9 @@ export class Document {
|
|||||||
if (!("is_deleted" in $$source)) {
|
if (!("is_deleted" in $$source)) {
|
||||||
this["is_deleted"] = false;
|
this["is_deleted"] = false;
|
||||||
}
|
}
|
||||||
|
if (!("is_locked" in $$source)) {
|
||||||
|
this["is_locked"] = false;
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(this, $$source);
|
Object.assign(this, $$source);
|
||||||
}
|
}
|
||||||
@@ -334,7 +373,7 @@ export class Extension {
|
|||||||
* Creates a new Extension instance from a string or object.
|
* Creates a new Extension instance from a string or object.
|
||||||
*/
|
*/
|
||||||
static createFrom($$source: any = {}): Extension {
|
static createFrom($$source: any = {}): Extension {
|
||||||
const $$createField3_0 = $$createType5;
|
const $$createField3_0 = $$createType6;
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
if ("config" in $$parsedSource) {
|
if ("config" in $$parsedSource) {
|
||||||
$$parsedSource["config"] = $$createField3_0($$parsedSource["config"]);
|
$$parsedSource["config"] = $$createField3_0($$parsedSource["config"]);
|
||||||
@@ -467,7 +506,7 @@ export class GeneralConfig {
|
|||||||
* Creates a new GeneralConfig instance from a string or object.
|
* Creates a new GeneralConfig instance from a string or object.
|
||||||
*/
|
*/
|
||||||
static createFrom($$source: any = {}): GeneralConfig {
|
static createFrom($$source: any = {}): GeneralConfig {
|
||||||
const $$createField5_0 = $$createType7;
|
const $$createField5_0 = $$createType8;
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
if ("globalHotkey" in $$parsedSource) {
|
if ("globalHotkey" in $$parsedSource) {
|
||||||
$$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]);
|
$$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]);
|
||||||
@@ -476,6 +515,55 @@ export class 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配置
|
* GiteaConfig Gitea配置
|
||||||
*/
|
*/
|
||||||
@@ -1028,6 +1116,279 @@ export enum TabType {
|
|||||||
TabTypeTab = "tab",
|
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 主题颜色配置
|
||||||
|
*/
|
||||||
|
export class ThemeColorConfig {
|
||||||
|
/**
|
||||||
|
* 基础色调
|
||||||
|
* 主背景色
|
||||||
|
*/
|
||||||
|
"background": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 次要背景色
|
||||||
|
*/
|
||||||
|
"backgroundSecondary": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 面板背景
|
||||||
|
*/
|
||||||
|
"surface": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主文本色
|
||||||
|
*/
|
||||||
|
"foreground": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 次要文本色
|
||||||
|
*/
|
||||||
|
"foregroundSecondary": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语法高亮
|
||||||
|
* 注释色
|
||||||
|
*/
|
||||||
|
"comment": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关键字
|
||||||
|
*/
|
||||||
|
"keyword": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字符串
|
||||||
|
*/
|
||||||
|
"string": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 函数名
|
||||||
|
*/
|
||||||
|
"function": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数字
|
||||||
|
*/
|
||||||
|
"number": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作符
|
||||||
|
*/
|
||||||
|
"operator": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变量
|
||||||
|
*/
|
||||||
|
"variable": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型
|
||||||
|
*/
|
||||||
|
"type": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 界面元素
|
||||||
|
* 光标
|
||||||
|
*/
|
||||||
|
"cursor": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选中背景
|
||||||
|
*/
|
||||||
|
"selection": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失焦选中背景
|
||||||
|
*/
|
||||||
|
"selectionBlur": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前行高亮
|
||||||
|
*/
|
||||||
|
"activeLine": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 行号
|
||||||
|
*/
|
||||||
|
"lineNumber": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活动行号
|
||||||
|
*/
|
||||||
|
"activeLineNumber": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 边框分割线
|
||||||
|
* 边框色
|
||||||
|
*/
|
||||||
|
"borderColor": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 浅色边框
|
||||||
|
*/
|
||||||
|
"borderLight": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索匹配
|
||||||
|
* 搜索匹配
|
||||||
|
*/
|
||||||
|
"searchMatch": string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匹配括号
|
||||||
|
*/
|
||||||
|
"matchingBracket": string;
|
||||||
|
|
||||||
|
/** Creates a new ThemeColorConfig instance. */
|
||||||
|
constructor($$source: Partial<ThemeColorConfig> = {}) {
|
||||||
|
if (!("background" in $$source)) {
|
||||||
|
this["background"] = "";
|
||||||
|
}
|
||||||
|
if (!("backgroundSecondary" in $$source)) {
|
||||||
|
this["backgroundSecondary"] = "";
|
||||||
|
}
|
||||||
|
if (!("surface" in $$source)) {
|
||||||
|
this["surface"] = "";
|
||||||
|
}
|
||||||
|
if (!("foreground" in $$source)) {
|
||||||
|
this["foreground"] = "";
|
||||||
|
}
|
||||||
|
if (!("foregroundSecondary" in $$source)) {
|
||||||
|
this["foregroundSecondary"] = "";
|
||||||
|
}
|
||||||
|
if (!("comment" in $$source)) {
|
||||||
|
this["comment"] = "";
|
||||||
|
}
|
||||||
|
if (!("keyword" in $$source)) {
|
||||||
|
this["keyword"] = "";
|
||||||
|
}
|
||||||
|
if (!("string" in $$source)) {
|
||||||
|
this["string"] = "";
|
||||||
|
}
|
||||||
|
if (!("function" in $$source)) {
|
||||||
|
this["function"] = "";
|
||||||
|
}
|
||||||
|
if (!("number" in $$source)) {
|
||||||
|
this["number"] = "";
|
||||||
|
}
|
||||||
|
if (!("operator" in $$source)) {
|
||||||
|
this["operator"] = "";
|
||||||
|
}
|
||||||
|
if (!("variable" in $$source)) {
|
||||||
|
this["variable"] = "";
|
||||||
|
}
|
||||||
|
if (!("type" in $$source)) {
|
||||||
|
this["type"] = "";
|
||||||
|
}
|
||||||
|
if (!("cursor" in $$source)) {
|
||||||
|
this["cursor"] = "";
|
||||||
|
}
|
||||||
|
if (!("selection" in $$source)) {
|
||||||
|
this["selection"] = "";
|
||||||
|
}
|
||||||
|
if (!("selectionBlur" in $$source)) {
|
||||||
|
this["selectionBlur"] = "";
|
||||||
|
}
|
||||||
|
if (!("activeLine" in $$source)) {
|
||||||
|
this["activeLine"] = "";
|
||||||
|
}
|
||||||
|
if (!("lineNumber" in $$source)) {
|
||||||
|
this["lineNumber"] = "";
|
||||||
|
}
|
||||||
|
if (!("activeLineNumber" in $$source)) {
|
||||||
|
this["activeLineNumber"] = "";
|
||||||
|
}
|
||||||
|
if (!("borderColor" in $$source)) {
|
||||||
|
this["borderColor"] = "";
|
||||||
|
}
|
||||||
|
if (!("borderLight" in $$source)) {
|
||||||
|
this["borderLight"] = "";
|
||||||
|
}
|
||||||
|
if (!("searchMatch" in $$source)) {
|
||||||
|
this["searchMatch"] = "";
|
||||||
|
}
|
||||||
|
if (!("matchingBracket" in $$source)) {
|
||||||
|
this["matchingBracket"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new ThemeColorConfig instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): ThemeColorConfig {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new ThemeColorConfig($$parsedSource as Partial<ThemeColorConfig>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeType 主题类型枚举
|
||||||
|
*/
|
||||||
|
export enum ThemeType {
|
||||||
|
/**
|
||||||
|
* The Go zero value for the underlying type of the enum.
|
||||||
|
*/
|
||||||
|
$zero = "",
|
||||||
|
|
||||||
|
ThemeTypeDark = "dark",
|
||||||
|
ThemeTypeLight = "light",
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UpdateSourceType 更新源类型
|
* UpdateSourceType 更新源类型
|
||||||
*/
|
*/
|
||||||
@@ -1126,8 +1487,8 @@ export class UpdatesConfig {
|
|||||||
* Creates a new UpdatesConfig instance from a string or object.
|
* Creates a new UpdatesConfig instance from a string or object.
|
||||||
*/
|
*/
|
||||||
static createFrom($$source: any = {}): UpdatesConfig {
|
static createFrom($$source: any = {}): UpdatesConfig {
|
||||||
const $$createField6_0 = $$createType8;
|
const $$createField6_0 = $$createType10;
|
||||||
const $$createField7_0 = $$createType9;
|
const $$createField7_0 = $$createType11;
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
if ("github" in $$parsedSource) {
|
if ("github" in $$parsedSource) {
|
||||||
$$parsedSource["github"] = $$createField6_0($$parsedSource["github"]);
|
$$parsedSource["github"] = $$createField6_0($$parsedSource["github"]);
|
||||||
@@ -1144,14 +1505,16 @@ const $$createType0 = GeneralConfig.createFrom;
|
|||||||
const $$createType1 = EditingConfig.createFrom;
|
const $$createType1 = EditingConfig.createFrom;
|
||||||
const $$createType2 = AppearanceConfig.createFrom;
|
const $$createType2 = AppearanceConfig.createFrom;
|
||||||
const $$createType3 = UpdatesConfig.createFrom;
|
const $$createType3 = UpdatesConfig.createFrom;
|
||||||
const $$createType4 = ConfigMetadata.createFrom;
|
const $$createType4 = GitBackupConfig.createFrom;
|
||||||
var $$createType5 = (function $$initCreateType5(...args): any {
|
const $$createType5 = ConfigMetadata.createFrom;
|
||||||
if ($$createType5 === $$initCreateType5) {
|
var $$createType6 = (function $$initCreateType6(...args): any {
|
||||||
$$createType5 = $$createType6;
|
if ($$createType6 === $$initCreateType6) {
|
||||||
|
$$createType6 = $$createType7;
|
||||||
}
|
}
|
||||||
return $$createType5(...args);
|
return $$createType6(...args);
|
||||||
});
|
});
|
||||||
const $$createType6 = $Create.Map($Create.Any, $Create.Any);
|
const $$createType7 = $Create.Map($Create.Any, $Create.Any);
|
||||||
const $$createType7 = HotkeyCombo.createFrom;
|
const $$createType8 = HotkeyCombo.createFrom;
|
||||||
const $$createType8 = GithubConfig.createFrom;
|
const $$createType9 = ThemeColorConfig.createFrom;
|
||||||
const $$createType9 = GiteaConfig.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;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SetBackupConfigChangeCallback 设置备份配置变更回调
|
||||||
|
*/
|
||||||
|
export function SetBackupConfigChangeCallback(callback: any): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(3264871659, callback) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SetDataPathChangeCallback 设置数据路径配置变更回调
|
* SetDataPathChangeCallback 设置数据路径配置变更回调
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ export function OnDataPathChanged(): Promise<void> & { cancel(): void } {
|
|||||||
return $resultPromise;
|
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
|
* ServiceShutdown shuts down the service when the application closes
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ export function SelectDirectory(): Promise<string> & { cancel(): void } {
|
|||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SelectFile 打开文件选择对话框
|
||||||
|
*/
|
||||||
|
export function SelectFile(): Promise<string> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(37302920) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SetWindow 设置绑定的窗口
|
* SetWindow 设置绑定的窗口
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -81,6 +81,14 @@ export function ListDeletedDocumentsMeta(): Promise<(models$0.Document | null)[]
|
|||||||
return $typingPromise;
|
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
|
* RestoreDocument restores a deleted document
|
||||||
*/
|
*/
|
||||||
@@ -97,6 +105,14 @@ export function ServiceStartup(options: application$0.ServiceOptions): Promise<v
|
|||||||
return $resultPromise;
|
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
|
* UpdateDocumentContent updates the content of a document
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ export function GetCurrentHotkey(): Promise<models$0.HotkeyCombo | null> & { can
|
|||||||
/**
|
/**
|
||||||
* Initialize 初始化热键服务
|
* Initialize 初始化热键服务
|
||||||
*/
|
*/
|
||||||
export function Initialize(app: application$0.App | null): Promise<void> & { cancel(): void } {
|
export function Initialize(app: application$0.App | null, mainWindow: application$0.WebviewWindow | null): Promise<void> & { cancel(): void } {
|
||||||
let $resultPromise = $Call.ByID(3671360458, app) as any;
|
let $resultPromise = $Call.ByID(3671360458, app, mainWindow) as any;
|
||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
import * as BackupService from "./backupservice.js";
|
||||||
import * as ConfigService from "./configservice.js";
|
import * as ConfigService from "./configservice.js";
|
||||||
import * as DatabaseService from "./databaseservice.js";
|
import * as DatabaseService from "./databaseservice.js";
|
||||||
import * as DialogService from "./dialogservice.js";
|
import * as DialogService from "./dialogservice.js";
|
||||||
@@ -12,10 +13,12 @@ import * as MigrationService from "./migrationservice.js";
|
|||||||
import * as SelfUpdateService from "./selfupdateservice.js";
|
import * as SelfUpdateService from "./selfupdateservice.js";
|
||||||
import * as StartupService from "./startupservice.js";
|
import * as StartupService from "./startupservice.js";
|
||||||
import * as SystemService from "./systemservice.js";
|
import * as SystemService from "./systemservice.js";
|
||||||
|
import * as ThemeService from "./themeservice.js";
|
||||||
import * as TranslationService from "./translationservice.js";
|
import * as TranslationService from "./translationservice.js";
|
||||||
import * as TrayService from "./trayservice.js";
|
import * as TrayService from "./trayservice.js";
|
||||||
import * as WindowService from "./windowservice.js";
|
import * as WindowService from "./windowservice.js";
|
||||||
export {
|
export {
|
||||||
|
BackupService,
|
||||||
ConfigService,
|
ConfigService,
|
||||||
DatabaseService,
|
DatabaseService,
|
||||||
DialogService,
|
DialogService,
|
||||||
@@ -27,6 +30,7 @@ export {
|
|||||||
SelfUpdateService,
|
SelfUpdateService,
|
||||||
StartupService,
|
StartupService,
|
||||||
SystemService,
|
SystemService,
|
||||||
|
ThemeService,
|
||||||
TranslationService,
|
TranslationService,
|
||||||
TrayService,
|
TrayService,
|
||||||
WindowService
|
WindowService
|
||||||
|
|||||||
@@ -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);
|
||||||
Vendored
+1
@@ -11,6 +11,7 @@ declare module 'vue' {
|
|||||||
BlockLanguageSelector: typeof import('./src/components/toolbar/BlockLanguageSelector.vue')['default']
|
BlockLanguageSelector: typeof import('./src/components/toolbar/BlockLanguageSelector.vue')['default']
|
||||||
DocumentSelector: typeof import('./src/components/toolbar/DocumentSelector.vue')['default']
|
DocumentSelector: typeof import('./src/components/toolbar/DocumentSelector.vue')['default']
|
||||||
LinuxTitleBar: typeof import('./src/components/titlebar/LinuxTitleBar.vue')['default']
|
LinuxTitleBar: typeof import('./src/components/titlebar/LinuxTitleBar.vue')['default']
|
||||||
|
LoadingScreen: typeof import('./src/components/loading/LoadingScreen.vue')['default']
|
||||||
MacOSTitleBar: typeof import('./src/components/titlebar/MacOSTitleBar.vue')['default']
|
MacOSTitleBar: typeof import('./src/components/titlebar/MacOSTitleBar.vue')['default']
|
||||||
MemoryMonitor: typeof import('./src/components/monitor/MemoryMonitor.vue')['default']
|
MemoryMonitor: typeof import('./src/components/monitor/MemoryMonitor.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
|||||||
Generated
+383
-391
File diff suppressed because it is too large.
Load diff
+20
-20
@@ -24,24 +24,24 @@
|
|||||||
"@codemirror/lang-json": "^6.0.2",
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
"@codemirror/lang-less": "^6.0.2",
|
"@codemirror/lang-less": "^6.0.2",
|
||||||
"@codemirror/lang-lezer": "^6.0.2",
|
"@codemirror/lang-lezer": "^6.0.2",
|
||||||
"@codemirror/lang-liquid": "^6.2.3",
|
"@codemirror/lang-liquid": "^6.3.0",
|
||||||
"@codemirror/lang-markdown": "^6.3.3",
|
"@codemirror/lang-markdown": "^6.3.4",
|
||||||
"@codemirror/lang-php": "^6.0.2",
|
"@codemirror/lang-php": "^6.0.2",
|
||||||
"@codemirror/lang-python": "^6.2.1",
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
"@codemirror/lang-rust": "^6.0.2",
|
"@codemirror/lang-rust": "^6.0.2",
|
||||||
"@codemirror/lang-sass": "^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-vue": "^0.1.3",
|
||||||
"@codemirror/lang-wast": "^6.0.2",
|
"@codemirror/lang-wast": "^6.0.2",
|
||||||
"@codemirror/lang-xml": "^6.1.0",
|
"@codemirror/lang-xml": "^6.1.0",
|
||||||
"@codemirror/lang-yaml": "^6.1.2",
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
"@codemirror/language": "^6.11.2",
|
"@codemirror/language": "^6.11.3",
|
||||||
"@codemirror/language-data": "^6.5.1",
|
"@codemirror/language-data": "^6.5.1",
|
||||||
"@codemirror/legacy-modes": "^6.5.1",
|
"@codemirror/legacy-modes": "^6.5.1",
|
||||||
"@codemirror/lint": "^6.8.5",
|
"@codemirror/lint": "^6.8.5",
|
||||||
"@codemirror/search": "^6.5.11",
|
"@codemirror/search": "^6.5.11",
|
||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/view": "^6.38.0",
|
"@codemirror/view": "^6.38.1",
|
||||||
"@lezer/highlight": "^1.2.1",
|
"@lezer/highlight": "^1.2.1",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
@@ -52,30 +52,30 @@
|
|||||||
"hsl-matcher": "^1.2.4",
|
"hsl-matcher": "^1.2.4",
|
||||||
"lezer": "^0.13.5",
|
"lezer": "^0.13.5",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.4.1",
|
"pinia-plugin-persistedstate": "^4.5.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"remarkable": "^2.0.1",
|
"remarkable": "^2.0.1",
|
||||||
"sass": "^1.89.2",
|
"sass": "^1.90.0",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.18",
|
||||||
"vue-i18n": "^11.1.9",
|
"vue-i18n": "^11.1.11",
|
||||||
|
"vue-pick-colors": "^1.8.0",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.30.1",
|
"@eslint/js": "^9.33.0",
|
||||||
"@lezer/generator": "^1.8.0",
|
"@lezer/generator": "^1.8.0",
|
||||||
"@types/lodash": "^4.17.20",
|
"@types/node": "^24.3.0",
|
||||||
"@types/node": "^24.0.12",
|
|
||||||
"@types/remarkable": "^2.0.8",
|
"@types/remarkable": "^2.0.8",
|
||||||
"@vitejs/plugin-vue": "^6.0.0",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@wailsio/runtime": "latest",
|
"@wailsio/runtime": "latest",
|
||||||
"eslint": "^9.30.1",
|
"eslint": "^9.33.0",
|
||||||
"eslint-plugin-vue": "^10.3.0",
|
"eslint-plugin-vue": "^10.4.0",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.9.2",
|
||||||
"typescript-eslint": "^8.36.0",
|
"typescript-eslint": "^8.39.1",
|
||||||
"unplugin-vue-components": "^28.8.0",
|
"unplugin-vue-components": "^29.0.0",
|
||||||
"vite": "^7.0.3",
|
"vite": "^7.1.2",
|
||||||
"vue-eslint-parser": "^10.2.0",
|
"vue-eslint-parser": "^10.2.0",
|
||||||
"vue-tsc": "^3.0.1"
|
"vue-tsc": "^3.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+12
-7
@@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue';
|
import {onMounted} from 'vue';
|
||||||
import { useConfigStore } from '@/stores/configStore';
|
import {useConfigStore} from '@/stores/configStore';
|
||||||
import { useSystemStore } from '@/stores/systemStore';
|
import {useSystemStore} from '@/stores/systemStore';
|
||||||
import { useKeybindingStore } from '@/stores/keybindingStore';
|
import {useKeybindingStore} from '@/stores/keybindingStore';
|
||||||
import { useThemeStore } from '@/stores/themeStore';
|
import {useThemeStore} from '@/stores/themeStore';
|
||||||
import { useUpdateStore } from '@/stores/updateStore';
|
import {useUpdateStore} from '@/stores/updateStore';
|
||||||
|
import {useBackupStore} from '@/stores/backupStore';
|
||||||
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
|
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
|
||||||
|
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
@@ -12,6 +13,7 @@ const systemStore = useSystemStore();
|
|||||||
const keybindingStore = useKeybindingStore();
|
const keybindingStore = useKeybindingStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const updateStore = useUpdateStore();
|
const updateStore = useUpdateStore();
|
||||||
|
const backupStore = useBackupStore();
|
||||||
|
|
||||||
// 应用启动时加载配置和初始化系统信息
|
// 应用启动时加载配置和初始化系统信息
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -26,6 +28,9 @@ onMounted(async () => {
|
|||||||
await configStore.initializeLanguage();
|
await configStore.initializeLanguage();
|
||||||
themeStore.initializeTheme();
|
themeStore.initializeTheme();
|
||||||
|
|
||||||
|
// 初始化备份服务
|
||||||
|
await backupStore.initialize();
|
||||||
|
|
||||||
// 启动时检查更新
|
// 启动时检查更新
|
||||||
await updateStore.checkOnStartup();
|
await updateStore.checkOnStartup();
|
||||||
});
|
});
|
||||||
@@ -33,7 +38,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<WindowTitleBar />
|
<WindowTitleBar/>
|
||||||
<div class="app-content">
|
<div class="app-content">
|
||||||
<router-view/>
|
<router-view/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,6 +28,11 @@
|
|||||||
--dark-danger-color: #ff6b6b;
|
--dark-danger-color: #ff6b6b;
|
||||||
--dark-bg-primary: #1a1a1a;
|
--dark-bg-primary: #1a1a1a;
|
||||||
--dark-bg-hover: #2a2a2a;
|
--dark-bg-hover: #2a2a2a;
|
||||||
|
--dark-loading-bg-gradient: radial-gradient(#222922, #000500);
|
||||||
|
--dark-loading-color: #fff;
|
||||||
|
--dark-loading-glow: 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5);
|
||||||
|
--dark-loading-done-color: #6f6;
|
||||||
|
--dark-loading-overlay: linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%);
|
||||||
|
|
||||||
/* 浅色主题颜色变量 */
|
/* 浅色主题颜色变量 */
|
||||||
--light-toolbar-bg: #f8f9fa;
|
--light-toolbar-bg: #f8f9fa;
|
||||||
@@ -55,6 +60,11 @@
|
|||||||
--light-danger-color: #dc3545;
|
--light-danger-color: #dc3545;
|
||||||
--light-bg-primary: #ffffff;
|
--light-bg-primary: #ffffff;
|
||||||
--light-bg-hover: #f1f3f4;
|
--light-bg-hover: #f1f3f4;
|
||||||
|
--light-loading-bg-gradient: radial-gradient(#f0f6f0, #e5efe5);
|
||||||
|
--light-loading-color: #1a3c1a;
|
||||||
|
--light-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2);
|
||||||
|
--light-loading-done-color: #008800;
|
||||||
|
--light-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%);
|
||||||
|
|
||||||
/* 默认使用深色主题 */
|
/* 默认使用深色主题 */
|
||||||
--toolbar-bg: var(--dark-toolbar-bg);
|
--toolbar-bg: var(--dark-toolbar-bg);
|
||||||
@@ -83,6 +93,12 @@
|
|||||||
--text-danger: var(--dark-danger-color);
|
--text-danger: var(--dark-danger-color);
|
||||||
--bg-primary: var(--dark-bg-primary);
|
--bg-primary: var(--dark-bg-primary);
|
||||||
--bg-hover: var(--dark-bg-hover);
|
--bg-hover: var(--dark-bg-hover);
|
||||||
|
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
|
||||||
|
--voidraft-loading-color: var(--dark-loading-color);
|
||||||
|
--voidraft-loading-glow: var(--dark-loading-glow);
|
||||||
|
--voidraft-loading-done-color: var(--dark-loading-done-color);
|
||||||
|
--voidraft-loading-overlay: var(--dark-loading-overlay);
|
||||||
|
--voidraft-mono-font: "HarmonyOS Sans Mono", monospace;
|
||||||
|
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
}
|
}
|
||||||
@@ -116,6 +132,11 @@
|
|||||||
--text-danger: var(--dark-danger-color);
|
--text-danger: var(--dark-danger-color);
|
||||||
--bg-primary: var(--dark-bg-primary);
|
--bg-primary: var(--dark-bg-primary);
|
||||||
--bg-hover: var(--dark-bg-hover);
|
--bg-hover: var(--dark-bg-hover);
|
||||||
|
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
|
||||||
|
--voidraft-loading-color: var(--dark-loading-color);
|
||||||
|
--voidraft-loading-glow: var(--dark-loading-glow);
|
||||||
|
--voidraft-loading-done-color: var(--dark-loading-done-color);
|
||||||
|
--voidraft-loading-overlay: var(--dark-loading-overlay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +169,11 @@
|
|||||||
--text-danger: var(--light-danger-color);
|
--text-danger: var(--light-danger-color);
|
||||||
--bg-primary: var(--light-bg-primary);
|
--bg-primary: var(--light-bg-primary);
|
||||||
--bg-hover: var(--light-bg-hover);
|
--bg-hover: var(--light-bg-hover);
|
||||||
|
--voidraft-bg-gradient: var(--light-loading-bg-gradient);
|
||||||
|
--voidraft-loading-color: var(--light-loading-color);
|
||||||
|
--voidraft-loading-glow: var(--light-loading-glow);
|
||||||
|
--voidraft-loading-done-color: var(--light-loading-done-color);
|
||||||
|
--voidraft-loading-overlay: var(--light-loading-overlay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +205,11 @@
|
|||||||
--text-danger: var(--light-danger-color);
|
--text-danger: var(--light-danger-color);
|
||||||
--bg-primary: var(--light-bg-primary);
|
--bg-primary: var(--light-bg-primary);
|
||||||
--bg-hover: var(--light-bg-hover);
|
--bg-hover: var(--light-bg-hover);
|
||||||
|
--voidraft-bg-gradient: var(--light-loading-bg-gradient);
|
||||||
|
--voidraft-loading-color: var(--light-loading-color);
|
||||||
|
--voidraft-loading-glow: var(--light-loading-glow);
|
||||||
|
--voidraft-loading-done-color: var(--light-loading-done-color);
|
||||||
|
--voidraft-loading-overlay: var(--light-loading-overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 手动选择深色主题 */
|
/* 手动选择深色主题 */
|
||||||
@@ -207,4 +238,11 @@
|
|||||||
--selection-bg: var(--dark-selection-bg);
|
--selection-bg: var(--dark-selection-bg);
|
||||||
--selection-text: var(--dark-selection-text);
|
--selection-text: var(--dark-selection-text);
|
||||||
--text-danger: var(--dark-danger-color);
|
--text-danger: var(--dark-danger-color);
|
||||||
|
--bg-primary: var(--dark-bg-primary);
|
||||||
|
--bg-hover: var(--dark-bg-hover);
|
||||||
|
--voidraft-bg-gradient: var(--dark-loading-bg-gradient);
|
||||||
|
--voidraft-loading-color: var(--dark-loading-color);
|
||||||
|
--voidraft-loading-glow: var(--dark-loading-glow);
|
||||||
|
--voidraft-loading-done-color: var(--dark-loading-done-color);
|
||||||
|
--voidraft-loading-overlay: var(--dark-loading-overlay);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
default: 'LOADING'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const characters = ref<HTMLSpanElement[]>([]);
|
||||||
|
const isDone = ref(false);
|
||||||
|
const cycleCount = 5;
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()-_=+{}|[]\\;\':"<>?,./`~'.split('');
|
||||||
|
let animationFrameId: number | null = null;
|
||||||
|
let resetTimeoutId: number | null = null;
|
||||||
|
|
||||||
|
// 将字符串拆分为单个字符的span
|
||||||
|
function letterize() {
|
||||||
|
const container = document.querySelector('.loading-word');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// 清除现有内容
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
// 为每个字符创建span
|
||||||
|
for (let i = 0; i < props.text.length; i++) {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.setAttribute('data-orig', props.text[i]);
|
||||||
|
span.textContent = '-';
|
||||||
|
span.className = `char${i+1}`;
|
||||||
|
container.appendChild(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有span元素
|
||||||
|
characters.value = Array.from(container.querySelectorAll('span'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取随机字符
|
||||||
|
function getRandomChar() {
|
||||||
|
return chars[Math.floor(Math.random() * chars.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动画循环
|
||||||
|
function animationLoop() {
|
||||||
|
let currentCycle = 0;
|
||||||
|
let currentLetterIndex = 0;
|
||||||
|
let isAnimationDone = false;
|
||||||
|
|
||||||
|
function loop() {
|
||||||
|
// 为未完成的字符设置随机字符和不透明度
|
||||||
|
for (let i = currentLetterIndex; i < characters.value.length; i++) {
|
||||||
|
const char = characters.value[i];
|
||||||
|
if (!char.classList.contains('done')) {
|
||||||
|
char.textContent = getRandomChar();
|
||||||
|
char.style.opacity = Math.random().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentCycle < cycleCount) {
|
||||||
|
// 继续当前周期
|
||||||
|
currentCycle++;
|
||||||
|
} else if (currentLetterIndex < characters.value.length) {
|
||||||
|
// 当前周期结束,显示下一个字符的原始值
|
||||||
|
const currentChar = characters.value[currentLetterIndex];
|
||||||
|
currentChar.textContent = currentChar.getAttribute('data-orig') || '';
|
||||||
|
currentChar.style.opacity = '1';
|
||||||
|
currentChar.classList.add('done');
|
||||||
|
currentLetterIndex++;
|
||||||
|
currentCycle = 0;
|
||||||
|
} else {
|
||||||
|
// 所有字符都已显示
|
||||||
|
isAnimationDone = true;
|
||||||
|
isDone.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAnimationDone) {
|
||||||
|
animationFrameId = requestAnimationFrame(loop);
|
||||||
|
} else {
|
||||||
|
// 等待一段时间后重置动画
|
||||||
|
resetTimeoutId = window.setTimeout(() => {
|
||||||
|
reset();
|
||||||
|
}, 750);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置动画
|
||||||
|
function reset() {
|
||||||
|
isDone.value = false;
|
||||||
|
|
||||||
|
for (const char of characters.value) {
|
||||||
|
char.textContent = char.getAttribute('data-orig') || '';
|
||||||
|
char.classList.remove('done');
|
||||||
|
}
|
||||||
|
|
||||||
|
animationLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理所有定时器
|
||||||
|
function cleanup() {
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
animationFrameId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetTimeoutId !== null) {
|
||||||
|
clearTimeout(resetTimeoutId);
|
||||||
|
resetTimeoutId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
letterize();
|
||||||
|
animationLoop();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="loading-screen">
|
||||||
|
<div class="loading-word"></div>
|
||||||
|
<div class="overlay"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.loading-screen {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: var(--voidraft-bg-gradient, radial-gradient(#222922, #000500));
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: var(--voidraft-mono-font, monospace),serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-word {
|
||||||
|
color: var(--voidraft-loading-color, #fff);
|
||||||
|
font-size: 2.5em;
|
||||||
|
height: 2.5em;
|
||||||
|
line-height: 2.5em;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: var(--voidraft-loading-glow, 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-word span {
|
||||||
|
display: inline-block;
|
||||||
|
transform: translateX(100%) scale(0.9);
|
||||||
|
transition: transform 500ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-word .done {
|
||||||
|
color: var(--voidraft-loading-done-color, #6f6);
|
||||||
|
transform: translateX(0) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
background-image: var(--voidraft-loading-overlay, linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%));
|
||||||
|
background-size: 1000px 2px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="linux-titlebar" style="--wails-draggable:drag" @contextmenu.prevent @mouseenter="checkMaximizedState" @mouseup="checkMaximizedState">
|
<div class="linux-titlebar" style="--wails-draggable:drag" @contextmenu.prevent>
|
||||||
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
|
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
|
||||||
<div class="titlebar-icon">
|
<div class="titlebar-icon">
|
||||||
<img src="/appicon.png" alt="voidraft" />
|
<img src="/appicon.png" alt="voidraft"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="titlebar-title">{{ titleText }}</div>
|
<div class="titlebar-title">{{ titleText }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,85 +46,56 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
import {computed, onMounted, ref} from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
import { useWindowStore } from '@/stores/windowStore';
|
import {useDocumentStore} from '@/stores/documentStore';
|
||||||
import { useDocumentStore } from '@/stores/documentStore';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const {t} = useI18n();
|
||||||
const isMaximized = ref(false);
|
const isMaximized = ref(false);
|
||||||
const documentStore = useDocumentStore();
|
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 titleText = computed(() => {
|
||||||
const currentDoc = documentStore.currentDocument;
|
const currentDoc = documentStore.currentDocument;
|
||||||
return currentDoc ? `voidraft - ${currentDoc.title}` : 'voidraft';
|
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 () => {
|
onMounted(async () => {
|
||||||
await checkMaximizedState();
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,6 @@
|
|||||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
import { useWindowStore } from '@/stores/windowStore';
|
|
||||||
import { useDocumentStore } from '@/stores/documentStore';
|
import { useDocumentStore } from '@/stores/documentStore';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -65,26 +64,16 @@ const minimizeWindow = async () => {
|
|||||||
try {
|
try {
|
||||||
await runtime.Window.Minimise();
|
await runtime.Window.Minimise();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error handling
|
console.error(error)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleMaximize = async () => {
|
const toggleMaximize = async () => {
|
||||||
try {
|
try {
|
||||||
const newState = !isMaximized.value;
|
await runtime.Window.ToggleMaximise();
|
||||||
isMaximized.value = newState;
|
|
||||||
|
|
||||||
if (newState) {
|
|
||||||
await runtime.Window.Maximise();
|
|
||||||
} else {
|
|
||||||
await runtime.Window.UnMaximise();
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
await checkMaximizedState();
|
await checkMaximizedState();
|
||||||
}, 100);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
isMaximized.value = !isMaximized.value;
|
console.error(error)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,7 +81,7 @@ const closeWindow = async () => {
|
|||||||
try {
|
try {
|
||||||
await runtime.Window.Close();
|
await runtime.Window.Close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error handling
|
console.error(error)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,7 +89,7 @@ const checkMaximizedState = async () => {
|
|||||||
try {
|
try {
|
||||||
isMaximized.value = await runtime.Window.IsMaximised();
|
isMaximized.value = await runtime.Window.IsMaximised();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error handling
|
console.error(error)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,24 +101,6 @@ const titleText = computed(() => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await checkMaximizedState();
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="windows-titlebar" style="--wails-draggable:drag" @contextmenu.prevent @mouseenter="checkMaximizedState"
|
<div class="windows-titlebar" style="--wails-draggable:drag">
|
||||||
@mouseup="checkMaximizedState">
|
|
||||||
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
|
<div class="titlebar-content" @dblclick="toggleMaximize" @contextmenu.prevent>
|
||||||
<div class="titlebar-icon">
|
<div class="titlebar-icon">
|
||||||
<img src="/appicon.png" alt="voidraft"/>
|
<img src="/appicon.png" alt="voidraft"/>
|
||||||
@@ -37,11 +36,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, onMounted, onUnmounted, ref} from 'vue';
|
import {computed, onMounted, ref} from 'vue';
|
||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
import { useWindowStore } from '@/stores/windowStore';
|
import {useDocumentStore} from '@/stores/documentStore';
|
||||||
import { useDocumentStore } from '@/stores/documentStore';
|
|
||||||
|
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
const isMaximized = ref(false);
|
const isMaximized = ref(false);
|
||||||
@@ -60,30 +58,16 @@ const minimizeWindow = async () => {
|
|||||||
try {
|
try {
|
||||||
await runtime.Window.Minimise();
|
await runtime.Window.Minimise();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error handling
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleMaximize = async () => {
|
const toggleMaximize = async () => {
|
||||||
try {
|
try {
|
||||||
// 立即更新UI状态,提供即时反馈
|
await runtime.Window.ToggleMaximise();
|
||||||
const newState = !isMaximized.value;
|
|
||||||
isMaximized.value = newState;
|
|
||||||
|
|
||||||
// 然后执行实际操作
|
|
||||||
if (newState) {
|
|
||||||
await runtime.Window.Maximise();
|
|
||||||
} else {
|
|
||||||
await runtime.Window.UnMaximise();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 操作完成后再次确认状态(防止操作失败时状态不一致)
|
|
||||||
setTimeout(async () => {
|
|
||||||
await checkMaximizedState();
|
await checkMaximizedState();
|
||||||
}, 100);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 如果操作失败,恢复原状态
|
console.error(error);
|
||||||
isMaximized.value = !isMaximized.value;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,7 +75,7 @@ const closeWindow = async () => {
|
|||||||
try {
|
try {
|
||||||
await runtime.Window.Close();
|
await runtime.Window.Close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error handling
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,31 +83,12 @@ const checkMaximizedState = async () => {
|
|||||||
try {
|
try {
|
||||||
isMaximized.value = await runtime.Window.IsMaximised();
|
isMaximized.value = await runtime.Window.IsMaximised();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error handling
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await checkMaximizedState();
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,12 @@ const selectItem = async (item: any) => {
|
|||||||
// 选择文档
|
// 选择文档
|
||||||
const selectDoc = async (doc: Document) => {
|
const selectDoc = async (doc: Document) => {
|
||||||
try {
|
try {
|
||||||
|
// 如果选择的就是当前文档,直接关闭菜单
|
||||||
|
if (documentStore.currentDocument?.id === doc.id) {
|
||||||
|
closeMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
||||||
if (hasOpen) {
|
if (hasOpen) {
|
||||||
// 设置错误状态并启动定时器
|
// 设置错误状态并启动定时器
|
||||||
@@ -208,16 +214,41 @@ const openInNewWindow = async (doc: Document, event: Event) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理删除 - 简化确认机制
|
// 处理删除
|
||||||
const handleDelete = async (doc: Document, event: Event) => {
|
const handleDelete = async (doc: Document, event: Event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (deleteConfirmId.value === doc.id) {
|
if (deleteConfirmId.value === doc.id) {
|
||||||
// 确认删除
|
// 确认删除前检查文档是否在其他窗口打开
|
||||||
try {
|
try {
|
||||||
await documentStore.deleteDocument(doc.id);
|
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
||||||
await documentStore.updateDocuments();
|
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) {
|
if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) {
|
||||||
const firstDoc = documentStore.documentList[0];
|
const firstDoc = documentStore.documentList[0];
|
||||||
|
|||||||
@@ -119,12 +119,15 @@ export default {
|
|||||||
general: 'General',
|
general: 'General',
|
||||||
editing: 'Editor',
|
editing: 'Editor',
|
||||||
appearance: 'Appearance',
|
appearance: 'Appearance',
|
||||||
|
backupPage: 'Backup',
|
||||||
keyBindings: 'Key Bindings',
|
keyBindings: 'Key Bindings',
|
||||||
updates: 'Updates',
|
updates: 'Updates',
|
||||||
reset: 'Reset',
|
reset: 'Reset',
|
||||||
|
apply: 'Apply',
|
||||||
|
cancel: 'Cancel',
|
||||||
dangerZone: 'Danger Zone',
|
dangerZone: 'Danger Zone',
|
||||||
resetAllSettings: 'Reset All Settings',
|
resetAllSettings: 'Reset All Settings',
|
||||||
confirmReset: 'Click again to confirm reset',
|
confirmReset: 'Confirm the reset?',
|
||||||
globalHotkey: 'Global Keyboard Shortcuts',
|
globalHotkey: 'Global Keyboard Shortcuts',
|
||||||
enableGlobalHotkey: 'Enable Global Hotkeys',
|
enableGlobalHotkey: 'Enable Global Hotkeys',
|
||||||
window: 'Window/Application',
|
window: 'Window/Application',
|
||||||
@@ -156,6 +159,40 @@ export default {
|
|||||||
'800': 'Extra Bold (800)',
|
'800': 'Extra Bold (800)',
|
||||||
'900': 'Black (900)'
|
'900': 'Black (900)'
|
||||||
},
|
},
|
||||||
|
customThemeColors: 'Custom Theme Colors',
|
||||||
|
resetToDefault: 'Reset to Default',
|
||||||
|
colorValue: 'Color Value',
|
||||||
|
themeColors: {
|
||||||
|
basic: 'Basic Colors',
|
||||||
|
text: 'Text Colors',
|
||||||
|
syntax: 'Syntax Highlighting',
|
||||||
|
interface: 'Interface Elements',
|
||||||
|
border: 'Borders & Dividers',
|
||||||
|
search: 'Search & Matching',
|
||||||
|
background: 'Main Background',
|
||||||
|
backgroundSecondary: 'Secondary Background',
|
||||||
|
surface: 'Panel Background',
|
||||||
|
foreground: 'Primary Text',
|
||||||
|
foregroundSecondary: 'Secondary Text',
|
||||||
|
comment: 'Comments',
|
||||||
|
keyword: 'Keywords',
|
||||||
|
string: 'Strings',
|
||||||
|
function: 'Functions',
|
||||||
|
number: 'Numbers',
|
||||||
|
operator: 'Operators',
|
||||||
|
variable: 'Variables',
|
||||||
|
type: 'Types',
|
||||||
|
cursor: 'Cursor',
|
||||||
|
selection: 'Selection Background',
|
||||||
|
selectionBlur: 'Unfocused Selection',
|
||||||
|
activeLine: 'Active Line Highlight',
|
||||||
|
lineNumber: 'Line Numbers',
|
||||||
|
activeLineNumber: 'Active Line Number',
|
||||||
|
borderColor: 'Border Color',
|
||||||
|
borderLight: 'Light Border',
|
||||||
|
searchMatch: 'Search Match',
|
||||||
|
matchingBracket: 'Matching Bracket'
|
||||||
|
},
|
||||||
fontFamilies: {
|
fontFamilies: {
|
||||||
harmonyOS: 'HarmonyOS Sans',
|
harmonyOS: 'HarmonyOS Sans',
|
||||||
microsoftYahei: 'Microsoft YaHei',
|
microsoftYahei: 'Microsoft YaHei',
|
||||||
@@ -206,6 +243,49 @@ export default {
|
|||||||
restartNow: 'Restart Now',
|
restartNow: 'Restart Now',
|
||||||
hotkeyPreview: 'Preview:',
|
hotkeyPreview: 'Preview:',
|
||||||
none: 'None',
|
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: {
|
extensions: {
|
||||||
rainbowBrackets: {
|
rainbowBrackets: {
|
||||||
|
|||||||
@@ -119,13 +119,16 @@ export default {
|
|||||||
general: '常规',
|
general: '常规',
|
||||||
editing: '编辑器',
|
editing: '编辑器',
|
||||||
appearance: '外观',
|
appearance: '外观',
|
||||||
|
backupPage: '备份',
|
||||||
extensions: '扩展',
|
extensions: '扩展',
|
||||||
keyBindings: '快捷键',
|
keyBindings: '快捷键',
|
||||||
updates: '更新',
|
updates: '更新',
|
||||||
reset: '重置',
|
reset: '重置',
|
||||||
|
apply: '应用',
|
||||||
|
cancel: '取消',
|
||||||
dangerZone: '危险操作',
|
dangerZone: '危险操作',
|
||||||
resetAllSettings: '重置所有设置',
|
resetAllSettings: '重置所有设置',
|
||||||
confirmReset: '再次点击确认重置',
|
confirmReset: '确认重置?',
|
||||||
globalHotkey: '全局键盘快捷键',
|
globalHotkey: '全局键盘快捷键',
|
||||||
enableGlobalHotkey: '启用全局热键',
|
enableGlobalHotkey: '启用全局热键',
|
||||||
window: '窗口/应用程序',
|
window: '窗口/应用程序',
|
||||||
@@ -196,6 +199,40 @@ export default {
|
|||||||
'800': '超粗 (800)',
|
'800': '超粗 (800)',
|
||||||
'900': '极粗 (900)'
|
'900': '极粗 (900)'
|
||||||
},
|
},
|
||||||
|
customThemeColors: '自定义主题颜色',
|
||||||
|
resetToDefault: '重置为默认',
|
||||||
|
colorValue: '颜色值',
|
||||||
|
themeColors: {
|
||||||
|
basic: '基础色调',
|
||||||
|
text: '文本颜色',
|
||||||
|
syntax: '语法高亮',
|
||||||
|
interface: '界面元素',
|
||||||
|
border: '边框分割线',
|
||||||
|
search: '搜索匹配',
|
||||||
|
background: '主背景色',
|
||||||
|
backgroundSecondary: '次要背景色',
|
||||||
|
surface: '面板背景',
|
||||||
|
foreground: '主文本色',
|
||||||
|
foregroundSecondary: '次要文本色',
|
||||||
|
comment: '注释色',
|
||||||
|
keyword: '关键字',
|
||||||
|
string: '字符串',
|
||||||
|
function: '函数名',
|
||||||
|
number: '数字',
|
||||||
|
operator: '操作符',
|
||||||
|
variable: '变量',
|
||||||
|
type: '类型',
|
||||||
|
cursor: '光标',
|
||||||
|
selection: '选中背景',
|
||||||
|
selectionBlur: '失焦选中背景',
|
||||||
|
activeLine: '当前行高亮',
|
||||||
|
lineNumber: '行号',
|
||||||
|
activeLineNumber: '活动行号',
|
||||||
|
borderColor: '边框色',
|
||||||
|
borderLight: '浅色边框',
|
||||||
|
searchMatch: '搜索匹配',
|
||||||
|
matchingBracket: '匹配括号'
|
||||||
|
},
|
||||||
fontFamilies: {
|
fontFamilies: {
|
||||||
harmonyOS: '鸿蒙字体',
|
harmonyOS: '鸿蒙字体',
|
||||||
microsoftYahei: '微软雅黑',
|
microsoftYahei: '微软雅黑',
|
||||||
@@ -207,6 +244,49 @@ export default {
|
|||||||
},
|
},
|
||||||
hotkeyPreview: '预览:',
|
hotkeyPreview: '预览:',
|
||||||
none: '无',
|
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: {
|
extensions: {
|
||||||
rainbowBrackets: {
|
rainbowBrackets: {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import AppearancePage from '@/views/settings/pages/AppearancePage.vue';
|
|||||||
import KeyBindingsPage from '@/views/settings/pages/KeyBindingsPage.vue';
|
import KeyBindingsPage from '@/views/settings/pages/KeyBindingsPage.vue';
|
||||||
import UpdatesPage from '@/views/settings/pages/UpdatesPage.vue';
|
import UpdatesPage from '@/views/settings/pages/UpdatesPage.vue';
|
||||||
import ExtensionsPage from '@/views/settings/pages/ExtensionsPage.vue';
|
import ExtensionsPage from '@/views/settings/pages/ExtensionsPage.vue';
|
||||||
|
import BackupPage from '@/views/settings/pages/BackupPage.vue';
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
@@ -49,6 +50,11 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: 'updates',
|
path: 'updates',
|
||||||
name: 'SettingsUpdates',
|
name: 'SettingsUpdates',
|
||||||
component: UpdatesPage
|
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,
|
TabType,
|
||||||
UpdatesConfig,
|
UpdatesConfig,
|
||||||
UpdateSourceType,
|
UpdateSourceType,
|
||||||
|
GitBackupConfig,
|
||||||
|
AuthMethod
|
||||||
} from '@/../bindings/voidraft/internal/models/models';
|
} from '@/../bindings/voidraft/internal/models/models';
|
||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import {ConfigUtils} from '@/utils/configUtils';
|
import {ConfigUtils} from '@/utils/configUtils';
|
||||||
import {WindowController} from '@/utils/windowController';
|
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
// 国际化相关导入
|
// 国际化相关导入
|
||||||
export type SupportedLocaleType = 'zh-CN' | 'en-US';
|
export type SupportedLocaleType = 'zh-CN' | 'en-US';
|
||||||
@@ -48,6 +49,10 @@ type UpdatesConfigKeyMap = {
|
|||||||
readonly [K in keyof UpdatesConfig]: string;
|
readonly [K in keyof UpdatesConfig]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BackupConfigKeyMap = {
|
||||||
|
readonly [K in keyof GitBackupConfig]: string;
|
||||||
|
};
|
||||||
|
|
||||||
type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight';
|
type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight';
|
||||||
|
|
||||||
// 配置键映射
|
// 配置键映射
|
||||||
@@ -87,6 +92,20 @@ const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = {
|
|||||||
gitea: 'updates.gitea'
|
gitea: 'updates.gitea'
|
||||||
} as const;
|
} 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 = {
|
const CONFIG_LIMITS = {
|
||||||
fontSize: {min: 12, max: 28, default: 13},
|
fontSize: {min: 12, max: 28, default: 13},
|
||||||
@@ -190,6 +209,18 @@ const DEFAULT_CONFIG: AppConfig = {
|
|||||||
repo: "voidraft",
|
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: {
|
metadata: {
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
lastUpdated: new Date().toString(),
|
lastUpdated: new Date().toString(),
|
||||||
@@ -277,6 +308,21 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
state.config.updates[key] = value;
|
state.config.updates[key] = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateBackupConfig = async <K extends keyof GitBackupConfig>(key: K, value: GitBackupConfig[K]): Promise<void> => {
|
||||||
|
// 确保配置已加载
|
||||||
|
if (!state.configLoaded && !state.isLoading) {
|
||||||
|
await initConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
const backendKey = BACKUP_CONFIG_KEY_MAP[key];
|
||||||
|
if (!backendKey) {
|
||||||
|
throw new Error(`No backend key mapping found for backup.${key.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ConfigService.Set(backendKey, value);
|
||||||
|
state.config.backup[key] = value;
|
||||||
|
};
|
||||||
|
|
||||||
// 加载配置
|
// 加载配置
|
||||||
const initConfig = async (): Promise<void> => {
|
const initConfig = async (): Promise<void> => {
|
||||||
if (state.isLoading) return;
|
if (state.isLoading) return;
|
||||||
@@ -291,14 +337,12 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
if (appConfig.editing) Object.assign(state.config.editing, appConfig.editing);
|
if (appConfig.editing) Object.assign(state.config.editing, appConfig.editing);
|
||||||
if (appConfig.appearance) Object.assign(state.config.appearance, appConfig.appearance);
|
if (appConfig.appearance) Object.assign(state.config.appearance, appConfig.appearance);
|
||||||
if (appConfig.updates) Object.assign(state.config.updates, appConfig.updates);
|
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);
|
if (appConfig.metadata) Object.assign(state.config.metadata, appConfig.metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.configLoaded = true;
|
state.configLoaded = true;
|
||||||
|
|
||||||
// 初始化热键监听器
|
|
||||||
const windowController = WindowController.getInstance();
|
|
||||||
await windowController.initializeHotkeyListener();
|
|
||||||
} finally {
|
} finally {
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
}
|
}
|
||||||
@@ -363,6 +407,8 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
await updateAppearanceConfig('systemTheme', systemTheme);
|
await updateAppearanceConfig('systemTheme', systemTheme);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 初始化语言设置
|
// 初始化语言设置
|
||||||
const initializeLanguage = async (): Promise<void> => {
|
const initializeLanguage = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@@ -476,6 +522,18 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 更新配置相关方法
|
// 更新配置相关方法
|
||||||
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),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -66,6 +66,9 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
selectedCharacters: 0
|
selectedCharacters: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 编辑器加载状态
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
// 异步操作竞态条件控制
|
// 异步操作竞态条件控制
|
||||||
const operationSequence = ref(0);
|
const operationSequence = ref(0);
|
||||||
const pendingOperations = ref(new Map<number, AbortController>());
|
const pendingOperations = ref(new Map<number, AbortController>());
|
||||||
@@ -434,6 +437,8 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 加载编辑器
|
// 加载编辑器
|
||||||
const loadEditor = async (documentId: number, content: string) => {
|
const loadEditor = async (documentId: number, content: string) => {
|
||||||
|
// 设置加载状态
|
||||||
|
isLoading.value = true;
|
||||||
// 生成新的操作ID
|
// 生成新的操作ID
|
||||||
const operationId = getNextOperationId();
|
const operationId = getNextOperationId();
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
@@ -500,15 +505,20 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.message === 'Operation cancelled') {
|
if (error instanceof Error && error.message === 'Operation cancelled') {
|
||||||
console.log(`Editor loading cancelled for document ${documentId}`);
|
console.log(`Editor loading cancelled for document ${documentId}`);
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
console.error('Failed to load editor:', error);
|
console.error('Failed to load editor:', error);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// 清理操作记录
|
// 清理操作记录
|
||||||
pendingOperations.value.delete(operationId);
|
pendingOperations.value.delete(operationId);
|
||||||
if (currentLoadingDocumentId.value === documentId) {
|
if (currentLoadingDocumentId.value === documentId) {
|
||||||
currentLoadingDocumentId.value = null;
|
currentLoadingDocumentId.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 延迟一段时间后再取消加载状态
|
||||||
|
setTimeout(() => {
|
||||||
|
isLoading.value = false;
|
||||||
|
}, 800);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -684,6 +694,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
// 状态
|
// 状态
|
||||||
currentEditor,
|
currentEditor,
|
||||||
documentStats,
|
documentStats,
|
||||||
|
isLoading,
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
setEditorContainer,
|
setEditorContainer,
|
||||||
|
|||||||
@@ -1,19 +1,47 @@
|
|||||||
import { defineStore } from 'pinia';
|
import {defineStore} from 'pinia';
|
||||||
import { computed } from 'vue';
|
import {computed, reactive} from 'vue';
|
||||||
import { SystemThemeType } from '@/../bindings/voidraft/internal/models/models';
|
import {SystemThemeType, ThemeType, ThemeColorConfig} from '@/../bindings/voidraft/internal/models/models';
|
||||||
import { useConfigStore } from './configStore';
|
import {ThemeService} from '@/../bindings/voidraft/internal/services';
|
||||||
|
import {useConfigStore} from './configStore';
|
||||||
|
import {useEditorStore} from './editorStore';
|
||||||
|
import {defaultDarkColors} from '@/views/editor/theme/dark';
|
||||||
|
import {defaultLightColors} from '@/views/editor/theme/light';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主题管理 Store
|
* 主题管理 Store
|
||||||
* 职责:
|
* 职责:管理主题状态和颜色配置
|
||||||
*/
|
*/
|
||||||
export const useThemeStore = defineStore('theme', () => {
|
export const useThemeStore = defineStore('theme', () => {
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
|
|
||||||
|
// 响应式状态 - 存储当前使用的主题颜色
|
||||||
|
const themeColors = reactive({
|
||||||
|
darkTheme: { ...defaultDarkColors },
|
||||||
|
lightTheme: { ...defaultLightColors }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性 - 当前选择的主题类型
|
||||||
const currentTheme = computed(() =>
|
const currentTheme = computed(() =>
|
||||||
configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
|
configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 初始化主题颜色 - 从数据库加载
|
||||||
|
const initializeThemeColors = async () => {
|
||||||
|
try {
|
||||||
|
const themes = await ThemeService.GetDefaultThemes();
|
||||||
|
if (themes.dark) {
|
||||||
|
Object.assign(themeColors.darkTheme, themes.dark.colors);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 应用主题到 DOM
|
// 应用主题到 DOM
|
||||||
const applyThemeToDOM = (theme: SystemThemeType) => {
|
const applyThemeToDOM = (theme: SystemThemeType) => {
|
||||||
@@ -24,21 +52,104 @@ export const useThemeStore = defineStore('theme', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 初始化主题
|
// 初始化主题
|
||||||
const initializeTheme = () => {
|
const initializeTheme = async () => {
|
||||||
const theme = configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto;
|
const theme = configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto;
|
||||||
applyThemeToDOM(theme);
|
applyThemeToDOM(theme);
|
||||||
|
await initializeThemeColors();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设置主题
|
// 设置主题
|
||||||
const setTheme = async (theme: SystemThemeType) => {
|
const setTheme = async (theme: SystemThemeType) => {
|
||||||
await configStore.setSystemTheme(theme);
|
await configStore.setSystemTheme(theme);
|
||||||
applyThemeToDOM(theme);
|
applyThemeToDOM(theme);
|
||||||
|
refreshEditorTheme();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新主题颜色
|
||||||
|
const updateThemeColors = (darkColors: any = null, lightColors: any = null): boolean => {
|
||||||
|
let hasChanges = false;
|
||||||
|
|
||||||
|
if (darkColors) {
|
||||||
|
Object.entries(darkColors).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && themeColors.darkTheme[key] !== value) {
|
||||||
|
themeColors.darkTheme[key] = value;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lightColors) {
|
||||||
|
Object.entries(lightColors).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && themeColors.lightTheme[key] !== value) {
|
||||||
|
themeColors.lightTheme[key] = value;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasChanges;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存主题颜色到数据库
|
||||||
|
const saveThemeColors = async () => {
|
||||||
|
try {
|
||||||
|
const darkColors = ThemeColorConfig.createFrom(themeColors.darkTheme);
|
||||||
|
const lightColors = ThemeColorConfig.createFrom(themeColors.lightTheme);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
const dbThemeType = themeType === 'darkTheme' ? ThemeType.ThemeTypeDark : ThemeType.ThemeTypeLight;
|
||||||
|
|
||||||
|
// 1. 调用后端重置服务
|
||||||
|
await ThemeService.ResetThemeColors(dbThemeType);
|
||||||
|
|
||||||
|
// 2. 更新内存中的颜色状态
|
||||||
|
if (themeType === 'darkTheme') {
|
||||||
|
Object.assign(themeColors.darkTheme, defaultDarkColors);
|
||||||
|
} else {
|
||||||
|
Object.assign(themeColors.lightTheme, defaultLightColors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 刷新编辑器主题
|
||||||
|
refreshEditorTheme();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to reset theme colors:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 刷新编辑器主题(在主题颜色更改后调用)
|
||||||
|
const refreshEditorTheme = () => {
|
||||||
|
// 使用当前主题重新应用DOM主题
|
||||||
|
const theme = currentTheme.value;
|
||||||
|
applyThemeToDOM(theme);
|
||||||
|
|
||||||
|
const editorStore = useEditorStore();
|
||||||
|
if (editorStore) {
|
||||||
|
editorStore.applyThemeSettings();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentTheme,
|
currentTheme,
|
||||||
|
themeColors,
|
||||||
setTheme,
|
setTheme,
|
||||||
initializeTheme,
|
initializeTheme,
|
||||||
applyThemeToDOM,
|
applyThemeToDOM,
|
||||||
|
updateThemeColors,
|
||||||
|
saveThemeColors,
|
||||||
|
resetThemeColors,
|
||||||
|
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {onBeforeUnmount, onMounted, ref, watch} from 'vue';
|
import {onBeforeUnmount, onMounted, ref} from 'vue';
|
||||||
import {useEditorStore} from '@/stores/editorStore';
|
import {useEditorStore} from '@/stores/editorStore';
|
||||||
import {useDocumentStore} from '@/stores/documentStore';
|
import {useDocumentStore} from '@/stores/documentStore';
|
||||||
import {useConfigStore} from '@/stores/configStore';
|
import {useConfigStore} from '@/stores/configStore';
|
||||||
import {createWheelZoomHandler} from './basic/wheelZoomExtension';
|
import {createWheelZoomHandler} from './basic/wheelZoomExtension';
|
||||||
import Toolbar from '@/components/toolbar/Toolbar.vue';
|
import Toolbar from '@/components/toolbar/Toolbar.vue';
|
||||||
import {useWindowStore} from "@/stores/windowStore";
|
import {useWindowStore} from "@/stores/windowStore";
|
||||||
|
import LoadingScreen from '@/components/loading/LoadingScreen.vue';
|
||||||
|
|
||||||
const editorStore = useEditorStore();
|
const editorStore = useEditorStore();
|
||||||
const documentStore = useDocumentStore();
|
const documentStore = useDocumentStore();
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const windowStore = useWindowStore();
|
const windowStore = useWindowStore();
|
||||||
|
|
||||||
|
|
||||||
const editorElement = ref<HTMLElement | null>(null);
|
const editorElement = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
// 创建滚轮缩放处理器
|
// 创建滚轮缩放处理器
|
||||||
@@ -25,7 +25,6 @@ onMounted(async () => {
|
|||||||
if (!editorElement.value) return;
|
if (!editorElement.value) return;
|
||||||
|
|
||||||
// 从URL查询参数中获取documentId
|
// 从URL查询参数中获取documentId
|
||||||
|
|
||||||
const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined;
|
const urlDocumentId = windowStore.currentDocumentId ? parseInt(windowStore.currentDocumentId) : undefined;
|
||||||
|
|
||||||
// 初始化文档存储,优先使用URL参数中的文档ID
|
// 初始化文档存储,优先使用URL参数中的文档ID
|
||||||
@@ -48,6 +47,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="editor-container">
|
<div class="editor-container">
|
||||||
|
<LoadingScreen v-if="editorStore.isLoading" text="VOIDRAFT" />
|
||||||
<div ref="editorElement" class="editor"></div>
|
<div ref="editorElement" class="editor"></div>
|
||||||
<Toolbar/>
|
<Toolbar/>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,6 +60,7 @@ onBeforeUnmount(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Extension, Compartment } from '@codemirror/state';
|
import { Extension, Compartment } from '@codemirror/state';
|
||||||
import { EditorView } from '@codemirror/view';
|
import { EditorView } from '@codemirror/view';
|
||||||
import { SystemThemeType } from '@/../bindings/voidraft/internal/models/models';
|
import { SystemThemeType } from '@/../bindings/voidraft/internal/models/models';
|
||||||
import { dark } from '@/views/editor/theme/dark';
|
import { createDarkTheme } from '@/views/editor/theme/dark';
|
||||||
import { light } from '@/views/editor/theme/light';
|
import { createLightTheme } from '@/views/editor/theme/light';
|
||||||
|
import { useThemeStore } from '@/stores/themeStore';
|
||||||
|
|
||||||
// 主题区间 - 用于动态切换主题
|
// 主题区间 - 用于动态切换主题
|
||||||
export const themeCompartment = new Compartment();
|
export const themeCompartment = new Compartment();
|
||||||
@@ -11,6 +12,8 @@ export const themeCompartment = new Compartment();
|
|||||||
* 根据主题类型获取主题扩展
|
* 根据主题类型获取主题扩展
|
||||||
*/
|
*/
|
||||||
const getThemeExtension = (themeType: SystemThemeType): Extension => {
|
const getThemeExtension = (themeType: SystemThemeType): Extension => {
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
// 处理 auto 主题类型
|
// 处理 auto 主题类型
|
||||||
let actualTheme: SystemThemeType = themeType;
|
let actualTheme: SystemThemeType = themeType;
|
||||||
if (themeType === SystemThemeType.SystemThemeAuto) {
|
if (themeType === SystemThemeType.SystemThemeAuto) {
|
||||||
@@ -19,13 +22,11 @@ const getThemeExtension = (themeType: SystemThemeType): Extension => {
|
|||||||
: SystemThemeType.SystemThemeLight;
|
: SystemThemeType.SystemThemeLight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直接返回对应的主题扩展
|
// 根据主题类型创建主题
|
||||||
switch (actualTheme) {
|
if (actualTheme === SystemThemeType.SystemThemeLight) {
|
||||||
case SystemThemeType.SystemThemeLight:
|
return createLightTheme(themeStore.themeColors.lightTheme);
|
||||||
return light;
|
} else {
|
||||||
case SystemThemeType.SystemThemeDark:
|
return createDarkTheme(themeStore.themeColors.darkTheme);
|
||||||
default:
|
|
||||||
return dark;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,9 +46,13 @@ export const updateEditorTheme = (view: EditorView, themeType: SystemThemeType):
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const extension = getThemeExtension(themeType);
|
const extension = getThemeExtension(themeType);
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
effects: themeCompartment.reconfigure(extension)
|
effects: themeCompartment.reconfigure(extension)
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update editor theme:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3,8 +3,7 @@ import { Range } from '@codemirror/state';
|
|||||||
|
|
||||||
// 生成彩虹颜色数组
|
// 生成彩虹颜色数组
|
||||||
function generateColors(): string[] {
|
function generateColors(): string[] {
|
||||||
return [
|
return ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'
|
||||||
'red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,13 +74,13 @@ export default function rainbowBracketsExtension() {
|
|||||||
rainbowBracketsPlugin,
|
rainbowBracketsPlugin,
|
||||||
EditorView.baseTheme({
|
EditorView.baseTheme({
|
||||||
// 为每种颜色定义CSS样式
|
// 为每种颜色定义CSS样式
|
||||||
'.cm-rainbow-bracket-red': { color: 'red' },
|
'.cm-rainbow-bracket-red': { color: '#FF6B6B' },
|
||||||
'.cm-rainbow-bracket-orange': { color: 'orange' },
|
'.cm-rainbow-bracket-orange': { color: '#FF9E6B' },
|
||||||
'.cm-rainbow-bracket-yellow': { color: 'yellow' },
|
'.cm-rainbow-bracket-yellow': { color: '#FFD166' },
|
||||||
'.cm-rainbow-bracket-green': { color: 'green' },
|
'.cm-rainbow-bracket-green': { color: '#06D6A0' },
|
||||||
'.cm-rainbow-bracket-blue': { color: 'blue' },
|
'.cm-rainbow-bracket-blue': { color: '#118AB2' },
|
||||||
'.cm-rainbow-bracket-indigo': { color: 'indigo' },
|
'.cm-rainbow-bracket-indigo': { color: '#6B5B95' },
|
||||||
'.cm-rainbow-bracket-violet': { color: 'violet' },
|
'.cm-rainbow-bracket-violet': { color: '#9B5DE5' },
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -65,6 +65,10 @@ export class ExtensionManager {
|
|||||||
// 注册的扩展工厂
|
// 注册的扩展工厂
|
||||||
private extensionFactories = new Map<ExtensionID, ExtensionFactory>()
|
private extensionFactories = new Map<ExtensionID, ExtensionFactory>()
|
||||||
|
|
||||||
|
// 防抖处理
|
||||||
|
private debounceTimers = new Map<ExtensionID, number>()
|
||||||
|
private debounceDelay = 300 // 默认防抖时间为300毫秒
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册扩展工厂
|
* 注册扩展工厂
|
||||||
* @param id 扩展ID
|
* @param id 扩展ID
|
||||||
@@ -187,13 +191,24 @@ export class ExtensionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新单个扩展配置并应用到所有视图
|
* 更新单个扩展配置并应用到所有视图(带防抖功能)
|
||||||
* @param id 扩展ID
|
* @param id 扩展ID
|
||||||
* @param enabled 是否启用
|
* @param enabled 是否启用
|
||||||
* @param config 扩展配置
|
* @param config 扩展配置
|
||||||
*/
|
*/
|
||||||
updateExtension(id: ExtensionID, enabled: boolean, config: any = {}): void {
|
updateExtension(id: ExtensionID, enabled: boolean, config: any = {}): void {
|
||||||
|
// 清除之前的定时器
|
||||||
|
if (this.debounceTimers.has(id)) {
|
||||||
|
window.clearTimeout(this.debounceTimers.get(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置新的定时器
|
||||||
|
const timerId = window.setTimeout(() => {
|
||||||
this.updateExtensionImmediate(id, enabled, config)
|
this.updateExtensionImmediate(id, enabled, config)
|
||||||
|
this.debounceTimers.delete(id)
|
||||||
|
}, this.debounceDelay)
|
||||||
|
|
||||||
|
this.debounceTimers.set(id, timerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -262,6 +277,14 @@ export class ExtensionManager {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
config: any
|
config: any
|
||||||
}>): void {
|
}>): void {
|
||||||
|
// 清除所有相关的防抖定时器
|
||||||
|
for (const update of updates) {
|
||||||
|
if (this.debounceTimers.has(update.id)) {
|
||||||
|
window.clearTimeout(this.debounceTimers.get(update.id))
|
||||||
|
this.debounceTimers.delete(update.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 更新所有扩展状态
|
// 更新所有扩展状态
|
||||||
for (const update of updates) {
|
for (const update of updates) {
|
||||||
// 获取扩展状态
|
// 获取扩展状态
|
||||||
@@ -357,6 +380,12 @@ export class ExtensionManager {
|
|||||||
* 销毁管理器
|
* 销毁管理器
|
||||||
*/
|
*/
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
|
// 清除所有防抖定时器
|
||||||
|
for (const timerId of this.debounceTimers.values()) {
|
||||||
|
window.clearTimeout(timerId)
|
||||||
|
}
|
||||||
|
this.debounceTimers.clear()
|
||||||
|
|
||||||
this.viewsMap.clear()
|
this.viewsMap.clear()
|
||||||
this.activeViewId = null
|
this.activeViewId = null
|
||||||
this.extensionFactories.clear()
|
this.extensionFactories.clear()
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import {EditorView} from '@codemirror/view';
|
|||||||
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language';
|
import {HighlightStyle, syntaxHighlighting} from '@codemirror/language';
|
||||||
import {tags} from '@lezer/highlight';
|
import {tags} from '@lezer/highlight';
|
||||||
|
|
||||||
const colors = {
|
// 默认深色主题颜色
|
||||||
|
export const defaultDarkColors = {
|
||||||
// 基础色调
|
// 基础色调
|
||||||
background: '#252B37', // 主背景色
|
background: '#252B37', // 主背景色
|
||||||
// backgroundAlt: '#252B37', // 交替背景色
|
|
||||||
backgroundSecondary: '#213644', // 次要背景色
|
backgroundSecondary: '#213644', // 次要背景色
|
||||||
surface: '#474747', // 面板背景
|
surface: '#474747', // 面板背景
|
||||||
|
|
||||||
@@ -24,23 +24,25 @@ const colors = {
|
|||||||
type: '#8be9fd', // 类型
|
type: '#8be9fd', // 类型
|
||||||
|
|
||||||
// 界面元素
|
// 界面元素
|
||||||
cursor: '#fff', // 光标
|
cursor: '#ffffff', // 光标
|
||||||
selection: '#0865a9aa', // 选中背景
|
selection: '#0865a9', // 选中背景
|
||||||
selectionBlur: '#225377aa', // 失焦选中背景
|
selectionBlur: '#225377', // 失焦选中背景
|
||||||
activeLine: 'rgba(255,255,255,0.04)', // 当前行高亮
|
activeLine: '#ffffff0a', // 当前行高亮
|
||||||
lineNumber: 'rgba(255,255,255, 0.15)', // 行号
|
lineNumber: '#ffffff26', // 行号
|
||||||
activeLineNumber: 'rgba(255,255,255, 0.6)', // 活动行号
|
activeLineNumber: '#ffffff99', // 活动行号
|
||||||
|
|
||||||
// 边框和分割线
|
// 边框和分割线
|
||||||
border: '#1e222a', // 边框色
|
borderColor: '#1e222a', // 边框色
|
||||||
borderLight: 'rgba(255,255,255, 0.1)', // 浅色边框
|
borderLight: '#ffffff19', // 浅色边框
|
||||||
|
|
||||||
// 搜索和匹配
|
// 搜索和匹配
|
||||||
searchMatch: '#8fbcbb', // 搜索匹配
|
searchMatch: '#8fbcbb', // 搜索匹配
|
||||||
matchingBracket: 'rgba(255,255,255,0.1)', // 匹配括号
|
matchingBracket: '#ffffff19', // 匹配括号
|
||||||
};
|
};
|
||||||
|
|
||||||
const darkTheme = EditorView.theme({
|
// 创建深色主题
|
||||||
|
export function createDarkTheme(colors = defaultDarkColors) {
|
||||||
|
const darkTheme = EditorView.theme({
|
||||||
'&': {
|
'&': {
|
||||||
color: colors.foreground,
|
color: colors.foreground,
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.background,
|
||||||
@@ -177,14 +179,14 @@ const darkTheme = EditorView.theme({
|
|||||||
},
|
},
|
||||||
'.code-blocks-layer .block-even': {
|
'.code-blocks-layer .block-even': {
|
||||||
background: colors.background,
|
background: colors.background,
|
||||||
borderTop: `1px solid ${colors.border}`,
|
borderTop: `1px solid ${colors.borderColor}`,
|
||||||
},
|
},
|
||||||
'.code-blocks-layer .block-even:first-child': {
|
'.code-blocks-layer .block-even:first-child': {
|
||||||
borderTop: 'none',
|
borderTop: 'none',
|
||||||
},
|
},
|
||||||
'.code-blocks-layer .block-odd': {
|
'.code-blocks-layer .block-odd': {
|
||||||
background: colors.backgroundSecondary,
|
background: colors.backgroundSecondary,
|
||||||
borderTop: `1px solid ${colors.border}`,
|
borderTop: `1px solid ${colors.borderColor}`,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 代码块开始标记
|
// 代码块开始标记
|
||||||
@@ -195,10 +197,10 @@ const darkTheme = EditorView.theme({
|
|||||||
'.code-block-start.first': {
|
'.code-block-start.first': {
|
||||||
height: '0px',
|
height: '0px',
|
||||||
},
|
},
|
||||||
}, {dark: true});
|
}, {dark: true});
|
||||||
|
|
||||||
// 语法高亮样式
|
// 语法高亮样式
|
||||||
const darkHighlightStyle = HighlightStyle.define([
|
const darkHighlightStyle = HighlightStyle.define([
|
||||||
{tag: tags.keyword, color: colors.keyword},
|
{tag: tags.keyword, color: colors.keyword},
|
||||||
{tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], color: colors.variable},
|
{tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], color: colors.variable},
|
||||||
{tag: [tags.variableName], color: colors.variable},
|
{tag: [tags.variableName], color: colors.variable},
|
||||||
@@ -229,9 +231,13 @@ const darkHighlightStyle = HighlightStyle.define([
|
|||||||
{tag: [tags.heading1, tags.heading2], fontSize: '1.4em'},
|
{tag: [tags.heading1, tags.heading2], fontSize: '1.4em'},
|
||||||
{tag: [tags.heading3, tags.heading4], fontSize: '1.2em'},
|
{tag: [tags.heading3, tags.heading4], fontSize: '1.2em'},
|
||||||
{tag: [tags.heading5, tags.heading6], fontSize: '1.1em'},
|
{tag: [tags.heading5, tags.heading6], fontSize: '1.1em'},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const dark = [
|
return [
|
||||||
darkTheme,
|
darkTheme,
|
||||||
syntaxHighlighting(darkHighlightStyle),
|
syntaxHighlighting(darkHighlightStyle),
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认深色主题
|
||||||
|
export const dark = createDarkTheme(defaultDarkColors);
|
||||||
@@ -2,10 +2,10 @@ import { EditorView } from '@codemirror/view';
|
|||||||
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||||
import { tags } from '@lezer/highlight';
|
import { tags } from '@lezer/highlight';
|
||||||
|
|
||||||
const colors = {
|
// 默认浅色主题颜色
|
||||||
|
export const defaultLightColors = {
|
||||||
// 基础色调
|
// 基础色调
|
||||||
background: '#ffffff', // 主背景色
|
background: '#ffffff', // 主背景色
|
||||||
// backgroundAlt: '#f4f8f4', // 交替背景色
|
|
||||||
backgroundSecondary: '#f1faf1', // 次要背景色
|
backgroundSecondary: '#f1faf1', // 次要背景色
|
||||||
surface: '#f5f5f5', // 面板背景
|
surface: '#f5f5f5', // 面板背景
|
||||||
|
|
||||||
@@ -24,24 +24,25 @@ const colors = {
|
|||||||
type: '#6f42c1', // 类型
|
type: '#6f42c1', // 类型
|
||||||
|
|
||||||
// 界面元素
|
// 界面元素
|
||||||
cursor: '#000', // 光标
|
cursor: '#000000', // 光标
|
||||||
selection: '#77baff8c', // 选中背景
|
selection: '#77baff', // 选中背景
|
||||||
selectionBlur: '#b2c2ca85', // 失焦选中背景
|
selectionBlur: '#b2c2ca', // 失焦选中背景
|
||||||
activeLine: 'rgba(0,0,0, 0.04)', // 当前行高亮
|
activeLine: '#0000000a', // 当前行高亮
|
||||||
lineNumber: 'rgba(0,0,0, 0.25)', // 行号
|
lineNumber: '#00000040', // 行号
|
||||||
activeLineNumber: 'rgba(0,0,0, 0.6)', // 活动行号
|
activeLineNumber: '#000000aa', // 活动行号
|
||||||
|
|
||||||
// 边框和分割线
|
// 边框和分割线
|
||||||
border: '#dfdfdf', // 边框色
|
borderColor: '#dfdfdf', // 边框色
|
||||||
borderLight: 'rgba(0,0,0, 0.05)', // 浅色边框
|
borderLight: '#0000000c', // 浅色边框
|
||||||
|
|
||||||
// 搜索和匹配
|
// 搜索和匹配
|
||||||
searchMatch: '#005cc5', // 搜索匹配
|
searchMatch: '#005cc5', // 搜索匹配
|
||||||
matchingBracket: 'rgba(0,0,0,0.1)', // 匹配括号
|
matchingBracket: '#00000019', // 匹配括号
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const lightTheme = EditorView.theme({
|
// 创建浅色主题
|
||||||
|
export function createLightTheme(colors = defaultLightColors) {
|
||||||
|
const lightTheme = EditorView.theme({
|
||||||
'&': {
|
'&': {
|
||||||
color: colors.foreground,
|
color: colors.foreground,
|
||||||
backgroundColor: colors.background,
|
backgroundColor: colors.background,
|
||||||
@@ -180,14 +181,14 @@ const lightTheme = EditorView.theme({
|
|||||||
},
|
},
|
||||||
'.code-blocks-layer .block-even': {
|
'.code-blocks-layer .block-even': {
|
||||||
background: colors.background,
|
background: colors.background,
|
||||||
borderTop: `1px solid ${colors.border}`,
|
borderTop: `1px solid ${colors.borderColor}`,
|
||||||
},
|
},
|
||||||
'.code-blocks-layer .block-even:first-child': {
|
'.code-blocks-layer .block-even:first-child': {
|
||||||
borderTop: 'none',
|
borderTop: 'none',
|
||||||
},
|
},
|
||||||
'.code-blocks-layer .block-odd': {
|
'.code-blocks-layer .block-odd': {
|
||||||
background: colors.backgroundSecondary,
|
background: colors.backgroundSecondary,
|
||||||
borderTop: `1px solid ${colors.border}`,
|
borderTop: `1px solid ${colors.borderColor}`,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 代码块开始标记
|
// 代码块开始标记
|
||||||
@@ -197,10 +198,10 @@ const lightTheme = EditorView.theme({
|
|||||||
'.code-block-start.first': {
|
'.code-block-start.first': {
|
||||||
height: '0px',
|
height: '0px',
|
||||||
},
|
},
|
||||||
}, { dark: false });
|
}, { dark: false });
|
||||||
|
|
||||||
// 语法高亮样式
|
// 语法高亮样式
|
||||||
const lightHighlightStyle = HighlightStyle.define([
|
const lightHighlightStyle = HighlightStyle.define([
|
||||||
{ tag: tags.keyword, color: colors.keyword },
|
{ tag: tags.keyword, color: colors.keyword },
|
||||||
{ tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], color: colors.variable },
|
{ tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], color: colors.variable },
|
||||||
{ tag: [tags.variableName], color: colors.variable },
|
{ tag: [tags.variableName], color: colors.variable },
|
||||||
@@ -231,9 +232,13 @@ const lightHighlightStyle = HighlightStyle.define([
|
|||||||
{ tag: [tags.heading1, tags.heading2], fontSize: '1.4em' },
|
{ tag: [tags.heading1, tags.heading2], fontSize: '1.4em' },
|
||||||
{ tag: [tags.heading3, tags.heading4], fontSize: '1.2em' },
|
{ tag: [tags.heading3, tags.heading4], fontSize: '1.2em' },
|
||||||
{ tag: [tags.heading5, tags.heading6], fontSize: '1.1em' },
|
{ tag: [tags.heading5, tags.heading6], fontSize: '1.1em' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const light = [
|
return [
|
||||||
lightTheme,
|
lightTheme,
|
||||||
syntaxHighlighting(lightHighlightStyle),
|
syntaxHighlighting(lightHighlightStyle),
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认浅色主题
|
||||||
|
export const light = createLightTheme(defaultLightColors);
|
||||||
@@ -13,6 +13,7 @@ const navItems = [
|
|||||||
{ id: 'general', icon: '⚙️', route: '/settings/general' },
|
{ id: 'general', icon: '⚙️', route: '/settings/general' },
|
||||||
{ id: 'editing', icon: '✏️', route: '/settings/editing' },
|
{ id: 'editing', icon: '✏️', route: '/settings/editing' },
|
||||||
{ id: 'appearance', icon: '🎨', route: '/settings/appearance' },
|
{ id: 'appearance', icon: '🎨', route: '/settings/appearance' },
|
||||||
|
{ id: 'backupPage', icon: '🔗', route: '/settings/backup' },
|
||||||
{ id: 'extensions', icon: '🧩', route: '/settings/extensions' },
|
{ id: 'extensions', icon: '🧩', route: '/settings/extensions' },
|
||||||
{ id: 'keyBindings', icon: '⌨️', route: '/settings/key-bindings' },
|
{ id: 'keyBindings', icon: '⌨️', route: '/settings/key-bindings' },
|
||||||
{ id: 'updates', icon: '🔄', route: '/settings/updates' }
|
{ id: 'updates', icon: '🔄', route: '/settings/updates' }
|
||||||
@@ -194,8 +195,8 @@ const goBackToEditor = async () => {
|
|||||||
.settings-content {
|
.settings-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 20px;
|
padding: 0 20px;
|
||||||
overflow-y: auto;
|
overflow-y: scroll;
|
||||||
background-color: var(--settings-bg);
|
background-color: var(--settings-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ defineProps<{
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="setting-section">
|
<div class="setting-section">
|
||||||
|
<div class="section-header">
|
||||||
<h2 class="section-title">{{ title }}</h2>
|
<h2 class="section-title">{{ title }}</h2>
|
||||||
|
<div class="section-title-right">
|
||||||
|
<slot name="title-right"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="section-content">
|
<div class="section-content">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
@@ -22,14 +27,25 @@ defineProps<{
|
|||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
border: 1px solid var(--settings-border);
|
border: 1px solid var(--settings-border);
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background-color: var(--settings-hover);
|
||||||
|
border-bottom: 1px solid var(--settings-border);
|
||||||
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 10px 14px;
|
|
||||||
background-color: var(--settings-hover);
|
|
||||||
color: var(--settings-text);
|
color: var(--settings-text);
|
||||||
border-bottom: 1px solid var(--settings-border);
|
}
|
||||||
|
|
||||||
|
.section-title-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-content {
|
.section-content {
|
||||||
|
|||||||
@@ -2,14 +2,260 @@
|
|||||||
import { useConfigStore } from '@/stores/configStore';
|
import { useConfigStore } from '@/stores/configStore';
|
||||||
import { useThemeStore } from '@/stores/themeStore';
|
import { useThemeStore } from '@/stores/themeStore';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { computed, watch, onMounted, ref } from 'vue';
|
||||||
import SettingSection from '../components/SettingSection.vue';
|
import SettingSection from '../components/SettingSection.vue';
|
||||||
import SettingItem from '../components/SettingItem.vue';
|
import SettingItem from '../components/SettingItem.vue';
|
||||||
import { SystemThemeType, LanguageType } from '@/../bindings/voidraft/internal/models/models';
|
import { SystemThemeType, LanguageType } from '@/../bindings/voidraft/internal/models/models';
|
||||||
|
import { defaultDarkColors } from '@/views/editor/theme/dark';
|
||||||
|
import { defaultLightColors } from '@/views/editor/theme/light';
|
||||||
|
import PickColors from 'vue-pick-colors';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
|
// 添加临时颜色状态
|
||||||
|
const tempColors = ref({
|
||||||
|
darkTheme: { ...defaultDarkColors },
|
||||||
|
lightTheme: { ...defaultLightColors }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 标记是否有未保存的更改
|
||||||
|
const hasUnsavedChanges = ref(false);
|
||||||
|
|
||||||
|
// 重置按钮状态
|
||||||
|
const resetButtonState = ref({
|
||||||
|
confirming: false,
|
||||||
|
timer: null as number | null
|
||||||
|
});
|
||||||
|
|
||||||
|
// 防抖函数
|
||||||
|
const debounce = <T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
wait: number
|
||||||
|
): ((...args: Parameters<T>) => void) => {
|
||||||
|
let timeout: number | undefined;
|
||||||
|
|
||||||
|
return function(...args: Parameters<T>): void {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = window.setTimeout(() => {
|
||||||
|
func(...args);
|
||||||
|
}, wait);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当前激活的主题类型(基于当前系统主题)
|
||||||
|
const activeThemeType = computed(() => {
|
||||||
|
const isDark =
|
||||||
|
themeStore.currentTheme === SystemThemeType.SystemThemeDark ||
|
||||||
|
(themeStore.currentTheme === SystemThemeType.SystemThemeAuto &&
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
|
||||||
|
return isDark ? 'darkTheme' : 'lightTheme';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当前主题的颜色配置 - 使用临时状态
|
||||||
|
const currentColors = computed(() => {
|
||||||
|
const themeType = activeThemeType.value;
|
||||||
|
return tempColors.value[themeType] ||
|
||||||
|
(themeType === 'darkTheme' ? defaultDarkColors : defaultLightColors);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取当前主题模式
|
||||||
|
const currentThemeMode = computed(() => {
|
||||||
|
const isDark =
|
||||||
|
themeStore.currentTheme === SystemThemeType.SystemThemeDark ||
|
||||||
|
(themeStore.currentTheme === SystemThemeType.SystemThemeAuto &&
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
|
||||||
|
return isDark ? 'dark' : 'light';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听主题颜色变更,更新临时颜色
|
||||||
|
watch(
|
||||||
|
() => themeStore.themeColors,
|
||||||
|
(newValue) => {
|
||||||
|
if (!hasUnsavedChanges.value) {
|
||||||
|
tempColors.value = {
|
||||||
|
darkTheme: { ...newValue.darkTheme },
|
||||||
|
lightTheme: { ...newValue.lightTheme }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 初始化时加载主题颜色
|
||||||
|
onMounted(() => {
|
||||||
|
// 使用themeStore中的颜色作为初始值
|
||||||
|
tempColors.value = {
|
||||||
|
darkTheme: { ...themeStore.themeColors.darkTheme },
|
||||||
|
lightTheme: { ...themeStore.themeColors.lightTheme }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 颜色配置分组
|
||||||
|
const colorGroups = computed(() => [
|
||||||
|
{
|
||||||
|
key: 'basic',
|
||||||
|
title: t('settings.themeColors.basic'),
|
||||||
|
colors: [
|
||||||
|
{ key: 'background', label: t('settings.themeColors.background') },
|
||||||
|
{ key: 'backgroundSecondary', label: t('settings.themeColors.backgroundSecondary') },
|
||||||
|
{ key: 'surface', label: t('settings.themeColors.surface') }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'text',
|
||||||
|
title: t('settings.themeColors.text'),
|
||||||
|
colors: [
|
||||||
|
{ key: 'foreground', label: t('settings.themeColors.foreground') },
|
||||||
|
{ key: 'foregroundSecondary', label: t('settings.themeColors.foregroundSecondary') },
|
||||||
|
{ key: 'comment', label: t('settings.themeColors.comment') }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'syntax',
|
||||||
|
title: t('settings.themeColors.syntax'),
|
||||||
|
colors: [
|
||||||
|
{ key: 'keyword', label: t('settings.themeColors.keyword') },
|
||||||
|
{ key: 'string', label: t('settings.themeColors.string') },
|
||||||
|
{ key: 'function', label: t('settings.themeColors.function') },
|
||||||
|
{ key: 'number', label: t('settings.themeColors.number') },
|
||||||
|
{ key: 'operator', label: t('settings.themeColors.operator') },
|
||||||
|
{ key: 'variable', label: t('settings.themeColors.variable') },
|
||||||
|
{ key: 'type', label: t('settings.themeColors.type') }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'interface',
|
||||||
|
title: t('settings.themeColors.interface'),
|
||||||
|
colors: [
|
||||||
|
{ key: 'cursor', label: t('settings.themeColors.cursor') },
|
||||||
|
{ key: 'selection', label: t('settings.themeColors.selection') },
|
||||||
|
{ key: 'selectionBlur', label: t('settings.themeColors.selectionBlur') },
|
||||||
|
{ key: 'activeLine', label: t('settings.themeColors.activeLine') },
|
||||||
|
{ key: 'lineNumber', label: t('settings.themeColors.lineNumber') },
|
||||||
|
{ key: 'activeLineNumber', label: t('settings.themeColors.activeLineNumber') }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'border',
|
||||||
|
title: t('settings.themeColors.border'),
|
||||||
|
colors: [
|
||||||
|
{ key: 'borderColor', label: t('settings.themeColors.borderColor') },
|
||||||
|
{ key: 'borderLight', label: t('settings.themeColors.borderLight') }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'search',
|
||||||
|
title: t('settings.themeColors.search'),
|
||||||
|
colors: [
|
||||||
|
{ key: 'searchMatch', label: t('settings.themeColors.searchMatch') },
|
||||||
|
{ key: 'matchingBracket', label: t('settings.themeColors.matchingBracket') }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 处理重置按钮点击
|
||||||
|
const handleResetClick = () => {
|
||||||
|
if (resetButtonState.value.confirming) {
|
||||||
|
// 如果已经在确认状态,执行重置操作
|
||||||
|
resetCurrentTheme();
|
||||||
|
|
||||||
|
// 重置按钮状态
|
||||||
|
resetButtonState.value.confirming = false;
|
||||||
|
if (resetButtonState.value.timer !== null) {
|
||||||
|
clearTimeout(resetButtonState.value.timer);
|
||||||
|
resetButtonState.value.timer = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 进入确认状态
|
||||||
|
resetButtonState.value.confirming = true;
|
||||||
|
|
||||||
|
// 设置3秒后自动恢复
|
||||||
|
resetButtonState.value.timer = window.setTimeout(() => {
|
||||||
|
resetButtonState.value.confirming = false;
|
||||||
|
resetButtonState.value.timer = null;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置当前主题为默认配置
|
||||||
|
const resetCurrentTheme = debounce(async () => {
|
||||||
|
// 使用themeStore的原子重置操作
|
||||||
|
const themeType = activeThemeType.value;
|
||||||
|
const success = await themeStore.resetThemeColors(themeType);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// 更新临时颜色状态
|
||||||
|
tempColors.value = {
|
||||||
|
darkTheme: { ...themeStore.themeColors.darkTheme },
|
||||||
|
lightTheme: { ...themeStore.themeColors.lightTheme }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 标记没有未保存的更改
|
||||||
|
hasUnsavedChanges.value = false;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
// 更新本地颜色配置 - 仅更新临时状态,不提交到后端
|
||||||
|
const updateLocalColor = (colorKey: string, value: string) => {
|
||||||
|
const themeType = activeThemeType.value;
|
||||||
|
|
||||||
|
// 更新临时颜色
|
||||||
|
tempColors.value = {
|
||||||
|
...tempColors.value,
|
||||||
|
[themeType]: {
|
||||||
|
...tempColors.value[themeType],
|
||||||
|
[colorKey]: value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 标记有未保存的更改
|
||||||
|
hasUnsavedChanges.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 防抖包装的颜色更新函数
|
||||||
|
const updateColor = debounce(updateLocalColor, 100);
|
||||||
|
|
||||||
|
// 应用颜色更改到系统
|
||||||
|
const applyChanges = async () => {
|
||||||
|
try {
|
||||||
|
// 获取当前主题的自定义颜色
|
||||||
|
const customTheme = {
|
||||||
|
darkTheme: tempColors.value.darkTheme,
|
||||||
|
lightTheme: tempColors.value.lightTheme
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新themeStore中的颜色
|
||||||
|
themeStore.updateThemeColors(customTheme.darkTheme, customTheme.lightTheme);
|
||||||
|
|
||||||
|
// 保存到配置
|
||||||
|
await themeStore.saveThemeColors();
|
||||||
|
|
||||||
|
// 刷新编辑器主题
|
||||||
|
themeStore.refreshEditorTheme();
|
||||||
|
|
||||||
|
// 清除未保存标记
|
||||||
|
hasUnsavedChanges.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to apply theme change:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消颜色更改
|
||||||
|
const cancelChanges = () => {
|
||||||
|
// 恢复到themeStore中的颜色
|
||||||
|
tempColors.value = {
|
||||||
|
darkTheme: { ...themeStore.themeColors.darkTheme },
|
||||||
|
lightTheme: { ...themeStore.themeColors.lightTheme }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清除未保存标记
|
||||||
|
hasUnsavedChanges.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
// 语言选项
|
// 语言选项
|
||||||
const languageOptions = [
|
const languageOptions = [
|
||||||
{ value: LanguageType.LangZhCN, label: t('languages.zh-CN') },
|
{ value: LanguageType.LangZhCN, label: t('languages.zh-CN') },
|
||||||
@@ -38,6 +284,24 @@ const updateSystemTheme = async (event: Event) => {
|
|||||||
|
|
||||||
await themeStore.setTheme(selectedSystemTheme);
|
await themeStore.setTheme(selectedSystemTheme);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 控制颜色选择器显示状态
|
||||||
|
const showPickerMap = ref<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
// 切换颜色选择器显示状态
|
||||||
|
const toggleColorPicker = (colorKey: string) => {
|
||||||
|
showPickerMap.value[colorKey] = !showPickerMap.value[colorKey];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 颜色变更处理
|
||||||
|
const handleColorChange = (colorKey: string, value: string) => {
|
||||||
|
updateColor(colorKey, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 颜色选择器关闭处理
|
||||||
|
const handlePickerClose = () => {
|
||||||
|
// 可以在此添加额外的逻辑
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -61,6 +325,70 @@ const updateSystemTheme = async (event: Event) => {
|
|||||||
</select>
|
</select>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|
||||||
|
<!-- 自定义主题颜色配置 -->
|
||||||
|
<SettingSection :title="t('settings.customThemeColors')">
|
||||||
|
<template #title-right>
|
||||||
|
<div class="theme-controls">
|
||||||
|
<button
|
||||||
|
v-if="!hasUnsavedChanges"
|
||||||
|
:class="['reset-button', resetButtonState.confirming ? 'reset-button-confirming' : '']"
|
||||||
|
@click="handleResetClick"
|
||||||
|
>
|
||||||
|
{{ resetButtonState.confirming ? t('settings.confirmReset') : t('settings.resetToDefault') }}
|
||||||
|
</button>
|
||||||
|
<template v-else>
|
||||||
|
<button class="apply-button" @click="applyChanges">
|
||||||
|
{{ t('settings.apply') }}
|
||||||
|
</button>
|
||||||
|
<button class="cancel-button" @click="cancelChanges">
|
||||||
|
{{ t('settings.cancel') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="color-groups">
|
||||||
|
<div v-for="group in colorGroups" :key="group.key" class="color-group">
|
||||||
|
<h4 class="group-title">{{ group.title }}</h4>
|
||||||
|
<div class="color-items">
|
||||||
|
<SettingItem
|
||||||
|
v-for="color in group.colors"
|
||||||
|
:key="color.key"
|
||||||
|
:title="color.label"
|
||||||
|
class="color-setting-item"
|
||||||
|
>
|
||||||
|
<div class="color-input-wrapper">
|
||||||
|
<div class="color-picker-wrapper">
|
||||||
|
<PickColors
|
||||||
|
v-model:value="currentColors[color.key]"
|
||||||
|
v-model:show-picker="showPickerMap[color.key]"
|
||||||
|
:size="28"
|
||||||
|
show-alpha
|
||||||
|
:theme="currentThemeMode"
|
||||||
|
:colors="[]"
|
||||||
|
format="hex"
|
||||||
|
:format-options="['rgb', 'hex', 'hsl', 'hsv']"
|
||||||
|
placement="bottom"
|
||||||
|
position="absolute"
|
||||||
|
:z-index="1000"
|
||||||
|
@change="(val) => handleColorChange(color.key, val)"
|
||||||
|
@close-picker="handlePickerClose"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
:value="currentColors[color.key] || ''"
|
||||||
|
@input="updateColor(color.key, ($event.target as HTMLInputElement).value)"
|
||||||
|
class="color-text-input"
|
||||||
|
:placeholder="t('settings.colorValue')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingSection>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -95,4 +423,146 @@ const updateSystemTheme = async (event: Event) => {
|
|||||||
color: var(--settings-text);
|
color: var(--settings-text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 主题控制区域
|
||||||
|
.theme-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主题颜色配置样式
|
||||||
|
.reset-button, .apply-button, .cancel-button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid var(--settings-input-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button {
|
||||||
|
background-color: var(--settings-button-bg);
|
||||||
|
color: var(--settings-button-text);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--settings-button-hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.reset-button-confirming {
|
||||||
|
background-color: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
border-color: #c0392b;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #c0392b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.apply-button {
|
||||||
|
background-color: #4a9eff;
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #3a8eef;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-button {
|
||||||
|
background-color: var(--settings-button-bg);
|
||||||
|
color: var(--settings-button-text);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--settings-button-hover-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-groups {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-group {
|
||||||
|
.group-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--settings-text);
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
border-bottom: 1px solid var(--settings-input-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-items {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-setting-item {
|
||||||
|
:deep(.setting-item-content) {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.setting-item-title) {
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 28px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
:deep(.pick-colors-trigger) {
|
||||||
|
border: 1px solid var(--settings-input-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-text-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 160px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid var(--settings-input-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--settings-input-bg);
|
||||||
|
color: var(--settings-text);
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
height: 28px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--settings-text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -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>
|
||||||
@@ -266,6 +266,7 @@ const currentVersion = computed(() => {
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
border-top: 1px solid var(--settings-border, rgba(0,0,0,0.1));
|
border-top: 1px solid var(--settings-border, rgba(0,0,0,0.1));
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
.notes-title {
|
.notes-title {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -278,24 +279,76 @@ const currentVersion = computed(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--settings-text);
|
color: var(--settings-text);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
/* Markdown内容样式 */
|
/* Markdown内容样式 */
|
||||||
:deep(p) {
|
:deep(p) {
|
||||||
margin: 0 0 6px 0;
|
margin: 0 0 6px 0;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(ul), :deep(ol) {
|
:deep(ul), :deep(ol) {
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(li) {
|
:deep(li) {
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
|
:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
|
||||||
margin: 10px 0 6px 0;
|
margin: 10px 0 6px 0;
|
||||||
font-size: 13px;
|
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 (
|
require (
|
||||||
github.com/Masterminds/semver/v3 v3.4.0
|
github.com/Masterminds/semver/v3 v3.4.0
|
||||||
github.com/creativeprojects/go-selfupdate v1.5.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/parsers/json v1.0.0
|
||||||
github.com/knadh/koanf/providers/file v1.2.0
|
github.com/knadh/koanf/providers/file v1.2.0
|
||||||
github.com/knadh/koanf/providers/structs v1.0.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/robertkrimen/otto v0.5.1
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.10
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.25
|
||||||
golang.org/x/net v0.41.0
|
golang.org/x/net v0.43.0
|
||||||
golang.org/x/sys v0.33.0
|
golang.org/x/sys v0.35.0
|
||||||
golang.org/x/text v0.26.0
|
golang.org/x/text v0.28.0
|
||||||
|
modernc.org/sqlite v1.38.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -35,9 +37,8 @@ require (
|
|||||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.6.2 // 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-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/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/google/go-github/v30 v30.1.0 // indirect
|
github.com/google/go-github/v30 v30.1.0 // indirect
|
||||||
@@ -71,16 +72,15 @@ require (
|
|||||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
github.com/xanzy/go-gitlab v0.115.0 // indirect
|
github.com/xanzy/go-gitlab v0.115.0 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
golang.org/x/crypto v0.39.0 // indirect
|
golang.org/x/crypto v0.41.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||||
golang.org/x/oauth2 v0.30.0 // indirect
|
golang.org/x/oauth2 v0.30.0 // indirect
|
||||||
golang.org/x/time v0.12.0 // indirect
|
golang.org/x/time v0.12.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // 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/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // 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-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 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
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.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
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 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 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/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 h1:DznjB7NQykhqCar2LvNug3MuxEQsZ5KvfgMbio+23u4=
|
||||||
github.com/knadh/koanf/providers/structs v1.0.0/go.mod h1:kjo5TFtgpaZORlpoJqcbeLowM2cINodv8kX+oFAeQ1w=
|
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.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A=
|
||||||
github.com/knadh/koanf/v2 v2.2.1/go.mod h1:PSFru3ufQgTsI7IF+95rf9s8XA1+aHxKuO/W+dPoHEY=
|
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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
@@ -164,8 +164,8 @@ github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2
|
|||||||
github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
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 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
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.25 h1:o05zUiPEvmrq2lqqCs4wqnrnAjGmhryYHRhjQmtkvk8=
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.10/go.mod h1:4LCCW7s9e4PuSmu7l9OTvfWIGMO8TaSiftSeR5NpBIc=
|
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 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
|
||||||
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
|
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
|
||||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
github.com/xanzy/ssh-agent v0.3.3 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-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-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.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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
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-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-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-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-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
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.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 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-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.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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
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 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
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.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
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=
|
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=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
|
||||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
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 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
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.15 h1:rJAXTP6ilMW/1+kzDiqmBlHLWszheUFXIyGQIAvjJpY=
|
||||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
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 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
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 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
modernc.org/libc v1.66.7 h1:rjhZ8OSCybKWxS1CJr0hikpEi6Vg+944Ouyrd+bQsoY=
|
||||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
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 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 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/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
|
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||||
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
|
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 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 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"`
|
||||||
|
}
|
||||||
@@ -123,6 +123,7 @@ type AppConfig struct {
|
|||||||
Editing EditingConfig `json:"editing"` // 编辑设置
|
Editing EditingConfig `json:"editing"` // 编辑设置
|
||||||
Appearance AppearanceConfig `json:"appearance"` // 外观设置
|
Appearance AppearanceConfig `json:"appearance"` // 外观设置
|
||||||
Updates UpdatesConfig `json:"updates"` // 更新设置
|
Updates UpdatesConfig `json:"updates"` // 更新设置
|
||||||
|
Backup GitBackupConfig `json:"backup"` // Git备份设置
|
||||||
Metadata ConfigMetadata `json:"metadata"` // 配置元数据
|
Metadata ConfigMetadata `json:"metadata"` // 配置元数据
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,17 +165,17 @@ func NewDefaultAppConfig() *AppConfig {
|
|||||||
TabSize: 4,
|
TabSize: 4,
|
||||||
TabType: TabTypeTab,
|
TabType: TabTypeTab,
|
||||||
// 保存选项
|
// 保存选项
|
||||||
AutoSaveDelay: 2000, // 2秒后自动保存
|
AutoSaveDelay: 2000,
|
||||||
},
|
},
|
||||||
Appearance: AppearanceConfig{
|
Appearance: AppearanceConfig{
|
||||||
Language: LangEnUS,
|
Language: LangEnUS,
|
||||||
SystemTheme: SystemThemeAuto, // 默认使用深色系统主题
|
SystemTheme: SystemThemeAuto,
|
||||||
},
|
},
|
||||||
Updates: UpdatesConfig{
|
Updates: UpdatesConfig{
|
||||||
Version: "1.0.0",
|
Version: "1.3.0",
|
||||||
AutoUpdate: true,
|
AutoUpdate: true,
|
||||||
PrimarySource: UpdateSourceGithub,
|
PrimarySource: UpdateSourceGitea,
|
||||||
BackupSource: UpdateSourceGitea,
|
BackupSource: UpdateSourceGithub,
|
||||||
BackupBeforeUpdate: true,
|
BackupBeforeUpdate: true,
|
||||||
UpdateTimeout: 30,
|
UpdateTimeout: 30,
|
||||||
Github: GithubConfig{
|
Github: GithubConfig{
|
||||||
@@ -187,9 +188,20 @@ func NewDefaultAppConfig() *AppConfig {
|
|||||||
Repo: "voidraft",
|
Repo: "voidraft",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Backup: GitBackupConfig{
|
||||||
|
Enabled: false,
|
||||||
|
RepoURL: "",
|
||||||
|
AuthMethod: UserPass,
|
||||||
|
Username: "",
|
||||||
|
Password: "",
|
||||||
|
Token: "",
|
||||||
|
SSHKeyPath: "",
|
||||||
|
BackupInterval: 60,
|
||||||
|
AutoBackup: false,
|
||||||
|
},
|
||||||
Metadata: ConfigMetadata{
|
Metadata: ConfigMetadata{
|
||||||
LastUpdated: time.Now().Format(time.RFC3339),
|
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"`
|
Content string `json:"content" db:"content"`
|
||||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_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 {
|
func NewDocument(title, content string) *Document {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
return &Document{
|
return &Document{
|
||||||
@@ -23,6 +24,7 @@ func NewDocument(title, content string) *Document {
|
|||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
IsDeleted: false,
|
IsDeleted: false,
|
||||||
|
IsLocked: false, // 默认不锁定
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import "time"
|
|||||||
|
|
||||||
// Extension 单个扩展配置
|
// Extension 单个扩展配置
|
||||||
type Extension struct {
|
type Extension struct {
|
||||||
ID ExtensionID `json:"id"` // 扩展唯一标识
|
ID ExtensionID `json:"id" db:"id"` // 扩展唯一标识
|
||||||
Enabled bool `json:"enabled"` // 是否启用
|
Enabled bool `json:"enabled" db:"enabled"` // 是否启用
|
||||||
IsDefault bool `json:"isDefault"` // 是否为默认扩展
|
IsDefault bool `json:"isDefault" db:"is_default"` // 是否为默认扩展
|
||||||
Config ExtensionConfig `json:"config"` // 扩展配置项
|
Config ExtensionConfig `json:"config" db:"config"` // 扩展配置项
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtensionID 扩展标识符
|
// ExtensionID 扩展标识符
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import "time"
|
|||||||
|
|
||||||
// KeyBinding 单个快捷键绑定
|
// KeyBinding 单个快捷键绑定
|
||||||
type KeyBinding struct {
|
type KeyBinding struct {
|
||||||
Command KeyBindingCommand `json:"command"` // 快捷键动作
|
Command KeyBindingCommand `json:"command" db:"command"` // 快捷键动作
|
||||||
Extension ExtensionID `json:"extension"` // 所属扩展
|
Extension ExtensionID `json:"extension" db:"extension"` // 所属扩展
|
||||||
Key string `json:"key"` // 快捷键组合(如 "Mod-f", "Ctrl-Shift-p")
|
Key string `json:"key" db:"key"` // 快捷键组合(如 "Mod-f", "Ctrl-Shift-p")
|
||||||
Enabled bool `json:"enabled"` // 是否启用
|
Enabled bool `json:"enabled" db:"enabled"` // 是否启用
|
||||||
IsDefault bool `json:"isDefault"` // 是否为默认快捷键
|
IsDefault bool `json:"isDefault" db:"is_default"` // 是否为默认快捷键
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyBindingCommand 快捷键命令
|
// KeyBindingCommand 快捷键命令
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
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 {
|
||||||
|
// 基础色调
|
||||||
|
Background string `json:"background"` // 主背景色
|
||||||
|
BackgroundSecondary string `json:"backgroundSecondary"` // 次要背景色
|
||||||
|
Surface string `json:"surface"` // 面板背景
|
||||||
|
Foreground string `json:"foreground"` // 主文本色
|
||||||
|
ForegroundSecondary string `json:"foregroundSecondary"` // 次要文本色
|
||||||
|
|
||||||
|
// 语法高亮
|
||||||
|
Comment string `json:"comment"` // 注释色
|
||||||
|
Keyword string `json:"keyword"` // 关键字
|
||||||
|
String string `json:"string"` // 字符串
|
||||||
|
Function string `json:"function"` // 函数名
|
||||||
|
Number string `json:"number"` // 数字
|
||||||
|
Operator string `json:"operator"` // 操作符
|
||||||
|
Variable string `json:"variable"` // 变量
|
||||||
|
Type string `json:"type"` // 类型
|
||||||
|
|
||||||
|
// 界面元素
|
||||||
|
Cursor string `json:"cursor"` // 光标
|
||||||
|
Selection string `json:"selection"` // 选中背景
|
||||||
|
SelectionBlur string `json:"selectionBlur"` // 失焦选中背景
|
||||||
|
ActiveLine string `json:"activeLine"` // 当前行高亮
|
||||||
|
LineNumber string `json:"lineNumber"` // 行号
|
||||||
|
ActiveLineNumber string `json:"activeLineNumber"` // 活动行号
|
||||||
|
|
||||||
|
// 边框分割线
|
||||||
|
BorderColor string `json:"borderColor"` // 边框色
|
||||||
|
BorderLight string `json:"borderLight"` // 浅色边框
|
||||||
|
|
||||||
|
// 搜索匹配
|
||||||
|
SearchMatch string `json:"searchMatch"` // 搜索匹配
|
||||||
|
MatchingBracket string `json:"matchingBracket"` // 匹配括号
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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{
|
||||||
|
// 基础色调
|
||||||
|
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: "#ffffff",
|
||||||
|
Selection: "#0865a9",
|
||||||
|
SelectionBlur: "#225377",
|
||||||
|
ActiveLine: "#ffffff0a",
|
||||||
|
LineNumber: "#ffffff26",
|
||||||
|
ActiveLineNumber: "#ffffff99",
|
||||||
|
|
||||||
|
// 边框分割线
|
||||||
|
BorderColor: "#1e222a",
|
||||||
|
BorderLight: "#ffffff1a",
|
||||||
|
|
||||||
|
// 搜索匹配
|
||||||
|
SearchMatch: "#8fbcbb",
|
||||||
|
MatchingBracket: "#ffffff1a",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultLightTheme 创建默认浅色主题配置
|
||||||
|
func NewDefaultLightTheme() *ThemeColorConfig {
|
||||||
|
return &ThemeColorConfig{
|
||||||
|
// 基础色调
|
||||||
|
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: "#000000",
|
||||||
|
Selection: "#77baff",
|
||||||
|
SelectionBlur: "#b2c2ca",
|
||||||
|
ActiveLine: "#0000000a",
|
||||||
|
LineNumber: "#00000040",
|
||||||
|
ActiveLineNumber: "#000000aa",
|
||||||
|
|
||||||
|
// 边框分割线
|
||||||
|
BorderColor: "#dfdfdf",
|
||||||
|
BorderLight: "#0000000c",
|
||||||
|
|
||||||
|
// 搜索匹配
|
||||||
|
SearchMatch: "#005cc5",
|
||||||
|
MatchingBracket: "#00000019",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 (
|
const (
|
||||||
// CurrentAppConfigVersion 当前应用配置版本
|
// CurrentAppConfigVersion 当前应用配置版本
|
||||||
CurrentAppConfigVersion = "1.0.0"
|
CurrentAppConfigVersion = "1.3.0"
|
||||||
// BackupFilePattern 备份文件名模式
|
// BackupFilePattern 备份文件名模式
|
||||||
BackupFilePattern = "%s.backup.%s.json"
|
BackupFilePattern = "%s.backup.%s.json"
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ type Migratable interface {
|
|||||||
|
|
||||||
// ConfigMigrationService 配置迁移服务
|
// ConfigMigrationService 配置迁移服务
|
||||||
type ConfigMigrationService[T Migratable] struct {
|
type ConfigMigrationService[T Migratable] struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
configDir string
|
configDir string
|
||||||
configName string
|
configName string
|
||||||
targetVersion string
|
targetVersion string
|
||||||
@@ -54,7 +54,7 @@ type MigrationResult struct {
|
|||||||
|
|
||||||
// NewConfigMigrationService 创建配置迁移服务
|
// NewConfigMigrationService 创建配置迁移服务
|
||||||
func NewConfigMigrationService[T Migratable](
|
func NewConfigMigrationService[T Migratable](
|
||||||
logger *log.Service,
|
logger *log.LogService,
|
||||||
configDir string,
|
configDir string,
|
||||||
configName, targetVersion, configPath string,
|
configName, targetVersion, configPath string,
|
||||||
) *ConfigMigrationService[T] {
|
) *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](
|
return NewConfigMigrationService[*models.AppConfig](
|
||||||
logger, configDir, "settings", CurrentAppConfigVersion, settingsPath)
|
logger, configDir, "settings", CurrentAppConfigVersion, settingsPath)
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,8 @@ const (
|
|||||||
ConfigChangeTypeHotkey ConfigChangeType = "hotkey"
|
ConfigChangeTypeHotkey ConfigChangeType = "hotkey"
|
||||||
// ConfigChangeTypeDataPath 数据路径配置变更
|
// ConfigChangeTypeDataPath 数据路径配置变更
|
||||||
ConfigChangeTypeDataPath ConfigChangeType = "datapath"
|
ConfigChangeTypeDataPath ConfigChangeType = "datapath"
|
||||||
|
// ConfigChangeTypeBackup 备份配置变更
|
||||||
|
ConfigChangeTypeBackup ConfigChangeType = "backup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigChangeCallback 配置变更回调函数类型
|
// ConfigChangeCallback 配置变更回调函数类型
|
||||||
@@ -49,7 +51,7 @@ type ConfigListener struct {
|
|||||||
type ConfigNotificationService struct {
|
type ConfigNotificationService struct {
|
||||||
listeners map[ConfigChangeType][]*ConfigListener // 支持多监听器的map
|
listeners map[ConfigChangeType][]*ConfigListener // 支持多监听器的map
|
||||||
mu sync.RWMutex // 监听器map的读写锁
|
mu sync.RWMutex // 监听器map的读写锁
|
||||||
logger *log.Service // 日志服务
|
logger *log.LogService // 日志服务
|
||||||
koanf *koanf.Koanf // koanf实例
|
koanf *koanf.Koanf // koanf实例
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -57,7 +59,7 @@ type ConfigNotificationService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewConfigNotificationService 创建配置通知服务
|
// 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())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
return &ConfigNotificationService{
|
return &ConfigNotificationService{
|
||||||
listeners: make(map[ConfigChangeType][]*ConfigListener),
|
listeners: make(map[ConfigChangeType][]*ConfigListener),
|
||||||
@@ -445,6 +447,29 @@ 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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ServiceShutdown 关闭服务
|
// ServiceShutdown 关闭服务
|
||||||
func (cns *ConfigNotificationService) ServiceShutdown() error {
|
func (cns *ConfigNotificationService) ServiceShutdown() error {
|
||||||
cns.Cleanup()
|
cns.Cleanup()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
// ConfigService 应用配置服务
|
// ConfigService 应用配置服务
|
||||||
type ConfigService struct {
|
type ConfigService struct {
|
||||||
koanf *koanf.Koanf // koanf 实例
|
koanf *koanf.Koanf // koanf 实例
|
||||||
logger *log.Service // 日志服务
|
logger *log.LogService // 日志服务
|
||||||
configDir string // 配置目录
|
configDir string // 配置目录
|
||||||
settingsPath string // 设置文件路径
|
settingsPath string // 设置文件路径
|
||||||
mu sync.RWMutex // 读写锁
|
mu sync.RWMutex // 读写锁
|
||||||
@@ -55,7 +55,7 @@ func (e *ConfigError) Is(target error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewConfigService 创建新的配置服务实例
|
// NewConfigService 创建新的配置服务实例
|
||||||
func NewConfigService(logger *log.Service) *ConfigService {
|
func NewConfigService(logger *log.LogService) *ConfigService {
|
||||||
// 获取用户主目录
|
// 获取用户主目录
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -298,6 +298,16 @@ func (cs *ConfigService) SetDataPathChangeCallback(callback func() error) error
|
|||||||
return cs.notificationService.RegisterListener(dataPathListener)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
// ServiceShutdown 关闭服务
|
// ServiceShutdown 关闭服务
|
||||||
func (cs *ConfigService) ServiceShutdown() error {
|
func (cs *ConfigService) ServiceShutdown() error {
|
||||||
cs.stopWatching()
|
cs.stopWatching()
|
||||||
|
|||||||
@@ -2,14 +2,18 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
"voidraft/internal/models"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -31,7 +35,8 @@ CREATE TABLE IF NOT EXISTS documents (
|
|||||||
content TEXT DEFAULT '∞∞∞text-a',
|
content TEXT DEFAULT '∞∞∞text-a',
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_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
|
// Extensions table
|
||||||
@@ -58,28 +63,70 @@ CREATE TABLE IF NOT EXISTS key_bindings (
|
|||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE(command, extension)
|
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
|
// DatabaseService provides shared database functionality
|
||||||
type DatabaseService struct {
|
type DatabaseService struct {
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
SQLite *sqlite.Service
|
db *sql.DB
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
tableModels []TableModel // 注册的表模型
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDatabaseService creates a new database service
|
// 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 {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DatabaseService{
|
ds := &DatabaseService{
|
||||||
configService: configService,
|
configService: configService,
|
||||||
logger: logger,
|
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
|
// 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)
|
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)
|
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)
|
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)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,10 +196,11 @@ func (ds *DatabaseService) createTables() error {
|
|||||||
sqlCreateDocumentsTable,
|
sqlCreateDocumentsTable,
|
||||||
sqlCreateExtensionsTable,
|
sqlCreateExtensionsTable,
|
||||||
sqlCreateKeyBindingsTable,
|
sqlCreateKeyBindingsTable,
|
||||||
|
sqlCreateThemesTable,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
if err := ds.SQLite.Execute(table); err != nil {
|
if _, err := ds.db.Exec(table); err != nil {
|
||||||
return err
|
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_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_extension ON key_bindings(extension)`,
|
||||||
`CREATE INDEX IF NOT EXISTS idx_key_bindings_enabled ON key_bindings(enabled)`,
|
`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 {
|
for _, index := range indexes {
|
||||||
if err := ds.SQLite.Execute(index); err != nil {
|
if _, err := ds.db.Exec(index); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
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
|
// ServiceShutdown shuts down the service when the application closes
|
||||||
func (ds *DatabaseService) ServiceShutdown() error {
|
func (ds *DatabaseService) ServiceShutdown() error {
|
||||||
return ds.SQLite.Close()
|
if ds.db != nil {
|
||||||
|
return ds.db.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnDataPathChanged handles data path changes
|
// OnDataPathChanged handles data path changes
|
||||||
func (ds *DatabaseService) OnDataPathChanged() error {
|
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 err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 用新路径重新初始化
|
// 用新路径重新初始化
|
||||||
return ds.initDatabase()
|
return ds.initDatabase()
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import (
|
|||||||
|
|
||||||
// DialogService 对话框服务,处理文件选择等对话框操作
|
// DialogService 对话框服务,处理文件选择等对话框操作
|
||||||
type DialogService struct {
|
type DialogService struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
window *application.WebviewWindow // 绑定的窗口
|
window *application.WebviewWindow // 绑定的窗口
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDialogService 创建新的对话框服务实例
|
// NewDialogService 创建新的对话框服务实例
|
||||||
func NewDialogService(logger *log.Service) *DialogService {
|
func NewDialogService(logger *log.LogService) *DialogService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,6 @@ func (ds *DialogService) SelectDirectory() (string, error) {
|
|||||||
|
|
||||||
// 对话框文本配置
|
// 对话框文本配置
|
||||||
Title: "Select Directory",
|
Title: "Select Directory",
|
||||||
Message: "Select the folder where you want to store your app data",
|
|
||||||
ButtonText: "Select",
|
ButtonText: "Select",
|
||||||
|
|
||||||
// 不设置过滤器,因为我们选择目录
|
// 不设置过滤器,因为我们选择目录
|
||||||
@@ -69,3 +68,44 @@ func (ds *DialogService) SelectDirectory() (string, error) {
|
|||||||
}
|
}
|
||||||
return path, nil
|
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
|
// Document operations
|
||||||
sqlGetDocumentByID = `
|
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
|
FROM documents
|
||||||
WHERE id = ?`
|
WHERE id = ?`
|
||||||
|
|
||||||
sqlInsertDocument = `
|
sqlInsertDocument = `
|
||||||
INSERT INTO documents (title, content, created_at, updated_at, is_deleted)
|
INSERT INTO documents (title, content, created_at, updated_at, is_deleted, is_locked)
|
||||||
VALUES (?, ?, ?, ?, 0)`
|
VALUES (?, ?, ?, ?, 0, 0)`
|
||||||
|
|
||||||
sqlUpdateDocumentContent = `
|
sqlUpdateDocumentContent = `
|
||||||
UPDATE documents
|
UPDATE documents
|
||||||
SET content = ?, updated_at = ?
|
SET content = ?, updated_at = ?
|
||||||
WHERE id = ?`
|
WHERE id = ? AND is_deleted = 0`
|
||||||
|
|
||||||
sqlUpdateDocumentTitle = `
|
sqlUpdateDocumentTitle = `
|
||||||
UPDATE documents
|
UPDATE documents
|
||||||
SET title = ?, updated_at = ?
|
SET title = ?, updated_at = ?
|
||||||
WHERE id = ?`
|
WHERE id = ? AND is_deleted = 0`
|
||||||
|
|
||||||
sqlMarkDocumentAsDeleted = `
|
sqlMarkDocumentAsDeleted = `
|
||||||
UPDATE documents
|
UPDATE documents
|
||||||
SET is_deleted = 1, updated_at = ?
|
SET is_deleted = 1, updated_at = ?
|
||||||
WHERE id = ?`
|
WHERE id = ? AND is_locked = 0`
|
||||||
|
|
||||||
sqlRestoreDocument = `
|
sqlRestoreDocument = `
|
||||||
UPDATE documents
|
UPDATE documents
|
||||||
@@ -47,13 +47,13 @@ SET is_deleted = 0, updated_at = ?
|
|||||||
WHERE id = ?`
|
WHERE id = ?`
|
||||||
|
|
||||||
sqlListAllDocumentsMeta = `
|
sqlListAllDocumentsMeta = `
|
||||||
SELECT id, title, created_at, updated_at
|
SELECT id, title, created_at, updated_at, is_locked
|
||||||
FROM documents
|
FROM documents
|
||||||
WHERE is_deleted = 0
|
WHERE is_deleted = 0
|
||||||
ORDER BY updated_at DESC`
|
ORDER BY updated_at DESC`
|
||||||
|
|
||||||
sqlListDeletedDocumentsMeta = `
|
sqlListDeletedDocumentsMeta = `
|
||||||
SELECT id, title, created_at, updated_at
|
SELECT id, title, created_at, updated_at, is_locked
|
||||||
FROM documents
|
FROM documents
|
||||||
WHERE is_deleted = 1
|
WHERE is_deleted = 1
|
||||||
ORDER BY updated_at DESC`
|
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`
|
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
|
sqlDefaultDocumentID = 1 // 默认文档的ID
|
||||||
)
|
)
|
||||||
|
|
||||||
// DocumentService provides document management functionality
|
// DocumentService provides document management functionality
|
||||||
type DocumentService struct {
|
type DocumentService struct {
|
||||||
databaseService *DatabaseService
|
databaseService *DatabaseService
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDocumentService creates a new document service
|
// 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 {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DocumentService{
|
ds := &DocumentService{
|
||||||
databaseService: databaseService,
|
databaseService: databaseService,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ds
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceStartup initializes the service when the application starts
|
// ServiceStartup initializes the service when the application starts
|
||||||
func (ds *DocumentService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
func (ds *DocumentService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||||
ds.ctx = ctx
|
ds.ctx = ctx
|
||||||
// Ensure default document exists
|
|
||||||
|
// 确保默认文档存在
|
||||||
if err := ds.ensureDefaultDocument(); err != nil {
|
if err := ds.ensureDefaultDocument(); err != nil {
|
||||||
return fmt.Errorf("failed to ensure default document: %w", err)
|
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
|
// ensureDefaultDocument ensures a default document exists
|
||||||
func (ds *DocumentService) ensureDefaultDocument() error {
|
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
|
// 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 {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to query document count: %w", 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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no documents exist, create default document
|
// If no documents exist, create default document
|
||||||
@@ -127,48 +136,42 @@ func (ds *DocumentService) GetDocumentByID(id int64) (*models.Document, error) {
|
|||||||
ds.mu.RLock()
|
ds.mu.RLock()
|
||||||
defer ds.mu.RUnlock()
|
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 err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("failed to get document by ID: %w", err)
|
return nil, fmt.Errorf("failed to get document by ID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(rows) == 0 {
|
// 转换时间字段
|
||||||
return nil, nil
|
if t, err := time.Parse("2006-01-02 15:04:05", createdAt); err == 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 {
|
|
||||||
doc.CreatedAt = t
|
doc.CreatedAt = t
|
||||||
}
|
}
|
||||||
}
|
if t, err := time.Parse("2006-01-02 15:04:05", updatedAt); err == nil {
|
||||||
|
|
||||||
if updatedAt, ok := row["updated_at"].(string); ok {
|
|
||||||
t, err := time.Parse("2006-01-02 15:04:05", updatedAt)
|
|
||||||
if err == nil {
|
|
||||||
doc.UpdatedAt = t
|
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
|
return doc, nil
|
||||||
}
|
}
|
||||||
@@ -178,6 +181,10 @@ func (ds *DocumentService) CreateDocument(title string) (*models.Document, error
|
|||||||
ds.mu.Lock()
|
ds.mu.Lock()
|
||||||
defer ds.mu.Unlock()
|
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
|
// Create document with default content
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
doc := &models.Document{
|
doc := &models.Document{
|
||||||
@@ -186,41 +193,102 @@ func (ds *DocumentService) CreateDocument(title string) (*models.Document, error
|
|||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
IsDeleted: false,
|
IsDeleted: false,
|
||||||
|
IsLocked: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行插入操作
|
// 执行插入操作
|
||||||
if err := ds.databaseService.SQLite.Execute(sqlInsertDocument,
|
result, err := ds.databaseService.db.Exec(sqlInsertDocument,
|
||||||
doc.Title, doc.Content, doc.CreatedAt, doc.UpdatedAt); err != nil {
|
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)
|
return nil, fmt.Errorf("failed to create document: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取自增ID
|
// 获取自增ID
|
||||||
lastIDRows, err := ds.databaseService.SQLite.Query("SELECT last_insert_rowid()")
|
lastID, err := result.LastInsertId()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get last insert ID: %w", err)
|
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的文档
|
// 返回带ID的文档
|
||||||
doc.ID = lastID
|
doc.ID = lastID
|
||||||
return doc, nil
|
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
|
// UpdateDocumentContent updates the content of a document
|
||||||
func (ds *DocumentService) UpdateDocumentContent(id int64, content string) error {
|
func (ds *DocumentService) UpdateDocumentContent(id int64, content string) error {
|
||||||
ds.mu.Lock()
|
ds.mu.Lock()
|
||||||
defer ds.mu.Unlock()
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update document content: %w", err)
|
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()
|
ds.mu.Lock()
|
||||||
defer ds.mu.Unlock()
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update document title: %w", err)
|
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)
|
// DeleteDocument marks a document as deleted (default document with ID=1 cannot be deleted)
|
||||||
func (ds *DocumentService) DeleteDocument(id int64) error {
|
func (ds *DocumentService) DeleteDocument(id int64) error {
|
||||||
ds.mu.Lock()
|
if ds.databaseService == nil || ds.databaseService.db == nil {
|
||||||
defer ds.mu.Unlock()
|
ds.logger.Error("database service not available")
|
||||||
|
return errors.New("database service not available")
|
||||||
|
}
|
||||||
|
|
||||||
// 不允许删除默认文档
|
// 不允许删除默认文档
|
||||||
if id == sqlDefaultDocumentID {
|
if id == sqlDefaultDocumentID {
|
||||||
return fmt.Errorf("cannot delete the default document")
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to mark document as deleted: %w", err)
|
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()
|
ds.mu.Lock()
|
||||||
defer ds.mu.Unlock()
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to restore document: %w", err)
|
return fmt.Errorf("failed to restore document: %w", err)
|
||||||
}
|
}
|
||||||
@@ -273,38 +367,48 @@ func (ds *DocumentService) ListAllDocumentsMeta() ([]*models.Document, error) {
|
|||||||
ds.mu.RLock()
|
ds.mu.RLock()
|
||||||
defer ds.mu.RUnlock()
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to list document meta: %w", err)
|
return nil, fmt.Errorf("failed to list document meta: %w", err)
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
var documents []*models.Document
|
var documents []*models.Document
|
||||||
for _, row := range rows {
|
for rows.Next() {
|
||||||
doc := &models.Document{IsDeleted: false}
|
doc := &models.Document{IsDeleted: false}
|
||||||
|
var createdAt, updatedAt string
|
||||||
|
var isLocked int
|
||||||
|
|
||||||
if id, ok := row["id"].(int64); ok {
|
err := rows.Scan(
|
||||||
doc.ID = id
|
&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 t, err := time.Parse("2006-01-02 15:04:05", createdAt); err == nil {
|
||||||
}
|
|
||||||
|
|
||||||
if createdAt, ok := row["created_at"].(string); ok {
|
|
||||||
t, err := time.Parse("2006-01-02 15:04:05", createdAt)
|
|
||||||
if err == nil {
|
|
||||||
doc.CreatedAt = t
|
doc.CreatedAt = t
|
||||||
}
|
}
|
||||||
}
|
if t, err := time.Parse("2006-01-02 15:04:05", updatedAt); err == nil {
|
||||||
|
|
||||||
if updatedAt, ok := row["updated_at"].(string); ok {
|
|
||||||
t, err := time.Parse("2006-01-02 15:04:05", updatedAt)
|
|
||||||
if err == nil {
|
|
||||||
doc.UpdatedAt = t
|
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
|
return documents, nil
|
||||||
@@ -315,38 +419,48 @@ func (ds *DocumentService) ListDeletedDocumentsMeta() ([]*models.Document, error
|
|||||||
ds.mu.RLock()
|
ds.mu.RLock()
|
||||||
defer ds.mu.RUnlock()
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to list deleted document meta: %w", err)
|
return nil, fmt.Errorf("failed to list deleted document meta: %w", err)
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
var documents []*models.Document
|
var documents []*models.Document
|
||||||
for _, row := range rows {
|
for rows.Next() {
|
||||||
doc := &models.Document{IsDeleted: true}
|
doc := &models.Document{IsDeleted: true}
|
||||||
|
var createdAt, updatedAt string
|
||||||
|
var isLocked int
|
||||||
|
|
||||||
if id, ok := row["id"].(int64); ok {
|
err := rows.Scan(
|
||||||
doc.ID = id
|
&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 t, err := time.Parse("2006-01-02 15:04:05", createdAt); err == nil {
|
||||||
}
|
|
||||||
|
|
||||||
if createdAt, ok := row["created_at"].(string); ok {
|
|
||||||
t, err := time.Parse("2006-01-02 15:04:05", createdAt)
|
|
||||||
if err == nil {
|
|
||||||
doc.CreatedAt = t
|
doc.CreatedAt = t
|
||||||
}
|
}
|
||||||
}
|
if t, err := time.Parse("2006-01-02 15:04:05", updatedAt); err == nil {
|
||||||
|
|
||||||
if updatedAt, ok := row["updated_at"].(string); ok {
|
|
||||||
t, err := time.Parse("2006-01-02 15:04:05", updatedAt)
|
|
||||||
if err == nil {
|
|
||||||
doc.UpdatedAt = t
|
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
|
return documents, nil
|
||||||
@@ -357,7 +471,12 @@ func (ds *DocumentService) GetFirstDocumentID() (int64, error) {
|
|||||||
ds.mu.RLock()
|
ds.mu.RLock()
|
||||||
defer ds.mu.RUnlock()
|
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 err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return 0, nil // No documents exist
|
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)
|
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
|
return id, nil
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ WHERE id = ?`
|
|||||||
// ExtensionService 扩展管理服务
|
// ExtensionService 扩展管理服务
|
||||||
type ExtensionService struct {
|
type ExtensionService struct {
|
||||||
databaseService *DatabaseService
|
databaseService *DatabaseService
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -73,7 +73,7 @@ func (e *ExtensionError) Is(target error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewExtensionService 创建扩展服务实例
|
// NewExtensionService 创建扩展服务实例
|
||||||
func NewExtensionService(databaseService *DatabaseService, logger *log.Service) *ExtensionService {
|
func NewExtensionService(databaseService *DatabaseService, logger *log.LogService) *ExtensionService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
@@ -104,21 +104,17 @@ func (es *ExtensionService) initDatabase() error {
|
|||||||
es.mu.Lock()
|
es.mu.Lock()
|
||||||
defer es.mu.Unlock()
|
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 {
|
if err != nil {
|
||||||
return &ExtensionError{"check_extensions_count", "", err}
|
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 count == 0 {
|
||||||
if err := es.insertDefaultExtensions(); err != nil {
|
if err := es.insertDefaultExtensions(); err != nil {
|
||||||
@@ -133,16 +129,15 @@ func (es *ExtensionService) initDatabase() error {
|
|||||||
// insertDefaultExtensions 插入默认扩展配置
|
// insertDefaultExtensions 插入默认扩展配置
|
||||||
func (es *ExtensionService) insertDefaultExtensions() error {
|
func (es *ExtensionService) insertDefaultExtensions() error {
|
||||||
defaultSettings := models.NewDefaultExtensionSettings()
|
defaultSettings := models.NewDefaultExtensionSettings()
|
||||||
now := time.Now()
|
now := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
for _, ext := range defaultSettings.Extensions {
|
for _, ext := range defaultSettings.Extensions {
|
||||||
|
|
||||||
configJSON, err := json.Marshal(ext.Config)
|
configJSON, err := json.Marshal(ext.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &ExtensionError{"marshal_config", string(ext.ID), err}
|
return &ExtensionError{"marshal_config", string(ext.ID), err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = es.databaseService.SQLite.Execute(sqlInsertExtension,
|
_, err = es.databaseService.db.Exec(sqlInsertExtension,
|
||||||
string(ext.ID),
|
string(ext.ID),
|
||||||
ext.Enabled,
|
ext.Enabled,
|
||||||
ext.IsDefault,
|
ext.IsDefault,
|
||||||
@@ -153,7 +148,6 @@ func (es *ExtensionService) insertDefaultExtensions() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return &ExtensionError{"insert_extension", string(ext.ID), err}
|
return &ExtensionError{"insert_extension", string(ext.ID), err}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -179,38 +173,51 @@ func (es *ExtensionService) GetAllExtensions() ([]models.Extension, error) {
|
|||||||
es.mu.RLock()
|
es.mu.RLock()
|
||||||
defer es.mu.RUnlock()
|
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 {
|
if err != nil {
|
||||||
return nil, &ExtensionError{"query_extensions", "", err}
|
return nil, &ExtensionError{"query_extensions", "", err}
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
var extensions []models.Extension
|
var extensions []models.Extension
|
||||||
for _, row := range rows {
|
for rows.Next() {
|
||||||
var ext models.Extension
|
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)
|
ext.ID = models.ExtensionID(id)
|
||||||
}
|
|
||||||
|
|
||||||
if enabled, ok := row["enabled"].(int64); ok {
|
|
||||||
ext.Enabled = enabled == 1
|
ext.Enabled = enabled == 1
|
||||||
}
|
|
||||||
|
|
||||||
if isDefault, ok := row["is_default"].(int64); ok {
|
|
||||||
ext.IsDefault = isDefault == 1
|
ext.IsDefault = isDefault == 1
|
||||||
}
|
|
||||||
|
|
||||||
if configJSON, ok := row["config"].(string); ok {
|
|
||||||
var config models.ExtensionConfig
|
var config models.ExtensionConfig
|
||||||
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
|
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
|
ext.Config = config
|
||||||
}
|
|
||||||
|
|
||||||
extensions = append(extensions, ext)
|
extensions = append(extensions, ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return nil, &ExtensionError{"iterate_extensions", "", err}
|
||||||
|
}
|
||||||
|
|
||||||
return extensions, nil
|
return extensions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,6 +231,10 @@ func (es *ExtensionService) UpdateExtensionState(id models.ExtensionID, enabled
|
|||||||
es.mu.Lock()
|
es.mu.Lock()
|
||||||
defer es.mu.Unlock()
|
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 configJSON []byte
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -234,24 +245,19 @@ func (es *ExtensionService) UpdateExtensionState(id models.ExtensionID, enabled
|
|||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
if err != nil {
|
||||||
return &ExtensionError{"query_current_config", string(id), err}
|
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)
|
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 {
|
if err != nil {
|
||||||
return &ExtensionError{"update_extension", string(id), err}
|
return &ExtensionError{"update_extension", string(id), err}
|
||||||
}
|
}
|
||||||
@@ -276,8 +282,12 @@ func (es *ExtensionService) ResetAllExtensionsToDefault() error {
|
|||||||
es.mu.Lock()
|
es.mu.Lock()
|
||||||
defer es.mu.Unlock()
|
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 {
|
if err != nil {
|
||||||
return &ExtensionError{"delete_all_extensions", "", err}
|
return &ExtensionError{"delete_all_extensions", "", err}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,16 +24,18 @@ import (
|
|||||||
|
|
||||||
// HotkeyService Windows全局热键服务
|
// HotkeyService Windows全局热键服务
|
||||||
type HotkeyService struct {
|
type HotkeyService struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
|
windowService *WindowService
|
||||||
app *application.App
|
app *application.App
|
||||||
|
mainWindow *application.WebviewWindow
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
currentHotkey *models.HotkeyCombo
|
currentHotkey *models.HotkeyCombo
|
||||||
isRegistered atomic.Bool
|
isRegistered atomic.Bool
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancelFunc atomic.Value // 使用atomic.Value存储cancel函数,避免竞态条件
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,24 +53,48 @@ func (e *HotkeyError) Unwrap() error {
|
|||||||
return e.Err
|
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 创建热键服务实例
|
// NewHotkeyService 创建热键服务实例
|
||||||
func NewHotkeyService(configService *ConfigService, logger *log.Service) *HotkeyService {
|
func NewHotkeyService(configService *ConfigService, windowService *WindowService, logger *log.LogService) *HotkeyService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
return &HotkeyService{
|
service := &HotkeyService{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
configService: configService,
|
configService: configService,
|
||||||
|
windowService: windowService,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
|
||||||
}
|
}
|
||||||
|
// 初始化时设置cancel函数
|
||||||
|
service.setCancelFunc(cancel)
|
||||||
|
return service
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize 初始化热键服务
|
// Initialize 初始化热键服务
|
||||||
func (hs *HotkeyService) Initialize(app *application.App) error {
|
func (hs *HotkeyService) Initialize(app *application.App, mainWindow *application.WebviewWindow) error {
|
||||||
hs.app = app
|
hs.app = app
|
||||||
|
hs.mainWindow = mainWindow
|
||||||
|
|
||||||
config, err := hs.configService.GetConfig()
|
config, err := hs.configService.GetConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -119,7 +145,7 @@ func (hs *HotkeyService) RegisterHotkey(hotkey *models.HotkeyCombo) error {
|
|||||||
|
|
||||||
hs.currentHotkey = hotkey
|
hs.currentHotkey = hotkey
|
||||||
hs.isRegistered.Store(true)
|
hs.isRegistered.Store(true)
|
||||||
hs.cancel = cancel
|
hs.setCancelFunc(cancel)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -137,13 +163,15 @@ func (hs *HotkeyService) unregisterInternal() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if hs.cancel != nil {
|
// 原子地获取并调用cancel函数
|
||||||
hs.cancel()
|
if cancel := hs.getCancelFunc(); cancel != nil {
|
||||||
|
cancel()
|
||||||
hs.wg.Wait()
|
hs.wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
hs.currentHotkey = nil
|
hs.currentHotkey = nil
|
||||||
hs.isRegistered.Store(false)
|
hs.isRegistered.Store(false)
|
||||||
|
hs.clearCancelFunc()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +193,7 @@ func (hs *HotkeyService) hotkeyListener(ctx context.Context, hotkey *models.Hotk
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ticker := time.NewTicker(100 * time.Millisecond)
|
ticker := time.NewTicker(50 * time.Millisecond)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
var wasPressed bool
|
var wasPressed bool
|
||||||
@@ -199,11 +227,61 @@ func cBool(b bool) C.int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// toggleWindow 切换窗口
|
// toggleWindow 切换窗口显示状态
|
||||||
func (hs *HotkeyService) toggleWindow() {
|
func (hs *HotkeyService) toggleWindow() {
|
||||||
if hs.app != nil {
|
if hs.mainWindow == nil {
|
||||||
hs.app.Event.Emit("hotkey:toggle-window", 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 键名转虚拟键码
|
// keyToVirtualKeyCode 键名转虚拟键码
|
||||||
@@ -261,7 +339,10 @@ func (hs *HotkeyService) IsRegistered() bool {
|
|||||||
|
|
||||||
// ServiceShutdown 关闭服务
|
// ServiceShutdown 关闭服务
|
||||||
func (hs *HotkeyService) ServiceShutdown() error {
|
func (hs *HotkeyService) ServiceShutdown() error {
|
||||||
hs.cancel()
|
// 原子地获取并调用cancel函数
|
||||||
|
if cancel := hs.getCancelFunc(); cancel != nil {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
hs.wg.Wait()
|
hs.wg.Wait()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -66,6 +66,7 @@ int isHotkeyRegistered() {
|
|||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -80,12 +81,15 @@ var globalHotkeyService *HotkeyService
|
|||||||
|
|
||||||
// HotkeyService macOS全局热键服务
|
// HotkeyService macOS全局热键服务
|
||||||
type HotkeyService struct {
|
type HotkeyService struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
|
windowService *WindowService
|
||||||
app *application.App
|
app *application.App
|
||||||
|
mainWindow *application.WebviewWindow
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
isRegistered atomic.Bool
|
isRegistered atomic.Bool
|
||||||
currentHotkey *models.HotkeyCombo
|
currentHotkey *models.HotkeyCombo
|
||||||
|
cancelFunc atomic.Value // 使用atomic.Value存储cancel函数,避免竞态条件
|
||||||
}
|
}
|
||||||
|
|
||||||
// HotkeyError 热键错误
|
// HotkeyError 热键错误
|
||||||
@@ -104,8 +108,28 @@ func (e *HotkeyError) Unwrap() error {
|
|||||||
return e.Err
|
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 创建新的热键服务实例
|
// NewHotkeyService 创建新的热键服务实例
|
||||||
func NewHotkeyService(configService *ConfigService, logger *log.Service) *HotkeyService {
|
func NewHotkeyService(configService *ConfigService, windowService *WindowService, logger *log.LogService) *HotkeyService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
@@ -113,6 +137,7 @@ func NewHotkeyService(configService *ConfigService, logger *log.Service) *Hotkey
|
|||||||
service := &HotkeyService{
|
service := &HotkeyService{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
configService: configService,
|
configService: configService,
|
||||||
|
windowService: windowService,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置全局实例
|
// 设置全局实例
|
||||||
@@ -122,8 +147,9 @@ func NewHotkeyService(configService *ConfigService, logger *log.Service) *Hotkey
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize 初始化热键服务
|
// Initialize 初始化热键服务
|
||||||
func (hs *HotkeyService) Initialize(app *application.App) error {
|
func (hs *HotkeyService) Initialize(app *application.App, mainWindow *application.WebviewWindow) error {
|
||||||
hs.app = app
|
hs.app = app
|
||||||
|
hs.mainWindow = mainWindow
|
||||||
|
|
||||||
// 加载并应用当前配置
|
// 加载并应用当前配置
|
||||||
config, err := hs.configService.GetConfig()
|
config, err := hs.configService.GetConfig()
|
||||||
@@ -285,9 +311,59 @@ func (hs *HotkeyService) IsRegistered() bool {
|
|||||||
|
|
||||||
// ToggleWindow 切换窗口显示状态
|
// ToggleWindow 切换窗口显示状态
|
||||||
func (hs *HotkeyService) ToggleWindow() {
|
func (hs *HotkeyService) ToggleWindow() {
|
||||||
if hs.app != nil {
|
if hs.mainWindow == nil {
|
||||||
hs.app.EmitEvent("hotkey:toggle-window", 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 关闭热键服务
|
// ServiceShutdown 关闭热键服务
|
||||||
|
|||||||
@@ -141,16 +141,18 @@ import (
|
|||||||
|
|
||||||
// HotkeyService Linux全局热键服务
|
// HotkeyService Linux全局热键服务
|
||||||
type HotkeyService struct {
|
type HotkeyService struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
|
windowService *WindowService
|
||||||
app *application.App
|
app *application.App
|
||||||
|
mainWindow *application.WebviewWindow
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
currentHotkey *models.HotkeyCombo
|
currentHotkey *models.HotkeyCombo
|
||||||
isRegistered atomic.Bool
|
isRegistered atomic.Bool
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancelFunc atomic.Value // 使用atomic.Value存储cancel函数,避免竞态条件
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,24 +171,48 @@ func (e *HotkeyError) Unwrap() error {
|
|||||||
return e.Err
|
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 创建热键服务实例
|
// NewHotkeyService 创建热键服务实例
|
||||||
func NewHotkeyService(configService *ConfigService, logger *log.Service) *HotkeyService {
|
func NewHotkeyService(configService *ConfigService, windowService *WindowService, logger *log.LogService) *HotkeyService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
return &HotkeyService{
|
service := &HotkeyService{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
configService: configService,
|
configService: configService,
|
||||||
|
windowService: windowService,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
|
||||||
}
|
}
|
||||||
|
// 初始化时设置cancel函数
|
||||||
|
service.setCancelFunc(cancel)
|
||||||
|
return service
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize 初始化热键服务
|
// Initialize 初始化热键服务
|
||||||
func (hs *HotkeyService) Initialize(app *application.App) error {
|
func (hs *HotkeyService) Initialize(app *application.App, mainWindow *application.WebviewWindow) error {
|
||||||
hs.app = app
|
hs.app = app
|
||||||
|
hs.mainWindow = mainWindow
|
||||||
|
|
||||||
if int(C.initX11Display()) == 0 {
|
if int(C.initX11Display()) == 0 {
|
||||||
return &HotkeyError{"init_x11", fmt.Errorf("failed to initialize X11 display")}
|
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.currentHotkey = hotkey
|
||||||
hs.isRegistered.Store(true)
|
hs.isRegistered.Store(true)
|
||||||
hs.cancel = cancel
|
hs.setCancelFunc(cancel)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -271,8 +297,9 @@ func (hs *HotkeyService) unregisterInternal() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if hs.cancel != nil {
|
// 原子地获取并调用cancel函数
|
||||||
hs.cancel()
|
if cancel := hs.getCancelFunc(); cancel != nil {
|
||||||
|
cancel()
|
||||||
hs.wg.Wait()
|
hs.wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,6 +310,7 @@ func (hs *HotkeyService) unregisterInternal() error {
|
|||||||
|
|
||||||
hs.currentHotkey = nil
|
hs.currentHotkey = nil
|
||||||
hs.isRegistered.Store(false)
|
hs.isRegistered.Store(false)
|
||||||
|
hs.clearCancelFunc()
|
||||||
return nil
|
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) {
|
func (hs *HotkeyService) hotkeyListener(ctx context.Context, ready chan<- error) {
|
||||||
defer hs.wg.Done()
|
defer hs.wg.Done()
|
||||||
|
|
||||||
ticker := time.NewTicker(100 * time.Millisecond)
|
// 优化轮询频率从100ms改为50ms,提高响应性
|
||||||
|
ticker := time.NewTicker(50 * time.Millisecond)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
ready <- nil // 标记准备就绪
|
ready <- nil // 标记准备就绪
|
||||||
@@ -315,11 +344,61 @@ func (hs *HotkeyService) hotkeyListener(ctx context.Context, ready chan<- error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// toggleWindow 切换窗口
|
// toggleWindow 切换窗口显示状态
|
||||||
func (hs *HotkeyService) toggleWindow() {
|
func (hs *HotkeyService) toggleWindow() {
|
||||||
if hs.app != nil {
|
if hs.mainWindow == nil {
|
||||||
hs.app.EmitEvent("hotkey:toggle-window", 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键码
|
// keyToX11KeyCode 键名转X11键码
|
||||||
@@ -386,7 +465,10 @@ func (hs *HotkeyService) IsRegistered() bool {
|
|||||||
|
|
||||||
// ServiceShutdown 关闭服务
|
// ServiceShutdown 关闭服务
|
||||||
func (hs *HotkeyService) ServiceShutdown() error {
|
func (hs *HotkeyService) ServiceShutdown() error {
|
||||||
hs.cancel()
|
// 原子地获取并调用cancel函数
|
||||||
|
if cancel := hs.getCancelFunc(); cancel != nil {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
hs.wg.Wait()
|
hs.wg.Wait()
|
||||||
C.closeX11Display()
|
C.closeX11Display()
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const (
|
|||||||
// KeyBindingService 快捷键管理服务
|
// KeyBindingService 快捷键管理服务
|
||||||
type KeyBindingService struct {
|
type KeyBindingService struct {
|
||||||
databaseService *DatabaseService
|
databaseService *DatabaseService
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -83,7 +83,7 @@ func (e *KeyBindingError) Is(target error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewKeyBindingService 创建快捷键服务实例
|
// NewKeyBindingService 创建快捷键服务实例
|
||||||
func NewKeyBindingService(databaseService *DatabaseService, logger *log.Service) *KeyBindingService {
|
func NewKeyBindingService(databaseService *DatabaseService, logger *log.LogService) *KeyBindingService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
@@ -105,21 +105,17 @@ func (kbs *KeyBindingService) initDatabase() error {
|
|||||||
kbs.mu.Lock()
|
kbs.mu.Lock()
|
||||||
defer kbs.mu.Unlock()
|
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 {
|
if err != nil {
|
||||||
return &KeyBindingError{"check_keybindings_count", "", err}
|
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 count == 0 {
|
||||||
if err := kbs.insertDefaultKeyBindings(); err != nil {
|
if err := kbs.insertDefaultKeyBindings(); err != nil {
|
||||||
@@ -134,11 +130,10 @@ func (kbs *KeyBindingService) initDatabase() error {
|
|||||||
// insertDefaultKeyBindings 插入默认快捷键配置
|
// insertDefaultKeyBindings 插入默认快捷键配置
|
||||||
func (kbs *KeyBindingService) insertDefaultKeyBindings() error {
|
func (kbs *KeyBindingService) insertDefaultKeyBindings() error {
|
||||||
defaultConfig := models.NewDefaultKeyBindingConfig()
|
defaultConfig := models.NewDefaultKeyBindingConfig()
|
||||||
now := time.Now()
|
now := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
for _, kb := range defaultConfig.KeyBindings {
|
for _, kb := range defaultConfig.KeyBindings {
|
||||||
|
_, err := kbs.databaseService.db.Exec(sqlInsertKeyBinding,
|
||||||
err := kbs.databaseService.SQLite.Execute(sqlInsertKeyBinding,
|
|
||||||
string(kb.Command), // 转换为字符串存储
|
string(kb.Command), // 转换为字符串存储
|
||||||
string(kb.Extension), // 转换为字符串存储
|
string(kb.Extension), // 转换为字符串存储
|
||||||
kb.Key,
|
kb.Key,
|
||||||
@@ -150,7 +145,6 @@ func (kbs *KeyBindingService) insertDefaultKeyBindings() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return &KeyBindingError{"insert_keybinding", string(kb.Command), err}
|
return &KeyBindingError{"insert_keybinding", string(kb.Command), err}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -161,38 +155,46 @@ func (kbs *KeyBindingService) GetAllKeyBindings() ([]models.KeyBinding, error) {
|
|||||||
kbs.mu.RLock()
|
kbs.mu.RLock()
|
||||||
defer kbs.mu.RUnlock()
|
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 {
|
if err != nil {
|
||||||
return nil, &KeyBindingError{"query_keybindings", "", err}
|
return nil, &KeyBindingError{"query_keybindings", "", err}
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
var keyBindings []models.KeyBinding
|
var keyBindings []models.KeyBinding
|
||||||
for _, row := range rows {
|
for rows.Next() {
|
||||||
var kb models.KeyBinding
|
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)
|
kb.Command = models.KeyBindingCommand(command)
|
||||||
}
|
|
||||||
|
|
||||||
if extension, ok := row["extension"].(string); ok {
|
|
||||||
kb.Extension = models.ExtensionID(extension)
|
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
|
kb.Enabled = enabled == 1
|
||||||
}
|
|
||||||
|
|
||||||
if isDefault, ok := row["is_default"].(int64); ok {
|
|
||||||
kb.IsDefault = isDefault == 1
|
kb.IsDefault = isDefault == 1
|
||||||
}
|
|
||||||
|
|
||||||
keyBindings = append(keyBindings, kb)
|
keyBindings = append(keyBindings, kb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return nil, &KeyBindingError{"iterate_keybindings", "", err}
|
||||||
|
}
|
||||||
|
|
||||||
return keyBindings, nil
|
return keyBindings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ type MigrationProgress struct {
|
|||||||
|
|
||||||
// MigrationService 迁移服务
|
// MigrationService 迁移服务
|
||||||
type MigrationService struct {
|
type MigrationService struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
|
dbService *DatabaseService
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
progress atomic.Value // stores MigrationProgress
|
progress atomic.Value // stores MigrationProgress
|
||||||
|
|
||||||
@@ -42,13 +43,14 @@ type MigrationService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewMigrationService 创建迁移服务
|
// NewMigrationService 创建迁移服务
|
||||||
func NewMigrationService(logger *log.Service) *MigrationService {
|
func NewMigrationService(dbService *DatabaseService, logger *log.LogService) *MigrationService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
ms := &MigrationService{
|
ms := &MigrationService{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
dbService: dbService,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化进度
|
// 初始化进度
|
||||||
@@ -104,6 +106,17 @@ func (ms *MigrationService) MigrateDirectory(srcPath, dstPath string) error {
|
|||||||
return ms.failWithError(err)
|
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 {
|
if err := ms.atomicMove(ctx, srcPath, dstPath); err != nil {
|
||||||
return ms.failWithError(err)
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重命名失败,使用压缩迁移
|
// 重命名失败,使用压缩迁移
|
||||||
|
ms.logger.Info("Direct rename failed, using compress migration", "src", srcPath, "dst", dstPath)
|
||||||
ms.updateProgress(MigrationProgress{
|
ms.updateProgress(MigrationProgress{
|
||||||
Status: MigrationStatusMigrating,
|
Status: MigrationStatusMigrating,
|
||||||
Progress: 30,
|
Progress: 30,
|
||||||
@@ -249,12 +269,18 @@ func (ms *MigrationService) compressMove(ctx context.Context, srcPath, dstPath s
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证迁移是否成功
|
||||||
|
if err := ms.verifyMigration(dstPath); err != nil {
|
||||||
|
// 迁移验证失败,清理目标目录
|
||||||
|
os.RemoveAll(dstPath)
|
||||||
|
return fmt.Errorf("migration verification failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// 删除源目录
|
// 删除源目录
|
||||||
ms.updateProgress(MigrationProgress{
|
ms.updateProgress(MigrationProgress{
|
||||||
Status: MigrationStatusMigrating,
|
Status: MigrationStatusMigrating,
|
||||||
Progress: 90,
|
Progress: 90,
|
||||||
})
|
})
|
||||||
|
|
||||||
os.RemoveAll(srcPath)
|
os.RemoveAll(srcPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -404,6 +430,29 @@ func (ms *MigrationService) isSubDirectory(parent, target string) bool {
|
|||||||
return len(target) > len(parent) && strings.HasPrefix(target, parent)
|
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 取消迁移
|
// CancelMigration 取消迁移
|
||||||
func (ms *MigrationService) CancelMigration() error {
|
func (ms *MigrationService) CancelMigration() error {
|
||||||
ms.mu.Lock()
|
ms.mu.Lock()
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ type SelfUpdateResult struct {
|
|||||||
|
|
||||||
// SelfUpdateService 自我更新服务
|
// SelfUpdateService 自我更新服务
|
||||||
type SelfUpdateService struct {
|
type SelfUpdateService struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
config *models.AppConfig
|
config *models.AppConfig
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ type SelfUpdateService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewSelfUpdateService 创建自我更新服务实例
|
// NewSelfUpdateService 创建自我更新服务实例
|
||||||
func NewSelfUpdateService(configService *ConfigService, logger *log.Service) (*SelfUpdateService, error) {
|
func NewSelfUpdateService(configService *ConfigService, logger *log.LogService) (*SelfUpdateService, error) {
|
||||||
// 获取配置
|
// 获取配置
|
||||||
appConfig, err := configService.GetConfig()
|
appConfig, err := configService.GetConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ import (
|
|||||||
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/sqlite"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServiceManager 服务管理器,负责协调各个服务
|
// ServiceManager 服务管理器,负责协调各个服务
|
||||||
type ServiceManager struct {
|
type ServiceManager struct {
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
databaseService *DatabaseService
|
databaseService *DatabaseService
|
||||||
sqliteService *sqlite.Service
|
|
||||||
documentService *DocumentService
|
documentService *DocumentService
|
||||||
windowService *WindowService
|
windowService *WindowService
|
||||||
migrationService *MigrationService
|
migrationService *MigrationService
|
||||||
@@ -25,7 +23,9 @@ type ServiceManager struct {
|
|||||||
startupService *StartupService
|
startupService *StartupService
|
||||||
selfUpdateService *SelfUpdateService
|
selfUpdateService *SelfUpdateService
|
||||||
translationService *TranslationService
|
translationService *TranslationService
|
||||||
logger *log.Service
|
themeService *ThemeService
|
||||||
|
BackupService *BackupService
|
||||||
|
logger *log.LogService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServiceManager 创建新的服务管理器实例
|
// NewServiceManager 创建新的服务管理器实例
|
||||||
@@ -36,14 +36,11 @@ func NewServiceManager() *ServiceManager {
|
|||||||
// 初始化配置服务
|
// 初始化配置服务
|
||||||
configService := NewConfigService(logger)
|
configService := NewConfigService(logger)
|
||||||
|
|
||||||
// 初始化SQLite服务
|
|
||||||
sqliteService := sqlite.New()
|
|
||||||
|
|
||||||
// 初始化数据库服务
|
// 初始化数据库服务
|
||||||
databaseService := NewDatabaseService(configService, logger)
|
databaseService := NewDatabaseService(configService, logger)
|
||||||
|
|
||||||
// 初始化迁移服务
|
// 初始化迁移服务
|
||||||
migrationService := NewMigrationService(logger)
|
migrationService := NewMigrationService(databaseService, logger)
|
||||||
|
|
||||||
// 初始化文档服务
|
// 初始化文档服务
|
||||||
documentService := NewDocumentService(databaseService, logger)
|
documentService := NewDocumentService(databaseService, logger)
|
||||||
@@ -55,7 +52,7 @@ func NewServiceManager() *ServiceManager {
|
|||||||
systemService := NewSystemService(logger)
|
systemService := NewSystemService(logger)
|
||||||
|
|
||||||
// 初始化热键服务
|
// 初始化热键服务
|
||||||
hotkeyService := NewHotkeyService(configService, logger)
|
hotkeyService := NewHotkeyService(configService, windowService, logger)
|
||||||
|
|
||||||
// 初始化对话服务
|
// 初始化对话服务
|
||||||
dialogService := NewDialogService(logger)
|
dialogService := NewDialogService(logger)
|
||||||
@@ -81,6 +78,12 @@ func NewServiceManager() *ServiceManager {
|
|||||||
// 初始化翻译服务
|
// 初始化翻译服务
|
||||||
translationService := NewTranslationService(logger)
|
translationService := NewTranslationService(logger)
|
||||||
|
|
||||||
|
// 初始化主题服务
|
||||||
|
themeService := NewThemeService(databaseService, logger)
|
||||||
|
|
||||||
|
// 初始化备份服务
|
||||||
|
backupService := NewBackupService(configService, databaseService, logger)
|
||||||
|
|
||||||
// 使用新的配置通知系统设置热键配置变更监听
|
// 使用新的配置通知系统设置热键配置变更监听
|
||||||
err = configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error {
|
err = configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error {
|
||||||
return hotkeyService.UpdateHotkey(enable, hotkey)
|
return hotkeyService.UpdateHotkey(enable, hotkey)
|
||||||
@@ -97,10 +100,17 @@ func NewServiceManager() *ServiceManager {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置备份配置变更监听,处理备份配置变更
|
||||||
|
err = configService.SetBackupConfigChangeCallback(func(config *models.GitBackupConfig) error {
|
||||||
|
return backupService.HandleConfigChange(config)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
return &ServiceManager{
|
return &ServiceManager{
|
||||||
configService: configService,
|
configService: configService,
|
||||||
databaseService: databaseService,
|
databaseService: databaseService,
|
||||||
sqliteService: sqliteService,
|
|
||||||
documentService: documentService,
|
documentService: documentService,
|
||||||
windowService: windowService,
|
windowService: windowService,
|
||||||
migrationService: migrationService,
|
migrationService: migrationService,
|
||||||
@@ -113,6 +123,8 @@ func NewServiceManager() *ServiceManager {
|
|||||||
startupService: startupService,
|
startupService: startupService,
|
||||||
selfUpdateService: selfUpdateService,
|
selfUpdateService: selfUpdateService,
|
||||||
translationService: translationService,
|
translationService: translationService,
|
||||||
|
themeService: themeService,
|
||||||
|
BackupService: backupService,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +133,6 @@ func NewServiceManager() *ServiceManager {
|
|||||||
func (sm *ServiceManager) GetServices() []application.Service {
|
func (sm *ServiceManager) GetServices() []application.Service {
|
||||||
services := []application.Service{
|
services := []application.Service{
|
||||||
application.NewService(sm.configService),
|
application.NewService(sm.configService),
|
||||||
application.NewService(sm.sqliteService),
|
|
||||||
application.NewService(sm.databaseService),
|
application.NewService(sm.databaseService),
|
||||||
application.NewService(sm.documentService),
|
application.NewService(sm.documentService),
|
||||||
application.NewService(sm.windowService),
|
application.NewService(sm.windowService),
|
||||||
@@ -135,6 +146,8 @@ func (sm *ServiceManager) GetServices() []application.Service {
|
|||||||
application.NewService(sm.startupService),
|
application.NewService(sm.startupService),
|
||||||
application.NewService(sm.selfUpdateService),
|
application.NewService(sm.selfUpdateService),
|
||||||
application.NewService(sm.translationService),
|
application.NewService(sm.translationService),
|
||||||
|
application.NewService(sm.themeService),
|
||||||
|
application.NewService(sm.BackupService),
|
||||||
}
|
}
|
||||||
return services
|
return services
|
||||||
}
|
}
|
||||||
@@ -150,7 +163,7 @@ func (sm *ServiceManager) GetDialogService() *DialogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetLogger 获取日志服务实例
|
// GetLogger 获取日志服务实例
|
||||||
func (sm *ServiceManager) GetLogger() *log.Service {
|
func (sm *ServiceManager) GetLogger() *log.LogService {
|
||||||
return sm.logger
|
return sm.logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,11 +207,6 @@ func (sm *ServiceManager) GetDatabaseService() *DatabaseService {
|
|||||||
return sm.databaseService
|
return sm.databaseService
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSQLiteService 获取SQLite服务实例
|
|
||||||
func (sm *ServiceManager) GetSQLiteService() *sqlite.Service {
|
|
||||||
return sm.sqliteService
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWindowService 获取窗口服务实例
|
// GetWindowService 获取窗口服务实例
|
||||||
func (sm *ServiceManager) GetWindowService() *WindowService {
|
func (sm *ServiceManager) GetWindowService() *WindowService {
|
||||||
return sm.windowService
|
return sm.windowService
|
||||||
@@ -208,3 +216,8 @@ func (sm *ServiceManager) GetWindowService() *WindowService {
|
|||||||
func (sm *ServiceManager) GetDocumentService() *DocumentService {
|
func (sm *ServiceManager) GetDocumentService() *DocumentService {
|
||||||
return sm.documentService
|
return sm.documentService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetThemeService 获取主题服务实例
|
||||||
|
func (sm *ServiceManager) GetThemeService() *ThemeService {
|
||||||
|
return sm.themeService
|
||||||
|
}
|
||||||
@@ -15,14 +15,14 @@ import (
|
|||||||
|
|
||||||
// DarwinStartupImpl macOS 平台开机启动实现
|
// DarwinStartupImpl macOS 平台开机启动实现
|
||||||
type DarwinStartupImpl struct {
|
type DarwinStartupImpl struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
disabled bool
|
disabled bool
|
||||||
appPath string
|
appPath string
|
||||||
appName string
|
appName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// newStartupImplementation 创建平台特定的开机启动实现
|
// newStartupImplementation 创建平台特定的开机启动实现
|
||||||
func newStartupImplementation(logger *log.Service) StartupImplementation {
|
func newStartupImplementation(logger *log.LogService) StartupImplementation {
|
||||||
return &DarwinStartupImpl{
|
return &DarwinStartupImpl{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
// LinuxStartupImpl Linux 平台开机启动实现
|
// LinuxStartupImpl Linux 平台开机启动实现
|
||||||
type LinuxStartupImpl struct {
|
type LinuxStartupImpl struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
autostartDir string
|
autostartDir string
|
||||||
execPath string
|
execPath string
|
||||||
appName string
|
appName string
|
||||||
@@ -37,7 +37,7 @@ X-GNOME-Autostart-enabled=true
|
|||||||
`
|
`
|
||||||
|
|
||||||
// newStartupImplementation 创建平台特定的开机启动实现
|
// newStartupImplementation 创建平台特定的开机启动实现
|
||||||
func newStartupImplementation(logger *log.Service) StartupImplementation {
|
func newStartupImplementation(logger *log.LogService) StartupImplementation {
|
||||||
return &LinuxStartupImpl{
|
return &LinuxStartupImpl{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
// StartupService 开机启动服务
|
// StartupService 开机启动服务
|
||||||
type StartupService struct {
|
type StartupService struct {
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
impl StartupImplementation
|
impl StartupImplementation
|
||||||
initError error
|
initError error
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ type StartupImplementation interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewStartupService 创建开机启动服务实例
|
// NewStartupService 创建开机启动服务实例
|
||||||
func NewStartupService(configService *ConfigService, logger *log.Service) *StartupService {
|
func NewStartupService(configService *ConfigService, logger *log.LogService) *StartupService {
|
||||||
service := &StartupService{
|
service := &StartupService{
|
||||||
configService: configService,
|
configService: configService,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
|
|
||||||
// WindowsStartupImpl Windows 平台开机启动实现
|
// WindowsStartupImpl Windows 平台开机启动实现
|
||||||
type WindowsStartupImpl struct {
|
type WindowsStartupImpl struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
registryKey string
|
registryKey string
|
||||||
execPath string
|
execPath string
|
||||||
workingDir string
|
workingDir string
|
||||||
@@ -23,7 +23,7 @@ type WindowsStartupImpl struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newStartupImplementation 创建平台特定的开机启动实现
|
// newStartupImplementation 创建平台特定的开机启动实现
|
||||||
func newStartupImplementation(logger *log.Service) StartupImplementation {
|
func newStartupImplementation(logger *log.LogService) StartupImplementation {
|
||||||
return &WindowsStartupImpl{
|
return &WindowsStartupImpl{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
type StoreOption struct {
|
type StoreOption struct {
|
||||||
FilePath string
|
FilePath string
|
||||||
AutoSave bool
|
AutoSave bool
|
||||||
Logger *log.Service
|
Logger *log.LogService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store 泛型存储服务
|
// Store 泛型存储服务
|
||||||
@@ -25,7 +25,7 @@ type Store[T any] struct {
|
|||||||
dataMap sync.Map // thread-safe map
|
dataMap sync.Map // thread-safe map
|
||||||
unsaved atomic.Bool
|
unsaved atomic.Bool
|
||||||
initOnce sync.Once
|
initOnce sync.Once
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStore 存储服务
|
// NewStore 存储服务
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
// SystemService 系统监控服务
|
// SystemService 系统监控服务
|
||||||
type SystemService struct {
|
type SystemService struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
}
|
}
|
||||||
|
|
||||||
// MemoryStats 内存统计信息
|
// MemoryStats 内存统计信息
|
||||||
@@ -29,7 +29,7 @@ type MemoryStats struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewSystemService 创建新的系统服务实例
|
// NewSystemService 创建新的系统服务实例
|
||||||
func NewSystemService(logger *log.Service) *SystemService {
|
func NewSystemService(logger *log.LogService) *SystemService {
|
||||||
return &SystemService{
|
return &SystemService{
|
||||||
logger: logger,
|
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 翻译服务
|
// TranslationService 翻译服务
|
||||||
type TranslationService struct {
|
type TranslationService struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
factory *translator.TranslatorFactory
|
factory *translator.TranslatorFactory
|
||||||
defaultTimeout time.Duration
|
defaultTimeout time.Duration
|
||||||
translators map[translator.TranslatorType]translator.Translator
|
translators map[translator.TranslatorType]translator.Translator
|
||||||
@@ -18,7 +18,7 @@ type TranslationService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewTranslationService 创建翻译服务实例
|
// NewTranslationService 创建翻译服务实例
|
||||||
func NewTranslationService(logger *log.Service) *TranslationService {
|
func NewTranslationService(logger *log.LogService) *TranslationService {
|
||||||
service := &TranslationService{
|
service := &TranslationService{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
factory: translator.NewTranslatorFactory(),
|
factory: translator.NewTranslatorFactory(),
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import (
|
|||||||
|
|
||||||
// TrayService 系统托盘服务
|
// TrayService 系统托盘服务
|
||||||
type TrayService struct {
|
type TrayService struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
app *application.App
|
app *application.App
|
||||||
mainWindow *application.WebviewWindow
|
mainWindow *application.WebviewWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTrayService 创建新的系统托盘服务实例
|
// NewTrayService 创建新的系统托盘服务实例
|
||||||
func NewTrayService(logger *log.Service, configService *ConfigService) *TrayService {
|
func NewTrayService(logger *log.LogService, configService *ConfigService) *TrayService {
|
||||||
return &TrayService{
|
return &TrayService{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
configService: configService,
|
configService: configService,
|
||||||
@@ -42,7 +42,6 @@ func (ts *TrayService) HandleWindowClose() {
|
|||||||
if ts.ShouldMinimizeToTray() {
|
if ts.ShouldMinimizeToTray() {
|
||||||
// 隐藏到托盘
|
// 隐藏到托盘
|
||||||
ts.mainWindow.Hide()
|
ts.mainWindow.Hide()
|
||||||
ts.app.Event.Emit("window:hidden", nil)
|
|
||||||
} else {
|
} else {
|
||||||
// 直接退出应用
|
// 直接退出应用
|
||||||
ts.app.Quit()
|
ts.app.Quit()
|
||||||
@@ -54,7 +53,6 @@ func (ts *TrayService) HandleWindowMinimize() {
|
|||||||
if ts.ShouldMinimizeToTray() {
|
if ts.ShouldMinimizeToTray() {
|
||||||
// 隐藏到托盘
|
// 隐藏到托盘
|
||||||
ts.mainWindow.Hide()
|
ts.mainWindow.Hide()
|
||||||
ts.app.Event.Emit("window:hidden", nil)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,9 +62,6 @@ func (ts *TrayService) ShowWindow() {
|
|||||||
ts.mainWindow.Show()
|
ts.mainWindow.Show()
|
||||||
ts.mainWindow.Restore()
|
ts.mainWindow.Restore()
|
||||||
ts.mainWindow.Focus()
|
ts.mainWindow.Focus()
|
||||||
if ts.app != nil {
|
|
||||||
ts.app.Event.Emit("window:shown", nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type WindowInfo struct {
|
|||||||
|
|
||||||
// WindowService 窗口管理服务
|
// WindowService 窗口管理服务
|
||||||
type WindowService struct {
|
type WindowService struct {
|
||||||
logger *log.Service
|
logger *log.LogService
|
||||||
documentService *DocumentService
|
documentService *DocumentService
|
||||||
app *application.App
|
app *application.App
|
||||||
mainWindow *application.WebviewWindow
|
mainWindow *application.WebviewWindow
|
||||||
@@ -27,7 +27,7 @@ type WindowService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewWindowService 创建新的窗口服务实例
|
// NewWindowService 创建新的窗口服务实例
|
||||||
func NewWindowService(logger *log.Service, documentService *DocumentService) *WindowService {
|
func NewWindowService(logger *log.LogService, documentService *DocumentService) *WindowService {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = log.New()
|
logger = log.New()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ var assets embed.FS
|
|||||||
// logs any error that might occur.
|
// logs any error that might occur.
|
||||||
func main() {
|
func main() {
|
||||||
serviceManager := services.NewServiceManager()
|
serviceManager := services.NewServiceManager()
|
||||||
|
var window *application.WebviewWindow
|
||||||
|
|
||||||
var encryptionKey = [32]byte{
|
var encryptionKey = [32]byte{
|
||||||
0x1e, 0x1f, 0x1c, 0x1d, 0x1a, 0x1b, 0x18, 0x19,
|
0x1e, 0x1f, 0x1c, 0x1d, 0x1a, 0x1b, 0x18, 0x19,
|
||||||
@@ -51,6 +52,13 @@ func main() {
|
|||||||
SingleInstance: &application.SingleInstanceOptions{
|
SingleInstance: &application.SingleInstanceOptions{
|
||||||
UniqueID: "com.voidraft",
|
UniqueID: "com.voidraft",
|
||||||
EncryptionKey: encryptionKey,
|
EncryptionKey: encryptionKey,
|
||||||
|
OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
|
||||||
|
if window != nil {
|
||||||
|
window.Show()
|
||||||
|
window.Restore()
|
||||||
|
window.Focus()
|
||||||
|
}
|
||||||
|
},
|
||||||
AdditionalData: map[string]string{
|
AdditionalData: map[string]string{
|
||||||
"launchtime": time.Now().Local().String(),
|
"launchtime": time.Now().Local().String(),
|
||||||
},
|
},
|
||||||
@@ -82,6 +90,7 @@ func main() {
|
|||||||
URL: "/",
|
URL: "/",
|
||||||
})
|
})
|
||||||
mainWindow.Center()
|
mainWindow.Center()
|
||||||
|
window = mainWindow
|
||||||
|
|
||||||
// 获取托盘服务并设置应用引用
|
// 获取托盘服务并设置应用引用
|
||||||
trayService := serviceManager.GetTrayService()
|
trayService := serviceManager.GetTrayService()
|
||||||
@@ -96,7 +105,7 @@ func main() {
|
|||||||
|
|
||||||
// 初始化热键服务
|
// 初始化热键服务
|
||||||
hotkeyService := serviceManager.GetHotkeyService()
|
hotkeyService := serviceManager.GetHotkeyService()
|
||||||
err := hotkeyService.Initialize(app)
|
err := hotkeyService.Initialize(app, mainWindow)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user