Files
review-videos/src-tauri/src/lib.rs
Julian Freeman f42ee4ec9e check page
2025-12-07 20:27:00 -04:00

365 lines
12 KiB
Rust

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<String, Vec<String>>);
// 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<String, String> {
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<AppConfig, String> {
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<std::path::PathBuf, String> {
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<HistoryMap, String> {
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<HistoryMap, String> {
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<HistoryMap, String> {
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<String>, prefix: String, base_name: String) -> Result<String, String> {
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<std::path::PathBuf, Vec<std::path::PathBuf>> = 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::<u32>() {
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(&current_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<String>) -> 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<Vec<String>, 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<Vec<String>, 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(&current_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");
}