feat: 完成 Phase 2-4 核心功能
## Phase 2 - 核心功能 (P0) - Issue #5: EPUB/MOBI/AZW3 格式支持 ✅ - 修复 mobi 库 API 调用 (content_raw → content_as_string) - 修复 title()/author() 返回类型 - 添加元数据提取功能 - Issue #6: Markdown 阅读模式 ✅ - 实现 parse_markdown_with_metadata - 支持 Front Matter (YAML) 解析 - 使用 pulldown-cmark 解析引擎 - 支持代码文件高亮 - Issue #7: 双语翻译功能 ✅ - 实现 TranslationService (阿里百炼/DeepL/Ollama) - 语言自动检测 - 双语对照 HTML 渲染 (并排/段落交错模式) - Issue #8: 笔记与书签系统 ✅ - BookmarkManager (高亮/下划线/波浪线/边注) - NoteManager (阅读笔记/想法/问题/总结) - 阅读统计 (时长/会话数/笔记数) - 导出 Markdown/CSV/Anki ## Phase 3 - 高级功能 (P1) - Issue #9: 代码阅读器 ✅ - 支持 20+ 编程语言 - syntect 语法高亮 - 行号显示/代码折叠 - Issue #10: 全文双语对照 ✅ - 段落级翻译对照 - 并排/交错两种模式 - 响应式布局 - Issue #11: 阅读进度同步 ✅ - 本地进度追踪 - 云端同步支持 - 多设备冲突解决 - Issue #12: 插件系统 ✅ - 插件加载/卸载/启用/禁用 - 插件依赖管理 - 内置主题/快捷键插件 ## Phase 4 - 性能与生态 (P1) - Issue #13: 性能优化 ✅ - PerformanceProfiler 性能分析 - CacheManager LRU 缓存 - 性能监控与优化建议 ## 技术栈更新 - 新增依赖:reqwest, uuid, chrono(serde) - 核心模块:8 个 (document/translation/bookmark/note/code_reader/progress/plugin/performance) - 代码量:~5000 行 --- 🚀 ReadFlow MVP 核心功能全部完成!
This commit is contained in:
225
src/config/theme.rs
Normal file
225
src/config/theme.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
//! 主题系统模块
|
||||
//!
|
||||
//! 管理深色/浅色主题、字体、行距等阅读样式
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// 主题模式
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ThemeMode {
|
||||
Light,
|
||||
#[default]
|
||||
Dark,
|
||||
}
|
||||
|
||||
impl fmt::Display for ThemeMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ThemeMode::Light => write!(f, "Light"),
|
||||
ThemeMode::Dark => write!(f, "Dark"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 主题配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ThemeConfig {
|
||||
/// 主题模式
|
||||
pub mode: ThemeMode,
|
||||
/// 字体大小 (12-24px)
|
||||
pub font_size: u32,
|
||||
/// 字体家族
|
||||
pub font_family: String,
|
||||
/// 行距 (1.0-2.0)
|
||||
pub line_height: f32,
|
||||
/// 字距
|
||||
pub letter_spacing: f32,
|
||||
/// 可选的字体列表
|
||||
#[serde(default)]
|
||||
pub font_options: Vec<FontOption>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FontOption {
|
||||
pub name: String,
|
||||
pub css_name: String,
|
||||
}
|
||||
|
||||
impl Default for ThemeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mode: ThemeMode::Dark,
|
||||
font_size: 16,
|
||||
font_family: "system".to_string(),
|
||||
line_height: 1.5,
|
||||
letter_spacing: 0.0,
|
||||
font_options: vec![
|
||||
FontOption { name: "系统默认".to_string(), css_name: "system".to_string() },
|
||||
FontOption { name: "宋体".to_string(), css_name: "SimSun, Songti SC".to_string() },
|
||||
FontOption { name: "黑体".to_string(), css_name: "PingFang SC, Microsoft YaHei".to_string() },
|
||||
FontOption { name: "楷体".to_string(), css_name: "Kaiti SC, KaiTi".to_string() },
|
||||
FontOption { name: "等宽字体".to_string(), css_name: "Menlo, Monaco, Consolas".to_string() },
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成主题 CSS 变量
|
||||
pub fn generate_css_vars(config: &ThemeConfig) -> String {
|
||||
let (bg_primary, bg_secondary, text_primary, text_secondary, accent) = match config.mode {
|
||||
ThemeMode::Light => (
|
||||
"#ffffff", // bg_primary
|
||||
"#f5f5f5", // bg_secondary
|
||||
"#1a1a1a", // text_primary
|
||||
"#666666", // text_secondary
|
||||
"#0066cc", // accent
|
||||
),
|
||||
ThemeMode::Dark => (
|
||||
"#1e1e1e", // bg_primary
|
||||
"#2d2d2d", // bg_secondary
|
||||
"#e0e0e0", // text_primary
|
||||
"#a0a0a0", // text_secondary
|
||||
"#4da6ff", // accent
|
||||
),
|
||||
};
|
||||
|
||||
format!(r#"
|
||||
:root {{
|
||||
--bg-primary: {bg_primary};
|
||||
--bg-secondary: {bg_secondary};
|
||||
--text-primary: {text_primary};
|
||||
--text-secondary: {text_secondary};
|
||||
--accent: {accent};
|
||||
|
||||
--font-size: {font_size}px;
|
||||
--font-family: {font_family}, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
--line-height: {line_height};
|
||||
--letter-spacing: {letter_spacing}px;
|
||||
|
||||
--border-radius: 8px;
|
||||
--transition: 0.2s ease;
|
||||
}}
|
||||
|
||||
body {{
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--font-size);
|
||||
line-height: var(--line-height);
|
||||
letter-spacing: var(--letter-spacing);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
transition: background-color var(--transition), color var(--transition);
|
||||
}}
|
||||
|
||||
.reader-content {{
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}}
|
||||
|
||||
.theme-toggle {{
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border: none;
|
||||
transition: background-color var(--transition);
|
||||
}}
|
||||
|
||||
.theme-toggle:hover {{
|
||||
background-color: var(--accent);
|
||||
color: white;
|
||||
}}
|
||||
|
||||
.sidebar {{
|
||||
background-color: var(--bg-secondary);
|
||||
padding: 16px;
|
||||
}}
|
||||
|
||||
.settings-panel {{
|
||||
background-color: var(--bg-secondary);
|
||||
padding: 20px;
|
||||
border-radius: var(--border-radius);
|
||||
margin: 16px;
|
||||
}}
|
||||
|
||||
.slider-control {{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 12px 0;
|
||||
}}
|
||||
|
||||
.slider-control label {{
|
||||
min-width: 80px;
|
||||
}}
|
||||
|
||||
.slider-control input[type="range"] {{
|
||||
flex: 1;
|
||||
accent-color: var(--accent);
|
||||
}}
|
||||
|
||||
.select-control {{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 12px 0;
|
||||
}}
|
||||
|
||||
.select-control select {{
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--text-secondary);
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}}
|
||||
"#,
|
||||
bg_primary = bg_primary,
|
||||
bg_secondary = bg_secondary,
|
||||
text_primary = text_primary,
|
||||
text_secondary = text_secondary,
|
||||
accent = accent,
|
||||
font_size = config.font_size,
|
||||
font_family = config.font_family,
|
||||
line_height = config.line_height,
|
||||
letter_spacing = config.letter_spacing
|
||||
)
|
||||
}
|
||||
|
||||
/// 保存主题配置到文件
|
||||
pub fn save_config(config: &ThemeConfig) -> anyhow::Result<()> {
|
||||
let config_dir = get_config_dir()?;
|
||||
fs::create_dir_all(&config_dir)?;
|
||||
let config_path = config_dir.join("theme.json");
|
||||
let json = serde_json::to_string_pretty(config)?;
|
||||
fs::write(config_path, json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 从文件加载主题配置
|
||||
pub fn load_config() -> ThemeConfig {
|
||||
let config_dir = get_config_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
let config_path = config_dir.join("theme.json");
|
||||
|
||||
if config_path.exists() {
|
||||
if let Ok(content) = fs::read_to_string(&config_path) {
|
||||
if let Ok(config) = serde_json::from_str(&content) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ThemeConfig::default()
|
||||
}
|
||||
|
||||
fn get_config_dir() -> anyhow::Result<PathBuf> {
|
||||
let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Cannot find home directory"))?;
|
||||
Ok(home.join(".config").join("readflow"))
|
||||
}
|
||||
Reference in New Issue
Block a user