//! 笔记模块 //! //! 支持阅读笔记、时间统计、导出等功能 use anyhow::Result; use serde::{Deserialize, Serialize}; use chrono::{DateTime, Utc, Duration}; use std::collections::HashMap; /// 笔记类型 #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum NoteType { /// 阅读笔记 Reading, /// 想法/灵感 Idea, /// 问题 Question, /// 总结 Summary, } /// 笔记记录 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Note { /// 唯一标识 pub id: String, /// 关联文档路径 pub document_path: String, /// 笔记类型 pub note_type: NoteType, /// 笔记内容 pub content: String, /// 关联的页码 pub page_number: Option, /// 关联的书签 ID pub bookmark_id: Option, /// 标签 pub tags: Vec, /// 创建时间 pub created_at: DateTime, /// 更新时间 pub updated_at: DateTime, } impl Note { pub fn new( document_path: String, note_type: NoteType, content: String, ) -> Self { let now = Utc::now(); Self { id: uuid::Uuid::new_v4().to_string(), document_path, note_type, content, page_number: None, bookmark_id: None, tags: Vec::new(), created_at: now, updated_at: now, } } pub fn with_tags(mut self, tags: Vec) -> Self { self.tags = tags; self.updated_at = Utc::now(); self } pub fn with_page(mut self, page_number: usize) -> Self { self.page_number = Some(page_number); self.updated_at = Utc::now(); self } pub fn with_bookmark(mut self, bookmark_id: String) -> Self { self.bookmark_id = Some(bookmark_id); self.updated_at = Utc::now(); self } } /// 阅读会话记录 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReadingSession { /// 文档路径 pub document_path: String, /// 开始时间 pub start_time: DateTime, /// 结束时间 pub end_time: Option>, /// 阅读的页码范围 pub page_range: Option<(usize, usize)>, } impl ReadingSession { pub fn start(document_path: String) -> Self { Self { document_path, start_time: Utc::now(), end_time: None, page_range: None, } } pub fn end(mut self, page_range: Option<(usize, usize)>) -> Self { self.end_time = Some(Utc::now()); self.page_range = page_range; self } /// 获取阅读时长(秒) pub fn duration_seconds(&self) -> i64 { let end = self.end_time.unwrap_or(Utc::now()); (end - self.start_time).num_seconds() } } /// 阅读统计 #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ReadingStats { /// 总阅读时长(秒) pub total_seconds: i64, /// 阅读会话数 pub session_count: usize, /// 笔记数量 pub note_count: usize, /// 书签数量 pub bookmark_count: usize, /// 最后阅读时间 pub last_read_at: Option>, } /// 笔记管理器 pub struct NoteManager { db: sled::Db, } impl NoteManager { /// 创建笔记管理器 pub fn new(db_path: &str) -> Result { let db = sled::open(db_path)?; Ok(Self { db }) } /// 添加笔记 pub fn add(&self, note: &Note) -> Result<()> { let key = format!("note:{}", note.id); let value = serde_json::to_vec(note)?; self.db.insert(key, value)?; Ok(()) } /// 删除笔记 pub fn remove(&self, id: &str) -> Result<()> { let key = format!("note:{}", id); self.db.remove(key)?; Ok(()) } /// 更新笔记 pub fn update(&self, note: &Note) -> Result<()> { self.add(note) } /// 获取文档的所有笔记 pub fn get_by_document(&self, document_path: &str) -> Result> { let mut notes = Vec::new(); for item in self.db.scan_prefix("note:") { let (_, value) = item?; let note: Note = serde_json::from_slice(&value)?; if note.document_path == document_path { notes.push(note); } } // 按创建时间排序 notes.sort_by(|a, b| b.created_at.cmp(&a.created_at)); Ok(notes) } /// 按标签搜索笔记 pub fn search_by_tag(&self, tag: &str) -> Result> { let mut notes = Vec::new(); for item in self.db.scan_prefix("note:") { let (_, value) = item?; let note: Note = serde_json::from_slice(&value)?; if note.tags.contains(&tag.to_string()) { notes.push(note); } } Ok(notes) } /// 记录阅读会话 pub fn record_session(&self, session: &ReadingSession) -> Result<()> { let key = format!("session:{}", session.start_time.timestamp()); let value = serde_json::to_vec(session)?; self.db.insert(key, value)?; Ok(()) } /// 获取文档的阅读统计 pub fn get_stats(&self, document_path: &str) -> Result { let mut stats = ReadingStats::default(); // 统计阅读会话 let mut total_seconds = 0i64; let mut session_count = 0usize; let mut last_read_at: Option> = None; for item in self.db.scan_prefix("session:") { let (_, value) = item?; let session: ReadingSession = serde_json::from_slice(&value)?; if session.document_path == document_path { total_seconds += session.duration_seconds(); session_count += 1; let session_end = session.end_time.unwrap_or(session.start_time); if last_read_at.is_none() || session_end > last_read_at.unwrap() { last_read_at = Some(session_end); } } } // 统计笔记 let note_count = self.get_by_document(document_path)?.len(); // 统计书签(从 bookmark tree 获取) let mut bookmark_count = 0usize; for item in self.db.scan_prefix("bookmark:") { let (_, value) = item?; let bookmark: crate::core::bookmark::Bookmark = serde_json::from_slice(&value)?; if bookmark.document_path == document_path { bookmark_count += 1; } } stats.total_seconds = total_seconds; stats.session_count = session_count; stats.note_count = note_count; stats.bookmark_count = bookmark_count; stats.last_read_at = last_read_at; Ok(stats) } /// 导出笔记为 Markdown pub fn export_markdown(&self, document_path: &str) -> Result { let notes = 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"))); // 添加统计信息 let stats = self.get_stats(document_path)?; let hours = stats.total_seconds / 3600; let minutes = (stats.total_seconds % 3600) / 60; md.push_str("## 📊 阅读统计\n\n"); md.push_str(&format!("- 阅读时长:{}小时{}分钟\n", hours, minutes)); md.push_str(&format!("- 阅读会话:{}次\n", stats.session_count)); md.push_str(&format!("- 笔记数量:{}\n", stats.note_count)); md.push_str(&format!("- 书签数量:{}\n\n", stats.bookmark_count)); if let Some(last_read) = stats.last_read_at { md.push_str(&format!("- 最后阅读:{}\n\n", last_read.format("%Y-%m-%d %H:%M:%S"))); } md.push_str("---\n\n"); // 按类型分组 let mut notes_by_type: HashMap> = HashMap::new(); for note in ¬es { let type_key = match note.note_type { NoteType::Reading => "reading", NoteType::Idea => "idea", NoteType::Question => "question", NoteType::Summary => "summary", }; notes_by_type.entry(type_key.to_string()) .or_insert_with(Vec::new) .push(note); } // 输出笔记 for (type_name, type_notes) in notes_by_type { let emoji = match type_name.as_str() { "reading" => "📖", "idea" => "💡", "question" => "❓", "summary" => "📝", _ => "📌", }; md.push_str(&format!("## {} {}\n\n", emoji, type_name.to_uppercase())); for note in type_notes { if let Some(page) = note.page_number { md.push_str(&format!("**页码**: {} \n", page)); } if !note.tags.is_empty() { md.push_str(&format!("**标签**: {} \n", note.tags.join(", "))); } md.push_str(&format!("\n{}\n\n", note.content)); md.push_str(&format!("_创建时间:{}_\n\n", note.created_at.format("%Y-%m-%d %H:%M:%S"))); md.push_str("---\n\n"); } } Ok(md) } /// 导出笔记为 CSV pub fn export_csv(&self, document_path: &str) -> Result { let notes = self.get_by_document(document_path)?; let mut csv = String::new(); csv.push_str("id,type,page,content,tags,created_at,updated_at\n"); for note in notes { let note_type_str = match note.note_type { NoteType::Reading => "reading", NoteType::Idea => "idea", NoteType::Question => "question", NoteType::Summary => "summary", }; let page = note.page_number.map(|p| p.to_string()).unwrap_or_default(); let tags = note.tags.join(","); let content_escaped = note.content.replace('"', "\"\""); csv.push_str(&format!( "{},{},{},\"{}\",\"{}\",{},{}\n", note.id, note_type_str, page, content_escaped, tags, note.created_at.to_rfc3339(), note.updated_at.to_rfc3339() )); } Ok(csv) } /// 导出为 Anki 卡片格式 pub fn export_anki(&self, document_path: &str) -> Result { let notes = self.get_by_document(document_path)?; let mut anki = String::new(); for note in notes { // Anki 导入格式:正面;背面;标签 let front = match note.note_type { NoteType::Question => note.content.clone(), _ => format!("{}: {}", match note.note_type { NoteType::Reading => "阅读笔记", NoteType::Idea => "想法", NoteType::Question => "问题", NoteType::Summary => "总结", }, note.content ), }; let mut back = String::new(); if let Some(page) = note.page_number { back.push_str(&format!("页码:{}\n", page)); } if let Some(bookmark_id) = ¬e.bookmark_id { back.push_str(&format!("关联标注:{}\n", bookmark_id)); } let tags = if note.tags.is_empty() { "readflow".to_string() } else { format!("readflow {}", note.tags.join(" ")) }; anki.push_str(&format!("{};{};{}\n", front, back, tags)); } Ok(anki) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_note_creation() { let note = Note::new( "/path/to/doc.md".to_string(), NoteType::Reading, "这是一条测试笔记".to_string(), ); assert_eq!(note.document_path, "/path/to/doc.md"); assert!(matches!(note.note_type, NoteType::Reading)); } #[test] fn test_session_duration() { let session = ReadingSession::start("/path/to/doc.md".to_string()); // 至少应该有 0 秒 assert!(session.duration_seconds() >= 0); } }