feat: add --review flag with browser-based side-by-side image review
- Launches local HTTP server with dark-themed review UI - Side-by-side image comparison per duplicate group - Checkbox selection + delete confirmation - Shows file size and path per image - Exact/similar badges per group - Shutdown endpoint for clean exit - Magic byte format detection (fixes misnamed screenshots) - 23 tests passing
This commit is contained in:
73
src/main.rs
73
src/main.rs
@@ -2,19 +2,52 @@ use deduper::{find_duplicate_groups, scan_images, DuplicateKind};
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
mod review;
|
||||
|
||||
struct Config {
|
||||
root: String,
|
||||
threshold: u32,
|
||||
review: bool,
|
||||
}
|
||||
|
||||
fn parse_args() -> Option<Config> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() < 2 {
|
||||
eprintln!("usage: deduper <folder> [hamming-threshold]");
|
||||
std::process::exit(1);
|
||||
let mut root = None;
|
||||
let mut threshold = 8u32;
|
||||
let mut review = false;
|
||||
|
||||
let mut i = 1;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
"--review" => review = true,
|
||||
arg if arg.starts_with('-') => {
|
||||
eprintln!("unknown flag: {arg}");
|
||||
return None;
|
||||
}
|
||||
_ => {
|
||||
if root.is_none() {
|
||||
root = Some(args[i].clone());
|
||||
} else {
|
||||
threshold = args[i].parse().unwrap_or(8);
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let root = Path::new(&args[1]);
|
||||
let threshold = args
|
||||
.get(2)
|
||||
.and_then(|s| s.parse::<u32>().ok())
|
||||
.unwrap_or(8);
|
||||
root.map(|r| Config { root: r, threshold, review })
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let config = match parse_args() {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
eprintln!("usage: deduper <folder> [hamming-threshold] [--review]");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let root = Path::new(&config.root);
|
||||
let entries = match scan_images(root) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
@@ -23,20 +56,24 @@ fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
let groups = find_duplicate_groups(&entries, threshold);
|
||||
let groups = find_duplicate_groups(&entries, config.threshold);
|
||||
if groups.is_empty() {
|
||||
println!("no image duplicates found");
|
||||
return;
|
||||
}
|
||||
|
||||
for (idx, group) in groups.iter().enumerate() {
|
||||
let kind = match group.kind {
|
||||
DuplicateKind::Exact => "exact",
|
||||
DuplicateKind::Similar => "similar",
|
||||
};
|
||||
println!("group {} [{}]", idx + 1, kind);
|
||||
for path in &group.paths {
|
||||
println!(" {}", path.display());
|
||||
if config.review {
|
||||
review::launch_review(&groups);
|
||||
} else {
|
||||
for (idx, group) in groups.iter().enumerate() {
|
||||
let kind = match group.kind {
|
||||
DuplicateKind::Exact => "exact",
|
||||
DuplicateKind::Similar => "similar",
|
||||
};
|
||||
println!("group {} [{}]", idx + 1, kind);
|
||||
for path in &group.paths {
|
||||
println!(" {}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user