//! 主题系统模块 //! //! 管理深色/浅色主题、字体、行距等阅读样式 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, } #[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 { let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Cannot find home directory"))?; Ok(home.join(".config").join("readflow")) }