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:
大麦
2026-03-10 14:33:36 +08:00
parent 600f205c87
commit 93f2f02d46
4 changed files with 832 additions and 2 deletions

View File

@@ -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
View 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 "$@"

View File

@@ -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
View 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"));
}
}