use crate::safety::QuarantineManager;
use crate::cli;
use inquire::{Confirm, Select, Text};
use std::{thread, time::Duration};
pub fn show_loading_screen() {
let cyan = "\x1b[36m";
let yellow = "\x1b[33m";
let green = "\x1b[32m";
let reset = "\x1b[0m";
println!(
r#"{cyan}
██████╗ ███████╗██████╗ ██████╗ ██████╗ ██████╗ ███████╗
██╔══██╗██╔════╝██╔══██╗██╔════╝██╔═══██╗██╔══██╗██╔════╝
██║ ██║█████╗ ██║ ██║██║ ██║ ██║██████╔╝█████╗
██║ ██║██╔══╝ ██║ ██║██║ ██║ ██║██╔══██╗██╔══╝
██████╔╝███████╗██████╔╝╚██████╗╚██████╔╝██║ ██║███████╗ v 0.1.0
DEDCORE
{reset}"#,
cyan = cyan,
reset = reset
);
println!(
"{yellow}dedcore: Oops, no more duplicates!{reset}\n",
yellow = yellow,
reset = reset
);
print!("{green}Loading: [", green = green);
use std::io::{Write, stdout};
let mut out = stdout();
for _ in 0..20 {
print!("#");
out.flush().unwrap();
thread::sleep(Duration::from_millis(30));
}
println!("]{reset}\n", reset = reset);
thread::sleep(Duration::from_millis(200));
}
pub fn show_quarantine_menu() {
loop {
let options = vec![
"Quarantine a File",
"List Quarantined Files",
"Commit Deletions",
"Rollback All Quarantined Files",
"Back",
];
let choice = Select::new("Quarantine Operations:", options.clone())
.prompt()
.unwrap_or_else(|_| "Back");
if choice == "Quarantine a File" {
let file = Text::new("Enter the path to the file you want to quarantine:")
.prompt()
.unwrap_or_default();
if file.is_empty() {
println!("No file path entered.");
continue;
}
let mut qm = QuarantineManager::new().expect("Failed to create QuarantineManager");
match qm.quarantine_file(&file) {
Ok(_) => println!("File quarantined: {}", file),
Err(e) => println!("Failed to quarantine file: {}: {}", file, e),
}
} else if choice == "List Quarantined Files" {
let qm = QuarantineManager::new().expect("Failed to create QuarantineManager");
let files_ref = qm.list_quarantined_files();
if files_ref.is_empty() {
println!("No files are currently quarantined.");
continue;
}
let files: Vec<_> = files_ref.iter().map(|rec| (*rec).clone()).collect();
let file_options: Vec<String> = files
.iter()
.map(|rec| {
let quarantine_exists = std::path::Path::new(&rec.quarantine_path).exists();
if quarantine_exists {
format!("{} ({} bytes)", rec.original_path, rec.file_size)
} else {
format!("{} (MISSING)", rec.original_path)
}
})
.collect();
let file_choice = Select::new(
"Select a file to manage:",
[&file_options[..], &["Back".to_string()]].concat(),
)
.prompt()
.unwrap_or_else(|_| "Back".to_string());
if file_choice == "Back" {
continue;
}
let idx = file_options.iter().position(|s| s == &file_choice);
if let Some(i) = idx {
let rec = &files[i];
let quarantine_exists = std::path::Path::new(&rec.quarantine_path).exists();
let action = Select::new(
&format!("What would you like to do with {}?", rec.original_path),
vec!["Restore (Rollback)", "Delete Permanently (Commit)", "Back"],
)
.prompt()
.unwrap_or_else(|_| "Back");
if action == "Restore (Rollback)" {
if quarantine_exists {
let quarantine_path = &rec.quarantine_path;
let original_path = &rec.original_path;
if let Some(parent) = std::path::Path::new(original_path).parent() {
let _ = std::fs::create_dir_all(parent);
}
match std::fs::rename(quarantine_path, original_path) {
Ok(_) => println!("Restored {}", original_path),
Err(e) => println!("Failed to restore {}: {}", original_path, e),
}
let mut qm2 =
QuarantineManager::new().expect("Failed to create QuarantineManager");
let _ = qm2.remove_quarantined_file(original_path);
} else {
println!(
"Quarantined file not found: {} (already missing)",
rec.quarantine_path
);
let mut qm2 =
QuarantineManager::new().expect("Failed to create QuarantineManager");
let _ = qm2.remove_quarantined_file(&rec.original_path);
}
} else if action == "Delete Permanently (Commit)" {
if quarantine_exists {
let quarantine_path = &rec.quarantine_path;
match std::fs::remove_file(quarantine_path) {
Ok(_) => println!("Deleted {}", quarantine_path),
Err(e) => println!("Failed to delete {}: {}", quarantine_path, e),
}
let mut qm2 =
QuarantineManager::new().expect("Failed to create QuarantineManager");
let _ = qm2.remove_quarantined_file(&rec.original_path);
} else {
println!(
"Quarantined file not found: {} (already missing)",
rec.quarantine_path
);
let mut qm2 =
QuarantineManager::new().expect("Failed to create QuarantineManager");
let _ = qm2.remove_quarantined_file(&rec.original_path);
}
} else {
continue;
}
}
} else if choice == "Commit Deletions" {
let mut qm = QuarantineManager::new().expect("Failed to create QuarantineManager");
match qm.commit_deletions() {
Ok(count) => println!("{} quarantined files permanently deleted.", count),
Err(e) => println!("Failed to commit deletions: {}", e),
}
} else if choice == "Rollback All Quarantined Files" {
let mut qm = QuarantineManager::new().expect("Failed to create QuarantineManager");
let mut missing = Vec::new();
let mut restored = 0;
for rec in qm
.list_quarantined_files()
.into_iter()
.cloned()
.collect::<Vec<_>>()
{
let quarantine_exists = std::path::Path::new(&rec.quarantine_path).exists();
if quarantine_exists {
let quarantine_path = &rec.quarantine_path;
let original_path = &rec.original_path;
if let Some(parent) = std::path::Path::new(original_path).parent() {
let _ = std::fs::create_dir_all(parent);
}
match std::fs::rename(quarantine_path, original_path) {
Ok(_) => {
let _ = qm.remove_quarantined_file(original_path);
restored += 1;
}
Err(e) => println!("Failed to restore {}: {}", original_path, e),
}
} else {
missing.push(rec.original_path.clone());
let _ = qm.remove_quarantined_file(&rec.original_path);
}
}
println!(
"{} quarantined files restored to their original locations.",
restored
);
if !missing.is_empty() {
println!(
"{} quarantined files were missing and could not be restored:",
missing.len()
);
for m in missing {
println!(" {}", m);
}
}
} else if choice == "Back" {
break;
}
}
}
pub fn show_help_menu() {
println!("\n=== DedCore Help ===");
println!("- Use the arrow keys to navigate menus and Enter to select.");
println!("- Main features:");
println!(" * Scan for Duplicates: Find duplicate files in a directory.");
println!(" * Quarantine Operations: Safely move, delete, or restore files.");
println!(" * Commit Deletions: Permanently delete quarantined files.");
println!(" * Rollback: Restore quarantined files to their original locations.");
println!("- For more info, see the README or project documentation.\n");
let _ = Text::new("Press Enter to return to the main menu...").prompt();
}
pub fn scan_menu() {
let path = select_path();
if path.trim().is_empty() {
println!("No path provided. Aborting scan.");
return;
}
let security = select_security();
let speed = select_speed();
let filetypes =
Text::new("File types to include (comma-separated, e.g. txt,jpg; leave blank for all):")
.with_placeholder("txt,jpg")
.prompt()
.unwrap_or_default();
let min_size = Text::new("Minimum file size in bytes (leave blank for none):")
.with_placeholder("0")
.prompt()
.unwrap_or_default();
let max_size = Text::new("Maximum file size in bytes (leave blank for none):")
.with_placeholder("1000000")
.prompt()
.unwrap_or_default();
let min_age = Text::new("Minimum file age in days (leave blank for none):")
.with_placeholder("0")
.prompt()
.unwrap_or_default();
let max_age = Text::new("Maximum file age in days (leave blank for none):")
.with_placeholder("365")
.prompt()
.unwrap_or_default();
let regex = Text::new("Regex filter for file paths (leave blank for none):")
.with_placeholder(".*backup.*")
.prompt()
.unwrap_or_default();
let dry_run = Confirm::new("Dry run? (Show what would happen, but make no changes)")
.with_default(false)
.prompt()
.unwrap_or(false);
let quarantine_all =
Confirm::new("Quarantine all duplicates (all but one per group) after scan?")
.with_default(false)
.prompt()
.unwrap_or(false);
let similarity_threshold = Text::new(
"Minimum similarity threshold for grouping similar text files (0.0-1.0, default: 0.8):",
)
.with_placeholder("0.8")
.prompt()
.unwrap_or_default();
let image_similarity_threshold = Text::new(
"Minimum similarity threshold for grouping similar image files (0.0-1.0, default: 0.9):",
)
.with_placeholder("0.9")
.prompt()
.unwrap_or_default();
println!("\n=== Scan Summary ===");
println!("Path: {}", path);
println!("Security: {}", security);
println!("Speed: {}", speed);
if !filetypes.is_empty() {
println!("File types: {}", filetypes);
}
if !min_size.is_empty() {
println!("Min size: {} bytes", min_size);
}
if !max_size.is_empty() {
println!("Max size: {} bytes", max_size);
}
if !min_age.is_empty() {
println!("Min age: {} days", min_age);
}
if !max_age.is_empty() {
println!("Max age: {} days", max_age);
}
if !regex.is_empty() {
println!("Regex: {}", regex);
}
println!("Dry run: {}", if dry_run { "yes" } else { "no" });
println!(
"Quarantine all duplicates: {}",
if quarantine_all { "yes" } else { "no" }
);
if !similarity_threshold.is_empty() {
println!("Text similarity threshold: {}", similarity_threshold);
}
if !image_similarity_threshold.is_empty() {
println!("Image similarity threshold: {}", image_similarity_threshold);
}
println!("====================\n");
if !Confirm::new("Proceed with scan?")
.with_default(true)
.prompt()
.unwrap_or(false)
{
println!("Scan cancelled.");
return;
}
let mut args = vec!["dedcore".to_string()];
args.push(path.clone());
args.push(format!("--security={}", security));
args.push(format!("--speed={}", speed));
if !filetypes.is_empty() {
args.push(format!("--filetypes={}", filetypes));
}
if !min_size.is_empty() {
args.push(format!("--min-size={}", min_size));
}
if !max_size.is_empty() {
args.push(format!("--max-size={}", max_size));
}
if !min_age.is_empty() {
args.push(format!("--min-age={}", min_age));
}
if !max_age.is_empty() {
args.push(format!("--max-age={}", max_age));
}
if !regex.is_empty() {
args.push(format!("--regex={}", regex));
}
if dry_run {
args.push("--dry".to_string());
}
if quarantine_all {
args.push("--quarantine-all-dupes".to_string());
}
if !similarity_threshold.trim().is_empty() {
if let Ok(val) = similarity_threshold.trim().parse::<f64>() {
if val >= 0.0 && val <= 1.0 {
args.push(format!("--similarity-threshold={}", val));
} else {
println!(
"Invalid similarity threshold, must be between 0.0 and 1.0. Using default (0.8)."
);
}
} else {
println!("Invalid similarity threshold input. Using default (0.8).");
}
}
if !image_similarity_threshold.trim().is_empty() {
if let Ok(val) = image_similarity_threshold.trim().parse::<f64>() {
if val >= 0.0 && val <= 1.0 {
args.push(format!("--image-similarity-threshold={}", val));
} else {
println!(
"Invalid image similarity threshold, must be between 0.0 and 1.0. Using default (0.9)."
);
}
} else {
println!("Invalid image similarity threshold input. Using default (0.9).");
}
}
cli::run_with_args(args);
}
pub fn main_menu() -> String {
let options = vec![
"Scan for Duplicates",
"Quarantine Operations",
"Help",
"Sponsor Us",
"Exit",
];
loop {
let choice = Select::new("What would you like to do?", options.clone())
.prompt()
.map(|s| s.to_string())
.unwrap_or_else(|_| "Exit".to_string());
if choice == "Sponsor Us" {
show_sponsor_message();
continue;
}
return choice;
}
}
pub fn select_path() -> String {
Text::new("Paste the path to a file or directory:")
.prompt()
.unwrap_or_else(|_| String::new())
}
pub fn select_security() -> String {
let options = vec!["high", "maximum", "medium", "low"];
Select::new("Select security level:", options)
.prompt()
.map(|s| s.to_string())
.unwrap_or_else(|_| "high".to_string())
}
pub fn select_speed() -> String {
let options = vec!["balanced", "fastest", "mostsecure"];
Select::new("Select speed preference:", options)
.prompt()
.map(|s| s.to_string())
.unwrap_or_else(|_| "balanced".to_string())
}
fn show_sponsor_message() {
println!("\n=== Sponsor DedCore ===");
println!("If you like this project, consider sponsoring us!");
println!("GitHub Sponsors: https://github.com/sponsors/yourusername");
println!("Or buy us a coffee: https://buymeacoffee.com/yourusername\n");
let _ = Text::new("Press Enter to return to the main menu...").prompt();
}