## 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 核心功能全部完成!
374 lines
10 KiB
Rust
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);
|
|
}
|
|
}
|