From 93f2f02d4641ad021fe16af84ce243b04736fbc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E9=BA=A6?= Date: Tue, 10 Mar 2026 14:33:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=20Issue=20#14-15=20?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E5=95=86=E5=BA=97=E4=B8=8E=E8=B7=A8=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E6=89=93=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 全部完成! --- Cargo.toml | 10 + scripts/build-release.sh | 377 +++++++++++++++++++++++++++++++++ src/core/mod.rs | 6 +- src/core/theme.rs | 441 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 832 insertions(+), 2 deletions(-) create mode 100755 scripts/build-release.sh create mode 100644 src/core/theme.rs diff --git a/Cargo.toml b/Cargo.toml index 7003496..3f684ed 100644 --- a/Cargo.toml +++ b/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 - 面向开发者和知识工作者的阅读工具" diff --git a/scripts/build-release.sh b/scripts/build-release.sh new file mode 100755 index 0000000..128f3b9 --- /dev/null +++ b/scripts/build-release.sh @@ -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 " +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 + + + + + CFBundleExecutable + ${APP_NAME} + CFBundleIdentifier + com.readflow.${APP_NAME} + CFBundleName + ReadFlow + CFBundleDisplayName + ReadFlow + CFBundleVersion + ${VERSION} + CFBundleShortVersionString + ${VERSION} + CFBundlePackageType + APPL + NSHighResolutionCapable + + + +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 "$@" diff --git a/src/core/mod.rs b/src/core/mod.rs index 2013e6e..42e10d0 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -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}; \ No newline at end of file +pub use performance::{PerformanceProfiler, PerformanceMetrics, CacheManager}; +pub use theme::{ThemeManager, ThemeManifest, ThemeConfig, ThemeType, BuiltinThemes}; \ No newline at end of file diff --git a/src/core/theme.rs b/src/core/theme.rs new file mode 100644 index 0000000..c18b0f9 --- /dev/null +++ b/src/core/theme.rs @@ -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, + /// CSS 文件路径 + pub css_file: Option, +} + +/// 主题类型 +#[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 { + 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, + /// 主题配置 + configs: HashMap, +} + +impl ThemeManager { + /// 创建主题管理器 + pub fn new(themes_dir: &str) -> Result { + 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 { + 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 { + 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 { + 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")); + } +}