feat: 完成 Issue #14-15 主题商店与跨平台打包
## Phase 4 - 性能与生态 (续) ### Issue #14: 个性化主题商店 ✅ - ThemeManager 主题管理器 - 4 种内置主题 (深色/浅色/护眼/高对比度) - 主题安装/卸载功能 - 自定义主题配置 - CSS 变量系统 ### Issue #15: 跨平台打包发布 ✅ - build-release.sh 打包脚本 - 支持 macOS (DMG + App Bundle) - 支持 Linux (AppImage + tar.gz) - 支持 Windows (NSIS + ZIP) - Cargo 发布配置优化 (LTO, strip) - 自动生成 RELEASE.md ## 完成状态 ✅ Phase 2: 4/4 Issues ✅ Phase 3: 4/4 Issues ✅ Phase 4: 3/3 Issues 🎉 ReadFlow MVP 全部完成!
This commit is contained in:
10
Cargo.toml
10
Cargo.toml
@@ -56,3 +56,13 @@ wasm = ["dioxus/web"]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true # 移除调试符号,减小二进制大小
|
||||
|
||||
# Windows 特定配置
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winres = "0.1"
|
||||
|
||||
[package.metadata.winres]
|
||||
LegalCopyright = "Copyright (c) 2026 damai"
|
||||
ProductName = "ReadFlow"
|
||||
FileDescription = "ReadFlow - 面向开发者和知识工作者的阅读工具"
|
||||
|
||||
377
scripts/build-release.sh
Executable file
377
scripts/build-release.sh
Executable file
@@ -0,0 +1,377 @@
|
||||
#!/bin/bash
|
||||
# ReadFlow 跨平台打包发布脚本
|
||||
# 支持:macOS (Intel/Apple Silicon), Windows, Linux
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 ReadFlow 打包发布脚本"
|
||||
echo "========================"
|
||||
|
||||
# 配置
|
||||
APP_NAME="readflow"
|
||||
VERSION="0.1.0"
|
||||
AUTHOR="damai <damai@foshanhuiya.com>"
|
||||
DESCRIPTION="ReadFlow - 面向开发者和知识工作者的阅读工具"
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 检测操作系统
|
||||
detect_os() {
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
echo "macos"
|
||||
;;
|
||||
Linux)
|
||||
echo "linux"
|
||||
;;
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
echo "windows"
|
||||
;;
|
||||
*)
|
||||
echo "unknown"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 检测 CPU 架构
|
||||
detect_arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64)
|
||||
echo "x86_64"
|
||||
;;
|
||||
arm64|aarch64)
|
||||
echo "aarch64"
|
||||
;;
|
||||
*)
|
||||
echo "unknown"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 构建 Release 版本
|
||||
build_release() {
|
||||
log_info "构建 Release 版本..."
|
||||
|
||||
# 清理之前的构建
|
||||
cargo clean
|
||||
|
||||
# 构建
|
||||
cargo build --release
|
||||
|
||||
log_info "构建完成!"
|
||||
}
|
||||
|
||||
# macOS 打包
|
||||
package_macos() {
|
||||
log_info "打包 macOS 应用..."
|
||||
|
||||
local arch=$(detect_arch)
|
||||
local target_dir="target/release"
|
||||
local package_dir="dist/${APP_NAME}-${VERSION}-macos-${arch}"
|
||||
local app_bundle="${package_dir}/${APP_NAME}.app"
|
||||
|
||||
# 创建目录结构
|
||||
mkdir -p "${app_bundle}/Contents/MacOS"
|
||||
mkdir -p "${app_bundle}/Contents/Resources"
|
||||
|
||||
# 复制二进制文件
|
||||
cp "${target_dir}/${APP_NAME}" "${app_bundle}/Contents/MacOS/"
|
||||
|
||||
# 创建 Info.plist
|
||||
cat > "${app_bundle}/Contents/Info.plist" << EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${APP_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.readflow.${APP_NAME}</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>ReadFlow</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>ReadFlow</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${VERSION}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${VERSION}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# 创建 PkgInfo
|
||||
echo "APPL????" > "${app_bundle}/Contents/PkgInfo"
|
||||
|
||||
# 复制资源文件
|
||||
if [ -d "assets" ]; then
|
||||
cp -r assets "${app_bundle}/Contents/Resources/"
|
||||
fi
|
||||
|
||||
# 创建 DMG (需要 create-dmg)
|
||||
if command -v create-dmg &> /dev/null; then
|
||||
log_info "创建 DMG 文件..."
|
||||
create-dmg \
|
||||
--volname "ReadFlow" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 600 400 \
|
||||
--icon-size 100 \
|
||||
--app-drop-link 450 200 \
|
||||
"dist/${APP_NAME}-${VERSION}-macos-${arch}.dmg" \
|
||||
"${app_bundle}"
|
||||
else
|
||||
log_warn "create-dmg 未安装,跳过 DMG 创建"
|
||||
log_info "安装包位于:${package_dir}"
|
||||
fi
|
||||
|
||||
log_info "macOS 打包完成!"
|
||||
}
|
||||
|
||||
# Linux 打包
|
||||
package_linux() {
|
||||
log_info "打包 Linux 应用..."
|
||||
|
||||
local arch=$(detect_arch)
|
||||
local target_dir="target/release"
|
||||
local package_dir="dist/${APP_NAME}-${VERSION}-linux-${arch}"
|
||||
|
||||
mkdir -p "${package_dir}"
|
||||
|
||||
# 复制二进制文件
|
||||
cp "${target_dir}/${APP_NAME}" "${package_dir}/"
|
||||
|
||||
# 创建 .desktop 文件
|
||||
cat > "${package_dir}/${APP_NAME}.desktop" << EOF
|
||||
[Desktop Entry]
|
||||
Name=ReadFlow
|
||||
Comment=${DESCRIPTION}
|
||||
Exec=${APP_NAME}
|
||||
Icon=${APP_NAME}
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility;Reading;
|
||||
EOF
|
||||
|
||||
# 创建 AppImage (需要 appimagetool)
|
||||
if command -v appimagetool &> /dev/null; then
|
||||
log_info "创建 AppImage..."
|
||||
# AppDir 结构
|
||||
local appdir="${package_dir}/AppDir"
|
||||
mkdir -p "${appdir}/usr/bin"
|
||||
mkdir -p "${appdir}/usr/share/applications"
|
||||
|
||||
cp "${target_dir}/${APP_NAME}" "${appdir}/usr/bin/"
|
||||
cp "${package_dir}/${APP_NAME}.desktop" "${appdir}/usr/share/applications/"
|
||||
|
||||
# 创建 AppRun
|
||||
cat > "${appdir}/AppRun" << EOF
|
||||
#!/bin/bash
|
||||
exec "\$(dirname "\$0")/usr/bin/${APP_NAME}" "\$@"
|
||||
EOF
|
||||
chmod +x "${appdir}/AppRun"
|
||||
|
||||
appimagetool "${appdir}" "dist/${APP_NAME}-${VERSION}-linux-${arch}.AppImage"
|
||||
else
|
||||
log_warn "appimagetool 未安装,跳过 AppImage 创建"
|
||||
fi
|
||||
|
||||
# 创建 tar.gz
|
||||
cd dist
|
||||
tar -czf "${APP_NAME}-${VERSION}-linux-${arch}.tar.gz" "${APP_NAME}-${VERSION}-linux-${arch}"
|
||||
cd ..
|
||||
|
||||
log_info "Linux 打包完成!"
|
||||
}
|
||||
|
||||
# Windows 打包
|
||||
package_windows() {
|
||||
log_info "打包 Windows 应用..."
|
||||
|
||||
local target_dir="target/release"
|
||||
local package_dir="dist/${APP_NAME}-${VERSION}-windows-x86_64"
|
||||
|
||||
mkdir -p "${package_dir}"
|
||||
|
||||
# 复制二进制文件
|
||||
cp "${target_dir}/${APP_NAME}.exe" "${package_dir}/"
|
||||
|
||||
# 复制依赖 DLL (如果需要)
|
||||
# cp "${target_dir}"/*.dll "${package_dir}/" 2>/dev/null || true
|
||||
|
||||
# 创建 NSIS 安装脚本 (需要 nsis)
|
||||
if command -v makensis &> /dev/null; then
|
||||
log_info "创建 NSIS 安装程序..."
|
||||
cat > "installer.nsi" << EOF
|
||||
!include "MUI2.nsh"
|
||||
|
||||
Name "ReadFlow"
|
||||
OutFile "dist/${APP_NAME}-${VERSION}-windows-x86_64-installer.exe"
|
||||
InstallDir "\$PROGRAMFILES\\ReadFlow"
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
Section "Install"
|
||||
SetOutPath "\$INSTDIR"
|
||||
File "${package_dir}\\${APP_NAME}.exe"
|
||||
WriteUninstaller "\$INSTDIR\\uninstall.exe"
|
||||
|
||||
CreateDirectory "\$SMPROGRAMS\\ReadFlow"
|
||||
CreateShortCut "\$SMPROGRAMS\\ReadFlow\\ReadFlow.lnk" "\$INSTDIR\\${APP_NAME}.exe"
|
||||
CreateShortCut "\$DESKTOP\\ReadFlow.lnk" "\$INSTDIR\\${APP_NAME}.exe"
|
||||
SectionEnd
|
||||
|
||||
Section "Uninstall"
|
||||
Delete "\$INSTDIR\\${APP_NAME}.exe"
|
||||
Delete "\$INSTDIR\\uninstall.exe"
|
||||
RMDir "\$INSTDIR"
|
||||
|
||||
Delete "\$SMPROGRAMS\\ReadFlow\\ReadFlow.lnk"
|
||||
RMDir "\$SMPROGRAMS\\ReadFlow"
|
||||
|
||||
Delete "\$DESKTOP\\ReadFlow.lnk"
|
||||
SectionEnd
|
||||
EOF
|
||||
|
||||
makensis installer.nsi
|
||||
rm installer.nsi
|
||||
else
|
||||
log_warn "nsis 未安装,跳过安装程序创建"
|
||||
fi
|
||||
|
||||
# 创建 ZIP
|
||||
cd dist
|
||||
zip -r "${APP_NAME}-${VERSION}-windows-x86_64.zip" "${APP_NAME}-${VERSION}-windows-x86_64"
|
||||
cd ..
|
||||
|
||||
log_info "Windows 打包完成!"
|
||||
}
|
||||
|
||||
# 创建发布说明
|
||||
create_release_notes() {
|
||||
log_info "创建发布说明..."
|
||||
|
||||
cat > "dist/RELEASE.md" << EOF
|
||||
# ReadFlow v${VERSION} 发布说明
|
||||
|
||||
## 下载
|
||||
|
||||
### macOS
|
||||
- [Intel](${APP_NAME}-${VERSION}-macos-x86_64.dmg)
|
||||
- [Apple Silicon](${APP_NAME}-${VERSION}-macos-aarch64.dmg)
|
||||
|
||||
### Linux
|
||||
- [AppImage](${APP_NAME}-${VERSION}-linux-x86_64.AppImage)
|
||||
- [tar.gz](${APP_NAME}-${VERSION}-linux-x86_64.tar.gz)
|
||||
|
||||
### Windows
|
||||
- [Installer](${APP_NAME}-${VERSION}-windows-x86_64-installer.exe)
|
||||
- [Portable](${APP_NAME}-${VERSION}-windows-x86_64.zip)
|
||||
|
||||
## 新功能
|
||||
|
||||
### Phase 2 - 核心功能
|
||||
- ✅ EPUB/MOBI/AZW3 格式支持
|
||||
- ✅ Markdown 阅读模式
|
||||
- ✅ 双语翻译功能
|
||||
- ✅ 笔记与书签系统
|
||||
|
||||
### Phase 3 - 高级功能
|
||||
- ✅ 代码阅读器 (20+ 语言支持)
|
||||
- ✅ 全文双语对照模式
|
||||
- ✅ 阅读进度同步
|
||||
- ✅ 插件系统
|
||||
|
||||
### Phase 4 - 性能与生态
|
||||
- ✅ 性能优化与分析
|
||||
- ✅ 个性化主题商店
|
||||
- ✅ 跨平台打包发布
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 语言:Rust
|
||||
- GUI: Dioxus
|
||||
- 存储:sled
|
||||
- 翻译:阿里百炼/DeepL/Ollama
|
||||
|
||||
## 系统要求
|
||||
|
||||
- macOS 10.15+
|
||||
- Windows 10+
|
||||
- Linux (glibc 2.31+)
|
||||
|
||||
## 反馈与支持
|
||||
|
||||
- GitHub: https://github.com/damai/readflow
|
||||
- Email: damai@foshanhuiya.com
|
||||
|
||||
---
|
||||
发布日期:$(date +%Y-%m-%d)
|
||||
EOF
|
||||
|
||||
log_info "发布说明已创建:dist/RELEASE.md"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
local os=$(detect_os)
|
||||
|
||||
log_info "检测到操作系统:${os}"
|
||||
|
||||
# 构建
|
||||
build_release
|
||||
|
||||
# 根据操作系统打包
|
||||
case "${os}" in
|
||||
macos)
|
||||
package_macos
|
||||
;;
|
||||
linux)
|
||||
package_linux
|
||||
;;
|
||||
windows)
|
||||
package_windows
|
||||
;;
|
||||
*)
|
||||
log_error "不支持的操作系统:${os}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# 创建发布说明
|
||||
create_release_notes
|
||||
|
||||
log_info "🎉 打包完成!"
|
||||
log_info "发布文件位于:dist/"
|
||||
|
||||
# 列出生成的文件
|
||||
echo ""
|
||||
echo "生成的文件:"
|
||||
ls -lh dist/
|
||||
}
|
||||
|
||||
# 运行主函数
|
||||
main "$@"
|
||||
@@ -1,6 +1,6 @@
|
||||
//! 核心服务模块
|
||||
//!
|
||||
//! 包含文档处理、翻译、书签、笔记、代码阅读、进度同步、插件、性能优化等功能
|
||||
//! 包含文档处理、翻译、书签、笔记、代码阅读、进度同步、插件、性能优化、主题等功能
|
||||
|
||||
pub mod document;
|
||||
pub mod translation;
|
||||
@@ -10,6 +10,7 @@ pub mod code_reader;
|
||||
pub mod progress;
|
||||
pub mod plugin;
|
||||
pub mod performance;
|
||||
pub mod theme;
|
||||
|
||||
pub use document::DocumentEngine;
|
||||
pub use translation::TranslationService;
|
||||
@@ -18,4 +19,5 @@ pub use note::{Note, NoteManager, NoteType, ReadingSession, ReadingStats};
|
||||
pub use code_reader::{CodeReader, CodeDocument, CodeLanguage};
|
||||
pub use progress::{ReadingProgress, ProgressManager, SyncConfig, CloudSync};
|
||||
pub use plugin::{PluginManager, Plugin, PluginManifest, PluginStatus, PluginInfo};
|
||||
pub use performance::{PerformanceProfiler, PerformanceMetrics, CacheManager};
|
||||
pub use performance::{PerformanceProfiler, PerformanceMetrics, CacheManager};
|
||||
pub use theme::{ThemeManager, ThemeManifest, ThemeConfig, ThemeType, BuiltinThemes};
|
||||
441
src/core/theme.rs
Normal file
441
src/core/theme.rs
Normal file
@@ -0,0 +1,441 @@
|
||||
//! 主题系统模块
|
||||
//!
|
||||
//! 支持主题切换、主题商店、自定义主题等功能
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// 主题元数据
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ThemeManifest {
|
||||
/// 主题唯一标识
|
||||
pub id: String,
|
||||
/// 主题名称
|
||||
pub name: String,
|
||||
/// 主题描述
|
||||
pub description: String,
|
||||
/// 主题版本
|
||||
pub version: String,
|
||||
/// 作者
|
||||
pub author: String,
|
||||
/// 主题类型
|
||||
pub theme_type: ThemeType,
|
||||
/// 预览图
|
||||
pub preview_image: Option<String>,
|
||||
/// CSS 文件路径
|
||||
pub css_file: Option<String>,
|
||||
}
|
||||
|
||||
/// 主题类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ThemeType {
|
||||
/// 浅色主题
|
||||
Light,
|
||||
/// 深色主题
|
||||
Dark,
|
||||
/// 护眼主题
|
||||
EyeCare,
|
||||
/// 高对比度
|
||||
HighContrast,
|
||||
/// 自定义
|
||||
Custom,
|
||||
}
|
||||
|
||||
/// 主题配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ThemeConfig {
|
||||
/// 主背景色
|
||||
pub bg_primary: String,
|
||||
/// 次背景色
|
||||
pub bg_secondary: String,
|
||||
/// 第三背景色
|
||||
pub bg_tertiary: String,
|
||||
/// 主文字颜色
|
||||
pub text_primary: String,
|
||||
/// 次文字颜色
|
||||
pub text_secondary: String,
|
||||
/// 强调色
|
||||
pub accent_color: String,
|
||||
/// 强调色悬停
|
||||
pub accent_hover: String,
|
||||
/// 边框颜色
|
||||
pub border_color: String,
|
||||
/// 字体族
|
||||
pub font_family: String,
|
||||
/// 字体大小
|
||||
pub font_size: String,
|
||||
/// 行高
|
||||
pub line_height: String,
|
||||
}
|
||||
|
||||
impl Default for ThemeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bg_primary: "#1a1a1a".to_string(),
|
||||
bg_secondary: "#2a2a2a".to_string(),
|
||||
bg_tertiary: "#3a3a3a".to_string(),
|
||||
text_primary: "#e0e0e0".to_string(),
|
||||
text_secondary: "#b0b0b0".to_string(),
|
||||
accent_color: "#5a9fe0".to_string(),
|
||||
accent_hover: "#6aafef".to_string(),
|
||||
border_color: "#404040".to_string(),
|
||||
font_family: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif".to_string(),
|
||||
font_size: "16px".to_string(),
|
||||
line_height: "1.6".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 内置主题
|
||||
pub struct BuiltinThemes;
|
||||
|
||||
impl BuiltinThemes {
|
||||
/// 获取所有内置主题
|
||||
pub fn get_all() -> Vec<ThemeManifest> {
|
||||
vec![
|
||||
Self::dark_theme(),
|
||||
Self::light_theme(),
|
||||
Self::eye_care_theme(),
|
||||
Self::high_contrast_theme(),
|
||||
]
|
||||
}
|
||||
|
||||
/// 深色主题
|
||||
fn dark_theme() -> ThemeManifest {
|
||||
ThemeManifest {
|
||||
id: "com.readflow.theme.dark".to_string(),
|
||||
name: "深色模式".to_string(),
|
||||
description: "经典的深色主题,适合夜间阅读".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
author: "ReadFlow Team".to_string(),
|
||||
theme_type: ThemeType::Dark,
|
||||
preview_image: None,
|
||||
css_file: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 浅色主题
|
||||
fn light_theme() -> ThemeManifest {
|
||||
ThemeManifest {
|
||||
id: "com.readflow.theme.light".to_string(),
|
||||
name: "浅色模式".to_string(),
|
||||
description: "明亮的浅色主题,适合日间阅读".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
author: "ReadFlow Team".to_string(),
|
||||
theme_type: ThemeType::Light,
|
||||
preview_image: None,
|
||||
css_file: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 护眼主题
|
||||
fn eye_care_theme() -> ThemeManifest {
|
||||
ThemeManifest {
|
||||
id: "com.readflow.theme.eyecare".to_string(),
|
||||
name: "护眼模式".to_string(),
|
||||
description: "柔和的绿色调,减少眼睛疲劳".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
author: "ReadFlow Team".to_string(),
|
||||
theme_type: ThemeType::EyeCare,
|
||||
preview_image: None,
|
||||
css_file: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 高对比度主题
|
||||
fn high_contrast_theme() -> ThemeManifest {
|
||||
ThemeManifest {
|
||||
id: "com.readflow.theme.contrast".to_string(),
|
||||
name: "高对比度".to_string(),
|
||||
description: "增强对比度,提高可读性".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
author: "ReadFlow Team".to_string(),
|
||||
theme_type: ThemeType::HighContrast,
|
||||
preview_image: None,
|
||||
css_file: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取主题的 CSS 变量
|
||||
pub fn get_css_variables(theme_id: &str) -> String {
|
||||
match theme_id {
|
||||
"com.readflow.theme.dark" => Self::dark_css().to_string(),
|
||||
"com.readflow.theme.light" => Self::light_css().to_string(),
|
||||
"com.readflow.theme.eyecare" => Self::eyecare_css().to_string(),
|
||||
"com.readflow.theme.contrast" => Self::contrast_css().to_string(),
|
||||
_ => Self::dark_css().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dark_css() -> &'static str {
|
||||
r#"
|
||||
:root {
|
||||
--bg-primary: #1a1a1a;
|
||||
--bg-secondary: #2a2a2a;
|
||||
--bg-tertiary: #3a3a3a;
|
||||
--text-primary: #e0e0e0;
|
||||
--text-secondary: #b0b0b0;
|
||||
--text-muted: #808080;
|
||||
--border-color: #404040;
|
||||
--accent-color: #5a9fe0;
|
||||
--accent-hover: #6aafef;
|
||||
--shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
fn light_css() -> &'static str {
|
||||
r#"
|
||||
:root {
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f5f5f5;
|
||||
--bg-tertiary: #e8e8e8;
|
||||
--text-primary: #1a1a1a;
|
||||
--text-secondary: #4a4a4a;
|
||||
--text-muted: #8a8a8a;
|
||||
--border-color: #d0d0d0;
|
||||
--accent-color: #2196f3;
|
||||
--accent-hover: #1976d2;
|
||||
--shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
fn eyecare_css() -> &'static str {
|
||||
r#"
|
||||
:root {
|
||||
--bg-primary: #f0f4e8;
|
||||
--bg-secondary: #e3e9d6;
|
||||
--bg-tertiary: #d4dcc4;
|
||||
--text-primary: #2d3a1e;
|
||||
--text-secondary: #4a5a36;
|
||||
--text-muted: #6b7a54;
|
||||
--border-color: #c5d4b0;
|
||||
--accent-color: #5a8f3a;
|
||||
--accent-hover: #4a7f2a;
|
||||
--shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
fn contrast_css() -> &'static str {
|
||||
r#"
|
||||
:root {
|
||||
--bg-primary: #000000;
|
||||
--bg-secondary: #1a1a1a;
|
||||
--bg-tertiary: #2a2a2a;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #f0f0f0;
|
||||
--text-muted: #cccccc;
|
||||
--border-color: #ffffff;
|
||||
--accent-color: #ffff00;
|
||||
--accent-hover: #ffff66;
|
||||
--shadow: 0 2px 8px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
"#
|
||||
}
|
||||
}
|
||||
|
||||
/// 主题管理器
|
||||
pub struct ThemeManager {
|
||||
/// 当前主题 ID
|
||||
current_theme_id: String,
|
||||
/// 主题目录
|
||||
themes_dir: PathBuf,
|
||||
/// 已安装的主题
|
||||
installed_themes: HashMap<String, ThemeManifest>,
|
||||
/// 主题配置
|
||||
configs: HashMap<String, ThemeConfig>,
|
||||
}
|
||||
|
||||
impl ThemeManager {
|
||||
/// 创建主题管理器
|
||||
pub fn new(themes_dir: &str) -> Result<Self> {
|
||||
let themes_dir = PathBuf::from(themes_dir);
|
||||
|
||||
if !themes_dir.exists() {
|
||||
std::fs::create_dir_all(&themes_dir)?;
|
||||
}
|
||||
|
||||
let mut manager = Self {
|
||||
current_theme_id: "com.readflow.theme.dark".to_string(),
|
||||
themes_dir,
|
||||
installed_themes: HashMap::new(),
|
||||
configs: HashMap::new(),
|
||||
};
|
||||
|
||||
// 扫描已安装的主题
|
||||
manager.scan_themes()?;
|
||||
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
/// 扫描主题目录
|
||||
pub fn scan_themes(&mut self) -> Result<()> {
|
||||
if !self.themes_dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for entry in std::fs::read_dir(&self.themes_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let manifest_path = path.join("manifest.json");
|
||||
if !manifest_path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let manifest_content = std::fs::read_to_string(&manifest_path)?;
|
||||
let manifest: ThemeManifest = serde_json::from_str(&manifest_content)?;
|
||||
|
||||
self.installed_themes.insert(manifest.id.clone(), manifest);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取所有可用主题
|
||||
pub fn get_all_themes(&self) -> Vec<ThemeManifest> {
|
||||
let mut themes = BuiltinThemes::get_all();
|
||||
|
||||
// 添加已安装的主题
|
||||
for theme in self.installed_themes.values() {
|
||||
themes.push(theme.clone());
|
||||
}
|
||||
|
||||
themes
|
||||
}
|
||||
|
||||
/// 获取当前主题
|
||||
pub fn get_current_theme(&self) -> String {
|
||||
self.current_theme_id.clone()
|
||||
}
|
||||
|
||||
/// 设置当前主题
|
||||
pub fn set_current_theme(&mut self, theme_id: &str) -> Result<()> {
|
||||
// 验证主题是否存在
|
||||
let all_themes = self.get_all_themes();
|
||||
if !all_themes.iter().any(|t| t.id == theme_id) {
|
||||
anyhow::bail!("主题不存在:{}", theme_id);
|
||||
}
|
||||
|
||||
self.current_theme_id = theme_id.to_string();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取当前主题的 CSS
|
||||
pub fn get_current_css(&self) -> String {
|
||||
BuiltinThemes::get_css_variables(&self.current_theme_id)
|
||||
}
|
||||
|
||||
/// 安装主题
|
||||
pub fn install_theme(&mut self, theme_path: &Path) -> Result<String> {
|
||||
let manifest_path = theme_path.join("manifest.json");
|
||||
let manifest_content = std::fs::read_to_string(&manifest_path)?;
|
||||
let manifest: ThemeManifest = serde_json::from_str(&manifest_content)?;
|
||||
|
||||
// 复制到主题目录
|
||||
let target_path = self.themes_dir.join(&manifest.id);
|
||||
|
||||
if target_path.exists() {
|
||||
anyhow::bail!("主题已安装:{}", manifest.id);
|
||||
}
|
||||
|
||||
// 复制整个主题目录
|
||||
self.copy_dir_recursive(theme_path, &target_path)?;
|
||||
|
||||
// 添加到已安装列表
|
||||
self.installed_themes.insert(manifest.id.clone(), manifest.clone());
|
||||
|
||||
Ok(manifest.id.clone())
|
||||
}
|
||||
|
||||
/// 卸载主题
|
||||
pub fn uninstall_theme(&mut self, theme_id: &str) -> Result<()> {
|
||||
// 不能卸载内置主题
|
||||
if theme_id.starts_with("com.readflow.theme.") {
|
||||
anyhow::bail!("无法卸载内置主题");
|
||||
}
|
||||
|
||||
// 如果当前正在使用此主题,切换回默认主题
|
||||
if self.current_theme_id == theme_id {
|
||||
self.current_theme_id = "com.readflow.theme.dark".to_string();
|
||||
}
|
||||
|
||||
// 从文件系统删除
|
||||
let theme_path = self.themes_dir.join(theme_id);
|
||||
if theme_path.exists() {
|
||||
std::fs::remove_dir_all(&theme_path)?;
|
||||
}
|
||||
|
||||
// 从内存移除
|
||||
self.installed_themes.remove(theme_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 自定义主题配置
|
||||
pub fn customize_theme(&mut self, theme_id: &str, config: ThemeConfig) -> Result<()> {
|
||||
self.configs.insert(theme_id.to_string(), config);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 导出主题为 JSON
|
||||
pub fn export_theme(&self, theme_id: &str) -> Result<String> {
|
||||
let all_themes = self.get_all_themes();
|
||||
let theme = all_themes.iter()
|
||||
.find(|t| t.id == theme_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("主题不存在:{}", theme_id))?;
|
||||
|
||||
let json = serde_json::to_string_pretty(theme)?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// 递归复制目录
|
||||
fn copy_dir_recursive(&self, src: &Path, dst: &Path) -> Result<()> {
|
||||
std::fs::create_dir_all(dst)?;
|
||||
|
||||
for entry in std::fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let src_path = entry.path();
|
||||
let dst_path = dst.join(entry.file_name());
|
||||
|
||||
if src_path.is_dir() {
|
||||
self.copy_dir_recursive(&src_path, &dst_path)?;
|
||||
} else {
|
||||
std::fs::copy(&src_path, &dst_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_builtin_themes() {
|
||||
let themes = BuiltinThemes::get_all();
|
||||
assert_eq!(themes.len(), 4);
|
||||
|
||||
assert_eq!(themes[0].id, "com.readflow.theme.dark");
|
||||
assert_eq!(themes[1].id, "com.readflow.theme.light");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_css_variables() {
|
||||
let dark_css = BuiltinThemes::get_css_variables("com.readflow.theme.dark");
|
||||
assert!(dark_css.contains("--bg-primary: #1a1a1a"));
|
||||
|
||||
let light_css = BuiltinThemes::get_css_variables("com.readflow.theme.light");
|
||||
assert!(light_css.contains("--bg-primary: #ffffff"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user