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:
242
src/core/bookmark.rs
Normal file
242
src/core/bookmark.rs
Normal file
@@ -0,0 +1,242 @@
|
||||
//! 书签与标注模块
|
||||
//!
|
||||
//! 支持高亮、下划线、波浪线、边注等标注类型
|
||||
|
||||
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, "这是一段测试文本");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user