use std::path::Path; use fs_extra::dir::CopyOptions; use tauri::Manager; use serde::{Deserialize, Serialize}; use std::fs; use std::collections::HashMap; use std::collections::HashSet; // Define the configuration structure #[derive(Serialize, Deserialize, Default, Debug)] pub struct AppConfig { pub working_dir: String, pub template_dir: String, } // History Structure #[derive(Serialize, Deserialize, Default, Debug, Clone)] pub struct HistoryMap(HashMap>); // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) } #[tauri::command] fn copy_directory(template_path: String, target_path: String, new_folder_name: String) -> Result { let template = Path::new(&template_path); let target_parent = Path::new(&target_path); let destination = target_parent.join(&new_folder_name); if !template.exists() { return Err("Template directory does not exist".to_string()); } if destination.exists() { return Err(format!("Destination directory already exists: {:?}", destination)); } // Create the destination directory first std::fs::create_dir_all(&destination).map_err(|e| e.to_string())?; let mut options = CopyOptions::new(); options.content_only = true; fs_extra::dir::copy(template, &destination, &options) .map_err(|e| e.to_string())?; Ok(destination.to_string_lossy().to_string()) } #[tauri::command] fn save_config(app_handle: tauri::AppHandle, config: AppConfig) -> Result<(), String> { let config_dir = app_handle.path().app_config_dir().map_err(|e| e.to_string())?; // Ensure the directory exists if !config_dir.exists() { fs::create_dir_all(&config_dir).map_err(|e| e.to_string())?; } let config_path = config_dir.join("config.json"); let json = serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?; fs::write(config_path, json).map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] fn load_config(app_handle: tauri::AppHandle) -> Result { let config_dir = app_handle.path().app_config_dir().map_err(|e| e.to_string())?; let config_path = config_dir.join("config.json"); if !config_path.exists() { return Ok(AppConfig::default()); } let json = fs::read_to_string(config_path).map_err(|e| e.to_string())?; let config: AppConfig = serde_json::from_str(&json).map_err(|e| e.to_string())?; Ok(config) } // --- History Commands --- fn get_history_path(app_handle: &tauri::AppHandle) -> Result { let config_dir = app_handle.path().app_config_dir().map_err(|e| e.to_string())?; if !config_dir.exists() { fs::create_dir_all(&config_dir).map_err(|e| e.to_string())?; } Ok(config_dir.join("history.json")) } #[tauri::command] fn load_history(app_handle: tauri::AppHandle) -> Result { let path = get_history_path(&app_handle)?; if !path.exists() { return Ok(HistoryMap::default()); } let json = fs::read_to_string(path).map_err(|e| e.to_string())?; let history: HistoryMap = serde_json::from_str(&json).map_err(|e| e.to_string())?; Ok(history) } #[tauri::command] fn save_history_item(app_handle: tauri::AppHandle, key: String, value: String) -> Result { let path = get_history_path(&app_handle)?; let mut history = if path.exists() { let json = fs::read_to_string(&path).map_err(|e| e.to_string())?; serde_json::from_str(&json).unwrap_or_default() } else { HistoryMap::default() }; let list = history.0.entry(key).or_insert_with(Vec::new); if !list.contains(&value) { list.push(value); } let json = serde_json::to_string_pretty(&history).map_err(|e| e.to_string())?; fs::write(path, json).map_err(|e| e.to_string())?; Ok(history) } #[tauri::command] fn remove_history_item(app_handle: tauri::AppHandle, key: String, value: String) -> Result { let path = get_history_path(&app_handle)?; if !path.exists() { return Ok(HistoryMap::default()); } let json = fs::read_to_string(&path).map_err(|e| e.to_string())?; let mut history: HistoryMap = serde_json::from_str(&json).map_err(|e| e.to_string())?; if let Some(list) = history.0.get_mut(&key) { list.retain(|x| x != &value); if list.is_empty() { history.0.remove(&key); } } let json = serde_json::to_string_pretty(&history).map_err(|e| e.to_string())?; fs::write(path, json).map_err(|e| e.to_string())?; Ok(history) } #[tauri::command] fn check_dir_exists(path: String) -> bool { Path::new(&path).exists() } // --- Renaming Logic --- #[tauri::command] fn rename_videos(files: Vec, prefix: String, base_name: String) -> Result { if files.is_empty() { return Err("No files provided".to_string()); } // 1. Group files by parent directory to ensure index uniqueness per directory let mut files_by_dir: HashMap> = HashMap::new(); for file_str in files { let path = std::path::PathBuf::from(file_str); if path.exists() && path.is_file() { if let Some(parent) = path.parent() { files_by_dir.entry(parent.to_path_buf()).or_default().push(path); } } } let mut renamed_count = 0; // 2. Process each directory for (dir, file_list) in files_by_dir { // Find existing indices let mut occupied_indices = HashSet::new(); let read_dir = fs::read_dir(&dir).map_err(|e| e.to_string())?; for entry in read_dir { let entry = entry.map_err(|e| e.to_string())?; let path = entry.path(); if path.is_file() { if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { // Check if file matches Pattern: Prefix + base_name + XX + .ext // e.g., "AI-20231207-" + "Text" + "01" + ".mp4" let name_without_ext = Path::new(file_name).file_stem().and_then(|s| s.to_str()).unwrap_or(""); let prefix_base = format!("{}{}", prefix, base_name); if name_without_ext.starts_with(&prefix_base) { let suffix = &name_without_ext[prefix_base.len()..]; // suffix should be digits (e.g., "01", "02") if let Ok(index) = suffix.parse::() { occupied_indices.insert(index); } } } } } // Rename files in this directory let mut current_index = 1; for file_path in file_list { // Check if the file already matches the desired format if let Some(file_name) = file_path.file_name().and_then(|n| n.to_str()) { let name_without_ext = Path::new(file_name).file_stem().and_then(|s| s.to_str()).unwrap_or(""); let prefix_base = format!("{}{}", prefix, base_name); if name_without_ext.starts_with(&prefix_base) { let suffix = &name_without_ext[prefix_base.len()..]; // If it ends with 2 digits, it's already named correctly if suffix.len() == 2 && suffix.chars().all(|c| c.is_digit(10)) { continue; // Skip this file } } } // Find next available index while occupied_indices.contains(¤t_index) { current_index += 1; } let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or(""); let new_name = if ext.is_empty() { format!("{}{}{:02}", prefix, base_name, current_index) } else { format!("{}{}{:02}.{}", prefix, base_name, current_index, ext) }; let new_path = dir.join(new_name); // Should not happen due to index check, but safety first if !new_path.exists() { fs::rename(&file_path, &new_path).map_err(|e| e.to_string())?; occupied_indices.insert(current_index); renamed_count += 1; } else { // If it exists, skip to next index (rare race condition or manually named weirdly) current_index += 1; // Retry logic could go here, but for now let's just try next loop or fail this file // For robustness, let's just skip this file to avoid overwrite } } } Ok(format!("Successfully renamed {} files.", renamed_count)) } // --- Check Logic --- fn remove_empty_dirs_recursive(path: &Path, deleted_list: &mut Vec) -> std::io::Result<()> { if path.is_dir() { let read_dir = fs::read_dir(path)?; for entry in read_dir { let entry = entry?; let child_path = entry.path(); if child_path.is_dir() { remove_empty_dirs_recursive(&child_path, deleted_list)?; } } // Re-check if empty after processing children // Use read_dir again to check if it's empty now let mut is_empty = true; let read_dir_check = fs::read_dir(path)?; for _ in read_dir_check { is_empty = false; break; } if is_empty { fs::remove_dir(path)?; deleted_list.push(path.to_string_lossy().to_string()); } } Ok(()) } #[tauri::command] fn delete_empty_dirs(path: String) -> Result, String> { let root_path = Path::new(&path); if !root_path.exists() || !root_path.is_dir() { return Err("Path is not a valid directory".to_string()); } let mut deleted = Vec::new(); // We cannot delete the root path itself even if empty, per logic usually expected in tools. // So we iterate children and call recursive function. let read_dir = fs::read_dir(root_path).map_err(|e| e.to_string())?; for entry in read_dir { let entry = entry.map_err(|e| e.to_string())?; let child_path = entry.path(); if child_path.is_dir() { remove_empty_dirs_recursive(&child_path, &mut deleted).map_err(|e| e.to_string())?; } } Ok(deleted) } #[tauri::command] fn check_file_naming(path: String, prefix: String) -> Result, String> { let root_path = Path::new(&path); if !root_path.exists() || !root_path.is_dir() { return Err("Path is not a valid directory".to_string()); } let mut mismatches = Vec::new(); let mut stack = vec![root_path.to_path_buf()]; while let Some(current_dir) = stack.pop() { let read_dir = fs::read_dir(¤t_dir).map_err(|e| e.to_string())?; for entry in read_dir { let entry = entry.map_err(|e| e.to_string())?; let path = entry.path(); if path.is_dir() { stack.push(path); } else { if let Some(name) = path.file_name().and_then(|n| n.to_str()) { // Check against prefix // NOTE: Should we check hidden files? Assuming ignoring hidden files for now if starting with dot? // Or just check everything. Let's check everything visible. if !name.starts_with(".") { if !name.starts_with(&prefix) { mismatches.push(path.to_string_lossy().to_string()); } } } } } } Ok(mismatches) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) .invoke_handler(tauri::generate_handler![ greet, copy_directory, save_config, load_config, load_history, save_history_item, remove_history_item, check_dir_exists, rename_videos, delete_empty_dirs, check_file_naming ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }