use serde::{Deserialize, Serialize}; use crate::material_type::{MaterialType, PreviewMode}; #[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct DocumentInfo { pub material_id: String, pub title: String, pub material_type: MaterialType, pub preview_mode: PreviewMode, pub file_size: u64, // Text stats pub page_count: Option, pub word_count: Option, pub char_count: Option, pub line_count: Option, // Image pub image_width: Option, pub image_height: Option, pub image_format: Option, // EPUB pub epub_chapter_count: Option, // Capabilities pub is_searchable: bool, pub supports_position: bool, pub supports_anchor: bool, pub created_at: Option, } impl DocumentInfo { pub fn new(material_id: String, title: String, material_type: MaterialType, file_size: u64) -> Self { let preview_mode = material_type.preview_mode(); let (is_searchable, supports_position, supports_anchor) = match material_type { MaterialType::Markdown | MaterialType::Text | MaterialType::Pdf | MaterialType::Epub => (true, true, true), MaterialType::Image => (false, true, true), MaterialType::Word | MaterialType::Excel | MaterialType::PowerPoint => (false, false, true), MaterialType::Unknown => (false, false, false), }; Self { material_id, title, material_type, preview_mode, file_size, page_count: None, word_count: None, char_count: None, line_count: None, image_width: None, image_height: None, image_format: None, epub_chapter_count: None, is_searchable, supports_position, supports_anchor, created_at: None, } } } /// Build a fully populated DocumentInfo from a file path. pub fn build_document_info( file_path: &str, material_id: String, title: String, ) -> Result { use std::path::Path; use crate::material_type::detect_material_type; let path = Path::new(file_path); let mt = detect_material_type(file_path).unwrap_or(MaterialType::Unknown); let file_size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0); let mut info = DocumentInfo::new(material_id, title, mt, file_size); match &info.material_type { MaterialType::Markdown | MaterialType::Text => { if let Ok(content) = std::fs::read_to_string(path) { let stats = crate::text::text_stats(&content); info.word_count = Some(stats.word_count); info.line_count = Some(stats.line_count); info.char_count = Some(content.chars().count() as u32); info.page_count = Some((stats.line_count / 50).max(1)); } } MaterialType::Image => { if let Ok(meta) = crate::image_meta::read_image_meta(file_path) { info.image_width = Some(meta.width); info.image_height = Some(meta.height); info.image_format = Some(meta.format); } } MaterialType::Pdf => { if let Ok(meta) = crate::pdf::read_pdf_metadata(path) { info.page_count = Some(meta.page_count); } } MaterialType::Epub => { if let Ok(meta) = crate::epub::read_epub_metadata(path) { info.epub_chapter_count = Some(meta.chapter_count); } } _ => {} } Ok(info) } #[cfg(test)] mod tests { use super::*; #[test] fn test_markdown_capabilities() { let info = DocumentInfo::new("m1".into(), "test.md".into(), MaterialType::Markdown, 1024); assert!(info.is_searchable); assert!(info.supports_position); assert_eq!(info.preview_mode, PreviewMode::NativeReader); } #[test] fn test_office_capabilities() { let info = DocumentInfo::new("m2".into(), "report.docx".into(), MaterialType::Word, 2048); assert!(!info.is_searchable); assert!(!info.supports_position); assert!(info.supports_anchor); // Material-level anchor only assert_eq!(info.preview_mode, PreviewMode::PlatformPreview); } #[test] fn test_unknown_capabilities() { let info = DocumentInfo::new("m3".into(), "file.bin".into(), MaterialType::Unknown, 0); assert!(!info.is_searchable); assert!(!info.supports_position); assert!(!info.supports_anchor); } #[test] fn test_text_capabilities() { let info = DocumentInfo::new("m4".into(), "notes.txt".into(), MaterialType::Text, 512); assert!(info.is_searchable); assert!(info.supports_position); assert!(info.supports_anchor); assert_eq!(info.preview_mode, PreviewMode::NativeReader); } #[test] fn test_pdf_capabilities() { let info = DocumentInfo::new("m5".into(), "book.pdf".into(), MaterialType::Pdf, 2048); assert!(info.is_searchable); assert!(info.supports_position); assert!(info.supports_anchor); assert_eq!(info.preview_mode, PreviewMode::PlatformPreview); } #[test] fn test_image_capabilities() { let info = DocumentInfo::new("m6".into(), "photo.jpg".into(), MaterialType::Image, 1024); assert!(!info.is_searchable); assert!(info.supports_position); assert!(info.supports_anchor); assert_eq!(info.preview_mode, PreviewMode::NativeReader); } #[test] fn test_epub_capabilities() { let info = DocumentInfo::new("m7".into(), "book.epub".into(), MaterialType::Epub, 4096); assert!(info.is_searchable); assert!(info.supports_position); assert!(info.supports_anchor); assert_eq!(info.preview_mode, PreviewMode::NativeReader); } #[test] fn test_excel_capabilities() { let info = DocumentInfo::new("m8".into(), "sheet.xlsx".into(), MaterialType::Excel, 2048); assert!(!info.is_searchable); assert!(!info.supports_position); assert!(info.supports_anchor); assert_eq!(info.preview_mode, PreviewMode::PlatformPreview); } #[test] fn test_powerpoint_capabilities() { let info = DocumentInfo::new("m9".into(), "slides.pptx".into(), MaterialType::PowerPoint, 4096); assert!(!info.is_searchable); assert!(!info.supports_position); assert!(info.supports_anchor); assert_eq!(info.preview_mode, PreviewMode::ExternalOpen); } #[test] fn test_all_stats_fields_start_none() { let info = DocumentInfo::new("m10".into(), "any".into(), MaterialType::Markdown, 100); assert_eq!(info.page_count, None); assert_eq!(info.word_count, None); assert_eq!(info.char_count, None); assert_eq!(info.line_count, None); assert_eq!(info.image_width, None); assert_eq!(info.image_height, None); assert_eq!(info.image_format, None); assert_eq!(info.epub_chapter_count, None); } #[test] fn test_file_size_is_set() { let info = DocumentInfo::new("m11".into(), "f".into(), MaterialType::Markdown, 12345); assert_eq!(info.file_size, 12345); } #[test] fn test_material_type_matches_input() { let info = DocumentInfo::new("m12".into(), "t".into(), MaterialType::Pdf, 0); assert_eq!(info.material_type, MaterialType::Pdf); } }