Files
readflow/src/core/progress.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

374 lines
10 KiB
Rust

//! 阅读进度同步模块
//!
//! 支持本地/云端进度同步、多设备同步等功能
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
/// 阅读进度
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadingProgress {
/// 文档路径/ID
pub document_id: String,
/// 当前页码
pub current_page: usize,
/// 总页数
pub total_pages: usize,
/// 进度百分比 (0-100)
pub percentage: f32,
/// 最后阅读位置(字符偏移)
pub position: usize,
/// 最后阅读时间
pub last_read_at: DateTime<Utc>,
/// 设备标识
pub device_id: Option<String>,
/// 是否已同步
pub synced: bool,
}
impl ReadingProgress {
pub fn new(document_id: String, total_pages: usize) -> Self {
Self {
document_id,
current_page: 1,
total_pages,
percentage: 0.0,
position: 0,
last_read_at: Utc::now(),
device_id: None,
synced: false,
}
}
/// 更新进度
pub fn update(&mut self, page: usize, position: usize) {
self.current_page = page;
self.position = position;
self.percentage = if self.total_pages > 0 {
(page as f32 / self.total_pages as f32) * 100.0
} else {
0.0
};
self.last_read_at = Utc::now();
}
/// 标记为已同步
pub fn mark_synced(&mut self) {
self.synced = true;
}
}
/// 进度同步管理器
pub struct ProgressManager {
db: sled::Db,
device_id: String,
}
impl ProgressManager {
/// 创建进度管理器
pub fn new(db_path: &str, device_id: Option<String>) -> Result<Self> {
let db = sled::open(db_path)?;
let device_id = device_id.unwrap_or_else(|| Self::generate_device_id());
Ok(Self { db, device_id })
}
/// 生成设备 ID
fn generate_device_id() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
format!("device_{}", timestamp)
}
/// 获取设备 ID
pub fn get_device_id(&self) -> &str {
&self.device_id
}
/// 保存进度
pub fn save_progress(&self, progress: &ReadingProgress) -> Result<()> {
let key = format!("progress:{}", progress.document_id);
let value = serde_json::to_vec(progress)?;
self.db.insert(key, value)?;
Ok(())
}
/// 获取进度
pub fn get_progress(&self, document_id: &str) -> Result<Option<ReadingProgress>> {
let key = format!("progress:{}", document_id);
match self.db.get(key)? {
Some(value) => {
let progress: ReadingProgress = serde_json::from_slice(&value)?;
Ok(Some(progress))
}
None => Ok(None),
}
}
/// 更新进度
pub fn update_progress(
&self,
document_id: &str,
total_pages: usize,
page: usize,
position: usize,
) -> Result<ReadingProgress> {
let mut progress = match self.get_progress(document_id)? {
Some(p) => p,
None => ReadingProgress::new(document_id.to_string(), total_pages),
};
progress.update(page, position);
progress.device_id = Some(self.device_id.clone());
self.save_progress(&progress)?;
Ok(progress)
}
/// 获取所有进度
pub fn get_all_progress(&self) -> Result<Vec<ReadingProgress>> {
let mut progress_list = Vec::new();
for item in self.db.scan_prefix("progress:") {
let (_, value) = item?;
let progress: ReadingProgress = serde_json::from_slice(&value)?;
progress_list.push(progress);
}
// 按最后阅读时间排序
progress_list.sort_by(|a, b| b.last_read_at.cmp(&a.last_read_at));
Ok(progress_list)
}
/// 获取未同步的进度
pub fn get_unsynced_progress(&self) -> Result<Vec<ReadingProgress>> {
let mut unsynced = Vec::new();
for item in self.db.scan_prefix("progress:") {
let (_, value) = item?;
let mut progress: ReadingProgress = serde_json::from_slice(&value)?;
if !progress.synced {
unsynced.push(progress);
}
}
Ok(unsynced)
}
/// 标记进度为已同步
pub fn mark_as_synced(&self, document_id: &str) -> Result<()> {
if let Some(mut progress) = self.get_progress(document_id)? {
progress.mark_synced();
self.save_progress(&progress)?;
}
Ok(())
}
/// 合并远程进度(解决冲突)
pub fn merge_remote_progress(&self, remote: &ReadingProgress) -> Result<()> {
let local = self.get_progress(&remote.document_id)?;
let should_update = match local {
None => true,
Some(local_progress) => {
// 使用最新的进度
remote.last_read_at > local_progress.last_read_at
}
};
if should_update {
let mut merged = remote.clone();
merged.device_id = Some(self.device_id.clone());
self.save_progress(&merged)?;
}
Ok(())
}
/// 导出进度为 JSON
pub fn export_json(&self) -> Result<String> {
let progress_list = self.get_all_progress()?;
let json = serde_json::to_string_pretty(&progress_list)?;
Ok(json)
}
/// 从 JSON 导入进度
pub fn import_json(&self, json: &str) -> Result<()> {
let progress_list: Vec<ReadingProgress> = serde_json::from_str(json)?;
for progress in progress_list {
self.save_progress(&progress)?;
}
Ok(())
}
/// 删除进度
pub fn remove_progress(&self, document_id: &str) -> Result<()> {
let key = format!("progress:{}", document_id);
self.db.remove(key)?;
Ok(())
}
/// 清除所有进度
pub fn clear_all(&self) -> Result<()> {
let keys: Vec<Vec<u8>> = self.db.scan_prefix("progress:").map(|item| {
item.map(|(k, _)| k.to_vec())
}).collect::<Result<Vec<_>, _>>()?;
for key in keys {
self.db.remove(key)?;
}
Ok(())
}
}
/// 云端同步配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncConfig {
/// 同步服务器 URL
pub server_url: String,
/// API 密钥
pub api_key: Option<String>,
/// 自动同步间隔(秒)
pub auto_sync_interval: u64,
/// 启用自动同步
pub auto_sync_enabled: bool,
}
impl Default for SyncConfig {
fn default() -> Self {
Self {
server_url: String::new(),
api_key: None,
auto_sync_interval: 300, // 5 分钟
auto_sync_enabled: false,
}
}
}
/// 云端同步器
pub struct CloudSync {
config: SyncConfig,
progress_manager: ProgressManager,
}
impl CloudSync {
/// 创建云端同步器
pub fn new(config: SyncConfig, progress_manager: ProgressManager) -> Self {
Self {
config,
progress_manager,
}
}
/// 同步到云端
pub fn sync_to_cloud(&self) -> Result<()> {
if self.config.server_url.is_empty() {
return Ok(()); // 未配置服务器,跳过
}
let unsynced = self.progress_manager.get_unsynced_progress()?;
if unsynced.is_empty() {
return Ok(());
}
let client = reqwest::blocking::Client::new();
for progress in unsynced {
let url = format!("{}/api/v1/progress", self.config.server_url);
let mut request = client
.post(&url)
.json(&progress);
if let Some(api_key) = &self.config.api_key {
request = request.header("Authorization", format!("Bearer {}", api_key));
}
let response = request
.send()
.context("同步请求失败")?;
if response.status().is_success() {
self.progress_manager.mark_as_synced(&progress.document_id)?;
}
}
Ok(())
}
/// 从云端同步
pub fn sync_from_cloud(&self) -> Result<()> {
if self.config.server_url.is_empty() {
return Ok(());
}
let client = reqwest::blocking::Client::new();
let url = format!("{}/api/v1/progress", self.config.server_url);
let mut request = client.get(&url);
if let Some(api_key) = &self.config.api_key {
request = request.header("Authorization", format!("Bearer {}", api_key));
}
let response = request
.send()
.context("获取云端进度失败")?;
if !response.status().is_success() {
return Ok(());
}
let remote_progress_list: Vec<ReadingProgress> = response.json()
.context("解析云端进度失败")?;
for remote in remote_progress_list {
self.progress_manager.merge_remote_progress(&remote)?;
}
Ok(())
}
/// 完全同步(双向)
pub fn sync_all(&self) -> Result<()> {
self.sync_to_cloud()?;
self.sync_from_cloud()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_progress_creation() {
let mut progress = ReadingProgress::new("test_doc".to_string(), 100);
assert_eq!(progress.current_page, 1);
assert_eq!(progress.total_pages, 100);
assert_eq!(progress.percentage, 1.0);
progress.update(50, 1000);
assert_eq!(progress.current_page, 50);
assert!((progress.percentage - 50.0).abs() < 0.1);
}
#[test]
fn test_device_id_generation() {
let id1 = ProgressManager::generate_device_id();
let id2 = ProgressManager::generate_device_id();
// 两次生成的 ID 应该不同(因为时间戳不同)
assert_ne!(id1, id2);
}
}