## 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 核心功能全部完成!
243 lines
6.8 KiB
Rust
243 lines
6.8 KiB
Rust
//! 书签与标注模块
|
|
//!
|
|
//! 支持高亮、下划线、波浪线、边注等标注类型
|
|
|
|
use anyhow::Result;
|
|
use serde::{Deserialize, Serialize};
|
|
use chrono::{DateTime, Utc};
|
|
|
|
/// 标注类型
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum HighlightType {
|
|
/// 高亮线
|
|
Highlight,
|
|
/// 下划线
|
|
Underline,
|
|
/// 波浪线
|
|
Wavy,
|
|
/// 边注
|
|
MarginNote,
|
|
}
|
|
|
|
/// 书签/标注记录
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Bookmark {
|
|
/// 唯一标识
|
|
pub id: String,
|
|
/// 文档路径
|
|
pub document_path: String,
|
|
/// 页码
|
|
pub page_number: usize,
|
|
/// 位置偏移
|
|
pub position: usize,
|
|
/// 标注类型
|
|
pub highlight_type: HighlightType,
|
|
/// 标注的原文内容
|
|
pub text: String,
|
|
/// 用户笔记
|
|
pub note: Option<String>,
|
|
/// 颜色 (HEX)
|
|
pub color: Option<String>,
|
|
/// 创建时间
|
|
pub created_at: DateTime<Utc>,
|
|
/// 更新时间
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
impl Bookmark {
|
|
pub fn new(
|
|
document_path: String,
|
|
page_number: usize,
|
|
position: usize,
|
|
highlight_type: HighlightType,
|
|
text: String,
|
|
) -> Self {
|
|
let now = Utc::now();
|
|
Self {
|
|
id: uuid::Uuid::new_v4().to_string(),
|
|
document_path,
|
|
page_number,
|
|
position,
|
|
highlight_type,
|
|
text,
|
|
note: None,
|
|
color: None,
|
|
created_at: now,
|
|
updated_at: now,
|
|
}
|
|
}
|
|
|
|
/// 设置笔记内容
|
|
pub fn with_note(mut self, note: String) -> Self {
|
|
self.note = Some(note);
|
|
self.updated_at = Utc::now();
|
|
self
|
|
}
|
|
|
|
/// 设置颜色
|
|
pub fn with_color(mut self, color: String) -> Self {
|
|
self.color = Some(color);
|
|
self.updated_at = Utc::now();
|
|
self
|
|
}
|
|
}
|
|
|
|
/// 书签管理器
|
|
pub struct BookmarkManager {
|
|
db: sled::Db,
|
|
}
|
|
|
|
impl BookmarkManager {
|
|
/// 创建书签管理器
|
|
pub fn new(db_path: &str) -> Result<Self> {
|
|
let db = sled::open(db_path)?;
|
|
Ok(Self { db })
|
|
}
|
|
|
|
/// 添加书签
|
|
pub fn add(&self, bookmark: &Bookmark) -> Result<()> {
|
|
let key = format!("bookmark:{}", bookmark.id);
|
|
let value = serde_json::to_vec(bookmark)?;
|
|
self.db.insert(key, value)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// 删除书签
|
|
pub fn remove(&self, id: &str) -> Result<()> {
|
|
let key = format!("bookmark:{}", id);
|
|
self.db.remove(key)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// 获取文档的所有书签
|
|
pub fn get_by_document(&self, document_path: &str) -> Result<Vec<Bookmark>> {
|
|
let prefix = format!("bookmark:");
|
|
let mut bookmarks = Vec::new();
|
|
|
|
for item in self.db.scan_prefix(prefix) {
|
|
let (_, value) = item?;
|
|
let bookmark: Bookmark = serde_json::from_slice(&value)?;
|
|
if bookmark.document_path == document_path {
|
|
bookmarks.push(bookmark);
|
|
}
|
|
}
|
|
|
|
// 按页码和位置排序
|
|
bookmarks.sort_by(|a, b| {
|
|
a.page_number.cmp(&b.page_number)
|
|
.then(a.position.cmp(&b.position))
|
|
});
|
|
|
|
Ok(bookmarks)
|
|
}
|
|
|
|
/// 获取所有书签
|
|
pub fn get_all(&self) -> Result<Vec<Bookmark>> {
|
|
let mut bookmarks = Vec::new();
|
|
|
|
for item in self.db.scan_prefix("bookmark:") {
|
|
let (_, value) = item?;
|
|
let bookmark: Bookmark = serde_json::from_slice(&value)?;
|
|
bookmarks.push(bookmark);
|
|
}
|
|
|
|
Ok(bookmarks)
|
|
}
|
|
|
|
/// 导出书签为 Markdown
|
|
pub fn export_markdown(&self, document_path: &str) -> Result<String> {
|
|
let bookmarks = self.get_by_document(document_path)?;
|
|
let mut md = String::new();
|
|
|
|
md.push_str(&format!("# {} 标注导出\n\n", document_path));
|
|
md.push_str(&format!("导出时间:{}\n\n", Utc::now().format("%Y-%m-%d %H:%M:%S")));
|
|
md.push_str(&format!("共 {} 处标注\n\n", bookmarks.len()));
|
|
md.push_str("---\n\n");
|
|
|
|
let mut current_page = 0;
|
|
for bookmark in bookmarks {
|
|
if bookmark.page_number != current_page {
|
|
current_page = bookmark.page_number;
|
|
md.push_str(&format!("## 第 {} 页\n\n", current_page));
|
|
}
|
|
|
|
// 标注类型图标
|
|
let icon = match bookmark.highlight_type {
|
|
HighlightType::Highlight => "🟨",
|
|
HighlightType::Underline => "📏",
|
|
HighlightType::Wavy => "〰️",
|
|
HighlightType::MarginNote => "📝",
|
|
};
|
|
|
|
md.push_str(&format!("{} **{}**\n\n", icon, bookmark.text));
|
|
|
|
if let Some(note) = &bookmark.note {
|
|
md.push_str(&format!("> 💬 {}\n\n", note));
|
|
}
|
|
|
|
md.push_str("---\n\n");
|
|
}
|
|
|
|
Ok(md)
|
|
}
|
|
|
|
/// 导出书签为 CSV
|
|
pub fn export_csv(&self, document_path: &str) -> Result<String> {
|
|
let bookmarks = self.get_by_document(document_path)?;
|
|
let mut csv = String::new();
|
|
|
|
// CSV 头部
|
|
csv.push_str("id,page,position,type,text,note,color,created_at\n");
|
|
|
|
for bookmark in bookmarks {
|
|
let highlight_type_str = match bookmark.highlight_type {
|
|
HighlightType::Highlight => "highlight",
|
|
HighlightType::Underline => "underline",
|
|
HighlightType::Wavy => "wavy",
|
|
HighlightType::MarginNote => "margin_note",
|
|
};
|
|
|
|
let note = bookmark.note.as_deref().unwrap_or("");
|
|
let color = bookmark.color.as_deref().unwrap_or("");
|
|
|
|
// CSV 转义
|
|
let text_escaped = bookmark.text.replace('"', "\"\"");
|
|
let note_escaped = note.replace('"', "\"\"");
|
|
|
|
csv.push_str(&format!(
|
|
"{},{},{},{},\"{}\",\"{}\",\"{}\",{}\n",
|
|
bookmark.id,
|
|
bookmark.page_number,
|
|
bookmark.position,
|
|
highlight_type_str,
|
|
text_escaped,
|
|
note_escaped,
|
|
color,
|
|
bookmark.created_at.to_rfc3339()
|
|
));
|
|
}
|
|
|
|
Ok(csv)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_bookmark_creation() {
|
|
let bookmark = Bookmark::new(
|
|
"/path/to/doc.md".to_string(),
|
|
1,
|
|
100,
|
|
HighlightType::Highlight,
|
|
"这是一段测试文本".to_string(),
|
|
);
|
|
|
|
assert_eq!(bookmark.page_number, 1);
|
|
assert_eq!(bookmark.text, "这是一段测试文本");
|
|
}
|
|
}
|