diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad44091 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# 构建产物 +/target/ +debug/ +release/ + +# Cargo +Cargo.lock + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# 系统文件 +.DS_Store +Thumbs.db + +# 临时文件 +*.tmp +*.log \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 19d9f03..06791cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ serde_json = "1.0" config = "0.14" anyhow = "1.0" tracing = "0.1" -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } # 工具 rayon = "1.8" # 并行计算 diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..2837547 --- /dev/null +++ b/assets/style.css @@ -0,0 +1,177 @@ +/* ReadFlow 基础样式 */ + +:root { + /* 浅色主题 */ + --bg-primary: #ffffff; + --bg-secondary: #f5f5f5; + --bg-tertiary: #e8e8e8; + --text-primary: #333333; + --text-secondary: #666666; + --text-muted: #999999; + --border-color: #e0e0e0; + --accent-color: #4a90d9; + --accent-hover: #3a7bc8; + --shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +[data-theme="dark"] { + /* 深色主题 */ + --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); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-size: 16px; + line-height: 1.6; + color: var(--text-primary); + background-color: var(--bg-primary); +} + +/* 文档容器 */ +.document { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} + +/* 页面样式 */ +.page { + background: var(--bg-primary); + border: 1px solid var(--border-color); + margin-bottom: 20px; + padding: 40px; + box-shadow: var(--shadow); + min-height: 300px; +} + +.pdf-page { + aspect-ratio: 8.5 / 11; +} + +/* 文本内容 */ +.text-page { + white-space: pre-wrap; + word-wrap: break-word; +} + +/* 代码块 */ +pre, code { + font-family: "SF Mono", Monaco, "Courier New", monospace; + font-size: 14px; + background: var(--bg-secondary); + border-radius: 4px; +} + +pre { + padding: 16px; + overflow-x: auto; +} + +code { + padding: 2px 6px; +} + +/* 搜索结果高亮 */ +.highlight { + background-color: #ffeb3b; + padding: 2px 4px; + border-radius: 2px; +} + +[data-theme="dark"] .highlight { + background-color: #ffc107; +} + +/* 目录 */ +.toc { + background: var(--bg-secondary); + padding: 16px; + border-radius: 8px; + margin-bottom: 20px; +} + +.toc-entry { + padding: 8px 0; + cursor: pointer; + transition: color 0.2s; +} + +.toc-entry:hover { + color: var(--accent-color); +} + +.toc-entry.level-1 { + font-weight: bold; +} + +.toc-entry.level-2 { + padding-left: 20px; +} + +.toc-entry.level-3 { + padding-left: 40px; +} + +/* 滚动条 */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-secondary); +} + +::-webkit-scrollbar-thumb { + background: var(--text-muted); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} + +/* 主题切换按钮 */ +.theme-toggle { + position: fixed; + top: 20px; + right: 20px; + padding: 8px 16px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 20px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} + +.theme-toggle:hover { + background: var(--accent-color); + color: white; +} + +/* 响应式 */ +@media (max-width: 768px) { + .document { + padding: 10px; + } + + .page { + padding: 20px; + } +} \ No newline at end of file diff --git a/src/core/document.rs b/src/core/document.rs index 6d13ed0..237d967 100644 --- a/src/core/document.rs +++ b/src/core/document.rs @@ -1,22 +1,51 @@ //! 文档处理引擎 +//! +//! 支持 PDF、EPUB、MOBI、TXT、Markdown 等格式 -use anyhow::Result; +use anyhow::{Context, Result}; +use std::path::Path; +#[derive(Debug)] pub enum DocumentFormat { Pdf, Epub, Mobi, Azw3, - Txt, + Txt, Markdown, - Code, + Code(String), // 代码语言 } pub struct Document { pub format: DocumentFormat, pub title: String, pub path: String, - pub content: Vec, + pub metadata: DocumentMetadata, + pub pages: Vec, +} + +#[derive(Debug, Default)] +pub struct DocumentMetadata { + pub author: Option, + pub page_count: usize, + pub file_size: u64, + pub creation_date: Option, + pub modification_date: Option, +} + +#[derive(Debug, Clone)] +pub struct Page { + pub number: usize, + pub width: f32, + pub height: f32, + pub content: PageContent, +} + +#[derive(Debug, Clone)] +pub enum PageContent { + Pdf(Vec), // PDF 渲染数据 + Text(String), // 纯文本内容 + Html(String), // HTML 渲染内容 } pub struct DocumentEngine; @@ -26,18 +55,271 @@ impl DocumentEngine { Self } + /// 根据文件扩展名判断文档格式 + fn detect_format(path: &str) -> Option { + let path = Path::new(path); + let ext = path.extension()?.to_str()?.to_lowercase(); + + match ext.as_str() { + "pdf" => Some(DocumentFormat::Pdf), + "epub" => Some(DocumentFormat::Epub), + "mobi" => Some(DocumentFormat::Mobi), + "azw" | "azw3" => Some(DocumentFormat::Azw3), + "txt" | "text" => Some(DocumentFormat::Txt), + "md" | "markdown" => Some(DocumentFormat::Markdown), + "js" | "ts" | "py" | "rs" | "go" | "java" | "c" | "cpp" | "h" | "css" | "html" | "json" | "xml" | "yaml" | "yml" | "toml" | "sql" | "sh" | "bash" | "zsh" => { + Some(DocumentFormat::Code(ext)) + } + _ => None, + } + } + + /// 打开文档 pub fn open(&self, path: &str) -> Result { - // 后续实现:基于文件扩展名判断格式并解析 - todo!("Implement document opening for: {}", path) + let format = Self::detect_format(path) + .context("Unsupported document format")?; + + let path_obj = Path::new(path); + let title = path_obj + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("Untitled") + .to_string(); + + let metadata = std::fs::metadata(path) + .map(|m| DocumentMetadata { + file_size: m.len(), + ..Default::default() + }) + .unwrap_or_default(); + + // 读取文件内容 + let content = std::fs::read(path)?; + + // 根据格式解析文档 + let pages = match format { + DocumentFormat::Pdf => self.parse_pdf(&content)?, + DocumentFormat::Epub => self.parse_epub(&content)?, + DocumentFormat::Mobi => self.parse_mobi(&content)?, + _ => vec![Page { + number: 1, + width: 612.0, + height: 792.0, + content: PageContent::Text(String::from_utf8_lossy(&content).to_string()), + }], + }; + + Ok(Document { + format, + title, + path: path.to_string(), + metadata: DocumentMetadata { + page_count: pages.len(), + ..metadata + }, + pages, + }) } + /// 解析 PDF 文档 + fn parse_pdf(&self, content: &[u8]) -> Result> { + // 使用 pdfium-render 库解析 PDF + // 这里简化实现,实际需要更复杂的处理 + + // 创建一个简单的页面列表 + // 实际实现中,pdfium 会返回每个页面的渲染数据 + Ok(vec![Page { + number: 1, + width: 612.0, // 美国信纸宽度 (8.5" * 72 dpi) + height: 792.0, // 美国信纸高度 + content: PageContent::Pdf(content.to_vec()), + }]) + } + + /// 解析 EPUB 文档 + fn parse_epub(&self, content: &[u8]) -> Result> { + // 使用 epub 库解析 + // 简化实现 + Ok(vec![Page { + number: 1, + width: 612.0, + height: 792.0, + content: PageContent::Text(String::from_utf8_lossy(content).to_string()), + }]) + } + + /// 解析 MOBI 文档 + fn parse_mobi(&self, content: &[u8]) -> Result> { + // 使用 mobi 库解析 + Ok(vec![Page { + number: 1, + width: 600.0, + height: 800.0, + content: PageContent::Text(String::from_utf8_lossy(content).to_string()), + }]) + } + + /// 渲染文档为 HTML pub fn render(&self, doc: &Document) -> Result { - // 后续实现:渲染文档内容 - todo!("Implement document rendering") + let mut html = String::new(); + + html.push_str("\n\n\n"); + html.push_str("\n"); + html.push_str(&format!("{}\n", doc.title)); + html.push_str("\n\n\n"); + html.push_str("
\n"); + + for page in &doc.pages { + match &page.content { + PageContent::Pdf(data) => { + html.push_str(&format!( + "
\n", + page.number + )); + // 后续:Base64 编码的 PDF 数据用于嵌入 + html.push_str("
\n"); + } + PageContent::Text(text) => { + html.push_str(&format!( + "
\n", + page.number + )); + html.push_str(&self.format_text_content(text)); + html.push_str("
\n"); + } + PageContent::Html(html_content) => { + html.push_str(html_content); + } + } + } + + html.push_str("
\n\n"); + + Ok(html) } - pub fn search(&self, doc: &Document, query: &str) -> Result> { - // 后续实现:全文搜索 - todo!("Implement search functionality") + /// 获取默认 CSS 样式 + fn get_default_css() -> &'static str { + r#":root { + --bg-primary: #ffffff; + --bg-secondary: #f5f5f5; + --bg-tertiary: #e8e8e8; + --text-primary: #333333; + --text-secondary: #666666; + --text-muted: #999999; + --border-color: #e0e0e0; + --accent-color: #4a90d9; + --accent-hover: #3a7bc8; + --shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} +[data-theme="dark"] { + --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); +} +* { margin: 0; padding: 0; box-sizing: border-box; } +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 16px; + line-height: 1.6; + color: var(--text-primary); + background-color: var(--bg-primary); +} +.document { max-width: 800px; margin: 0 auto; padding: 20px; } +.page { + background: var(--bg-primary); + border: 1px solid var(--border-color); + margin-bottom: 20px; + padding: 40px; + box-shadow: var(--shadow); + min-height: 300px; +} +.text-page { white-space: pre-wrap; word-wrap: break-word; } +pre, code { + font-family: "SF Mono", Monaco, monospace; + font-size: 14px; + background: var(--bg-secondary); + border-radius: 4px; +} +pre { padding: 16px; overflow-x: auto; } +code { padding: 2px 6px; } +::-webkit-scrollbar { width: 8px; height: 8px; } +::-webkit-scrollbar-track { background: var(--bg-secondary); } +::-webkit-scrollbar-thumb { background: var(--text-muted); border-radius: 4px; } +"# } + + /// 格式化文本内容 + fn format_text_content(&self, text: &str) -> String { + // 转义 HTML 特殊字符 + let escaped = text + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'"); + + // 保留换行 + escaped.replace('\n', "
\n") + } + + /// 搜索文档内容 + pub fn search(&self, doc: &Document, query: &str) -> Result> { + let mut results = Vec::new(); + let query_lower = query.to_lowercase(); + + for page in &doc.pages { + if let PageContent::Text(text) = &page.content { + let text_lower = text.to_lowercase(); + + // 简单实现:查找所有匹配位置 + let mut start = 0; + while let Some(pos) = text_lower[start..].find(&query_lower) { + let absolute_pos = start + pos; + let context_start = absolute_pos.saturating_sub(50); + let context_end = (absolute_pos + query.len() + 50).min(text.len()); + + results.push(SearchResult { + page: page.number, + position: absolute_pos, + context: text[context_start..context_end].to_string(), + }); + + start = absolute_pos + 1; + } + } + } + + Ok(results) + } + + /// 获取目录结构 + pub fn get_toc(&self, doc: &Document) -> Result> { + // 简化实现:返回空目录 + // 后续可以从 PDF/EPUB 元数据中提取目录 + Ok(vec![]) + } +} + +#[derive(Debug, Clone)] +pub struct SearchResult { + pub page: usize, + pub position: usize, + pub context: String, +} + +#[derive(Debug, Clone)] +pub struct TocEntry { + pub title: String, + pub page: usize, + pub level: usize, } \ No newline at end of file diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 604e4fd..52caf1b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,20 +1,182 @@ //! UI 模块 //! -//! 使用 Dioxus 构建跨平台 UI +//! ReadFlow 用户界面 +//! +//! 当前版本使用 CLI/TUI 模式,后续可扩展为桌面 GUI use crate::config::Config; +use crate::core::document::DocumentEngine; pub fn run(config: Config) { - println!("Starting UI with config: {:?}", config.theme); + println!("╔══════════════════════════════════════╗"); + println!("║ ReadFlow v0.1.0 ║"); + println!("║ 面向开发者的文档阅读工具 ║"); + println!("╚══════════════════════════════════════╝"); + println!(); + println!("主题: {}", config.theme.mode); + println!("默认格式: {}", config.reader.default_format); + println!("书库路径: {}", config.storage.library_path); + println!(); - // 后续实现:Dioxus UI 启动 - // 示例: - // dioxus::launch(App); + // 检查命令行参数 + let args: Vec = std::env::args().collect(); + + if args.len() < 2 { + print_help(); + return; + } + + let command = &args[1]; + + match command.as_str() { + "open" => { + if args.len() < 3 { + eprintln!("用法: readflow open <文件路径>"); + return; + } + open_document(&args[2]); + } + "search" => { + if args.len() < 4 { + eprintln!("用法: readflow search <文件路径> <关键词>"); + return; + } + search_document(&args[2], &args[3]); + } + "info" => { + if args.len() < 3 { + eprintln!("用法: readflow info <文件路径>"); + return; + } + show_document_info(&args[2]); + } + "help" | "--help" | "-h" => { + print_help(); + } + _ => { + // 尝试直接打开文件 + open_document(&args[1]); + } + } } -// 后续实现:Dioxus 组件 -// pub fn App(cx: Scope) -> Element { -// cx.render(rsx! { -// div { "Hello, ReadFlow!" } -// }) -// } \ No newline at end of file +fn print_help() { + println!("用法:"); + println!(" readflow <文件路径> 打开文档"); + println!(" readflow open <文件路径> 打开文档"); + println!(" readflow info <文件路径> 显示文档信息"); + println!(" readflow search <文件> <关键词> 搜索文档内容"); + println!(); + println!("支持格式: PDF, EPUB, MOBI, TXT, Markdown, 代码文件"); +} + +fn open_document(path: &str) { + println!("正在打开: {}", path); + println!("{}", "-".repeat(50)); + + let engine = DocumentEngine::new(); + + match engine.open(path) { + Ok(doc) => { + println!("✅ 文档打开成功!"); + println!(); + println!("📖 {}", doc.title); + println!("📄 格式: {:?}", doc.format); + println!("📑 页数: {}", doc.metadata.page_count); + println!("💾 大小: {} bytes", doc.metadata.file_size); + println!(); + + // 渲染文档内容(简化版) + match engine.render(&doc) { + Ok(html) => { + // 只显示前几行 + let preview: String = html.lines().take(20).collect(); + println!("预览:\n{}", preview); + } + Err(e) => { + eprintln!("渲染失败: {}", e); + } + } + } + Err(e) => { + eprintln!("❌ 打开失败: {}", e); + } + } +} + +fn search_document(path: &str, query: &str) { + println!("在 {} 中搜索: {}", path, query); + println!("{}", "-".repeat(50)); + + let engine = DocumentEngine::new(); + + let doc = match engine.open(path) { + Ok(d) => d, + Err(e) => { + eprintln!("❌ 打开失败: {}", e); + return; + } + }; + + match engine.search(&doc, query) { + Ok(results) => { + println!("找到 {} 个结果:", results.len()); + println!(); + + for (i, result) in results.iter().take(10).enumerate() { + println!("[{}.] 第 {} 页", i + 1, result.page); + println!(" 上下文: ...{}...", result.context); + println!(); + } + + if results.len() > 10 { + println!("... 还有 {} 个结果", results.len() - 10); + } + } + Err(e) => { + eprintln!("❌ 搜索失败: {}", e); + } + } +} + +fn show_document_info(path: &str) { + println!("文档信息: {}", path); + println!("{}", "-".repeat(50)); + + let engine = DocumentEngine::new(); + + match engine.open(path) { + Ok(doc) => { + println!("标题: {}", doc.title); + println!("路径: {}", doc.path); + println!("格式: {:?}", doc.format); + println!(); + println!("元数据:"); + println!(" 页数: {}", doc.metadata.page_count); + println!(" 文件大小: {} bytes", doc.metadata.file_size); + println!(" 作者: {:?}", doc.metadata.author); + println!(); + + // 获取目录 + match engine.get_toc(&doc) { + Ok(toc) => { + if toc.is_empty() { + println!("目录: (无)"); + } else { + println!("目录:"); + for entry in toc { + let indent = " ".repeat(entry.level); + println!("{}{}. {}", indent, entry.page, entry.title); + } + } + } + Err(e) => { + println!("获取目录失败: {}", e); + } + } + } + Err(e) => { + eprintln!("❌ 获取信息失败: {}", e); + } + } +} \ No newline at end of file