365 lines
12 KiB
Rust
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(¤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<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(¤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");
|
|
}
|