Files
readflow/src/core/bookmark.rs
大麦 600f205c87 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 核心功能全部完成!
2026-03-10 14:29:56 +08:00

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, "这是一段测试文本");
}
}