diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 723e94e..394fa2a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,32 +1,92 @@ use std::fs; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use std::env; -#[derive(serde::Serialize)] +#[derive(serde::Serialize, Clone)] struct ImageItem { path: String, name: String, + thumbnail: String, +} + +fn get_cache_dir() -> std::path::PathBuf { + let mut path = env::temp_dir(); + path.push("watermark-wizard-thumbs"); + if !path.exists() { + let _ = fs::create_dir_all(&path); + } + path +} + +fn generate_thumbnail(original_path: &Path) -> Option { + let cache_dir = get_cache_dir(); + + // Generate simple hash for filename + let mut hasher = DefaultHasher::new(); + original_path.hash(&mut hasher); + let hash = hasher.finish(); + let file_name = format!("{}.jpg", hash); + let thumb_path = cache_dir.join(file_name); + + // Return if exists + if thumb_path.exists() { + return Some(thumb_path.to_string_lossy().to_string()); + } + + // Generate + // Use image reader to be faster? open is fine. + if let Ok(img) = image::open(original_path) { + // Resize to height 200, preserve ratio + let thumb = img.thumbnail(u32::MAX, 200); + // Save as jpeg with quality 80 + let mut file = fs::File::create(&thumb_path).ok()?; + // thumb.write_to(&mut file, image::ImageOutputFormat::Jpeg(80)).ok()?; + // write_to might be slow due to encoding, but parallel execution helps. + // save directly + thumb.save_with_format(&thumb_path, image::ImageFormat::Jpeg).ok()?; + + return Some(thumb_path.to_string_lossy().to_string()); + } + + None } #[tauri::command] -fn scan_dir(path: String) -> Result, String> { - let mut images = Vec::new(); - let dir = fs::read_dir(&path).map_err(|e| e.to_string())?; - - for entry in dir { +async fn scan_dir(path: String) -> Result, String> { + let entries = fs::read_dir(&path).map_err(|e| e.to_string())?; + + // Collect valid paths first to avoid holding fs locks or iterators during parallel proc + let mut valid_paths = Vec::new(); + for entry in entries { if let Ok(entry) = entry { - let path = entry.path(); - if path.is_file() { - if let Some(ext) = path.extension() { + let p = entry.path(); + if p.is_file() { + if let Some(ext) = p.extension() { let ext_str = ext.to_string_lossy().to_lowercase(); if ["png", "jpg", "jpeg", "webp"].contains(&ext_str.as_str()) { - images.push(ImageItem { - path: path.to_string_lossy().to_string(), - name: path.file_name().unwrap_or_default().to_string_lossy().to_string(), - }); + valid_paths.push(p); } } } } } + + // Process in parallel + let mut images: Vec = valid_paths.par_iter().filter_map(|path| { + let name = path.file_name()?.to_string_lossy().to_string(); + let path_str = path.to_string_lossy().to_string(); + + // Generate thumbnail + let thumb = generate_thumbnail(path).unwrap_or_else(|| path_str.clone()); + + Some(ImageItem { + path: path_str, + name, + thumbnail: thumb, + }) + }).collect(); + // Sort by name images.sort_by(|a, b| a.name.cmp(&b.name)); Ok(images) diff --git a/src/components/HeroView.vue b/src/components/HeroView.vue index 58656d9..5f975ce 100644 --- a/src/components/HeroView.vue +++ b/src/components/HeroView.vue @@ -1,11 +1,41 @@ diff --git a/src/stores/gallery.ts b/src/stores/gallery.ts index edc63ec..5df597a 100644 --- a/src/stores/gallery.ts +++ b/src/stores/gallery.ts @@ -4,7 +4,7 @@ import { invoke } from "@tauri-apps/api/core"; export interface ImageItem { path: string; - thumbnail?: string; + thumbnail: string; // Now mandatory from backend name: string; width?: number; height?: number;