Add real pause, desktop notifications, undo, accessibility, CLI watch
This commit is contained in:
220
Cargo.lock
generated
220
Cargo.lock
generated
@@ -51,6 +51,15 @@ dependencies = [
|
|||||||
"equator",
|
"equator",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.21"
|
version = "0.6.21"
|
||||||
@@ -340,6 +349,17 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"num-traits",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clang-sys"
|
name = "clang-sys"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
@@ -404,6 +424,12 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core2"
|
name = "core2"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -1091,6 +1117,30 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.65"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core 0.62.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "id-arena"
|
name = "id-arena"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -1718,6 +1768,31 @@ dependencies = [
|
|||||||
"libm",
|
"libm",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
||||||
|
dependencies = [
|
||||||
|
"objc2-encode",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-encode"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-foundation"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
"objc2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
@@ -1808,6 +1883,12 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "percent-encoding"
|
||||||
|
version = "2.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -1819,7 +1900,10 @@ name = "pixstrip-cli"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
|
"dirs",
|
||||||
"pixstrip-core",
|
"pixstrip-core",
|
||||||
|
"serde_json",
|
||||||
|
"trash",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2238,6 +2322,12 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.27"
|
version = "1.0.27"
|
||||||
@@ -2507,6 +2597,24 @@ version = "1.0.6+spec-1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trash"
|
||||||
|
version = "5.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a9b93a14fcf658568eb11b3ac4cb406822e916e2c55cdebc421beeb0bd7c94d8"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"objc2",
|
||||||
|
"objc2-foundation",
|
||||||
|
"once_cell",
|
||||||
|
"percent-encoding",
|
||||||
|
"scopeguard",
|
||||||
|
"urlencoding",
|
||||||
|
"windows",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ttf-parser"
|
name = "ttf-parser"
|
||||||
version = "0.25.1"
|
version = "0.25.1"
|
||||||
@@ -2531,6 +2639,12 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -2702,12 +2816,118 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.56.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.56.0",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.56.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement 0.56.0",
|
||||||
|
"windows-interface 0.56.0",
|
||||||
|
"windows-result 0.1.2",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement 0.60.2",
|
||||||
|
"windows-interface 0.59.3",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result 0.4.1",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.56.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.56.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|||||||
@@ -7,3 +7,6 @@ license.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
pixstrip-core = { workspace = true }
|
pixstrip-core = { workspace = true }
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
trash = "5"
|
||||||
|
dirs = "6"
|
||||||
|
serde_json = "1"
|
||||||
|
|||||||
@@ -93,11 +93,45 @@ enum Commands {
|
|||||||
action: PresetAction,
|
action: PresetAction,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Manage watch folders
|
||||||
|
Watch {
|
||||||
|
#[command(subcommand)]
|
||||||
|
action: WatchAction,
|
||||||
|
},
|
||||||
|
|
||||||
/// View processing history
|
/// View processing history
|
||||||
History,
|
History,
|
||||||
|
|
||||||
/// Undo last batch operation
|
/// Undo last batch operation (moves output files to trash)
|
||||||
Undo,
|
Undo {
|
||||||
|
/// Undo the last N batches (default 1)
|
||||||
|
#[arg(long, default_value = "1")]
|
||||||
|
last: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum WatchAction {
|
||||||
|
/// Add a watch folder with a linked preset
|
||||||
|
Add {
|
||||||
|
/// Folder path to watch
|
||||||
|
path: String,
|
||||||
|
/// Preset name to apply
|
||||||
|
#[arg(long)]
|
||||||
|
preset: String,
|
||||||
|
/// Watch subdirectories recursively
|
||||||
|
#[arg(short, long)]
|
||||||
|
recursive: bool,
|
||||||
|
},
|
||||||
|
/// List configured watch folders
|
||||||
|
List,
|
||||||
|
/// Remove a watch folder
|
||||||
|
Remove {
|
||||||
|
/// Folder path to remove
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
|
/// Start watching configured folders (blocks until Ctrl+C)
|
||||||
|
Start,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
@@ -160,8 +194,18 @@ fn main() {
|
|||||||
PresetAction::Export { name, output } => cmd_preset_export(&name, &output),
|
PresetAction::Export { name, output } => cmd_preset_export(&name, &output),
|
||||||
PresetAction::Import { path } => cmd_preset_import(&path),
|
PresetAction::Import { path } => cmd_preset_import(&path),
|
||||||
},
|
},
|
||||||
|
Commands::Watch { action } => match action {
|
||||||
|
WatchAction::Add {
|
||||||
|
path,
|
||||||
|
preset,
|
||||||
|
recursive,
|
||||||
|
} => cmd_watch_add(&path, &preset, recursive),
|
||||||
|
WatchAction::List => cmd_watch_list(),
|
||||||
|
WatchAction::Remove { path } => cmd_watch_remove(&path),
|
||||||
|
WatchAction::Start => cmd_watch_start(),
|
||||||
|
},
|
||||||
Commands::History => cmd_history(),
|
Commands::History => cmd_history(),
|
||||||
Commands::Undo => cmd_undo(),
|
Commands::Undo { last } => cmd_undo(last),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,17 +464,49 @@ fn cmd_history() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_undo() {
|
fn cmd_undo(count: usize) {
|
||||||
let history = HistoryStore::new();
|
let history = HistoryStore::new();
|
||||||
match history.list() {
|
match history.list() {
|
||||||
Ok(entries) => {
|
Ok(entries) => {
|
||||||
if let Some(last) = entries.last() {
|
if entries.is_empty() {
|
||||||
println!("Last operation: {} images from {}", last.total, last.input_dir);
|
|
||||||
println!("Output files would be moved to trash.");
|
|
||||||
println!("(Undo with file deletion not yet implemented - requires gio trash support)");
|
|
||||||
} else {
|
|
||||||
println!("No processing history to undo.");
|
println!("No processing history to undo.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let to_undo = entries.iter().rev().take(count);
|
||||||
|
let mut total_trashed = 0;
|
||||||
|
|
||||||
|
for entry in to_undo {
|
||||||
|
if entry.output_files.is_empty() {
|
||||||
|
println!(
|
||||||
|
"Batch from {} has no recorded output files - cannot undo",
|
||||||
|
entry.timestamp
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Undoing batch: {} images from {}",
|
||||||
|
entry.total, entry.input_dir
|
||||||
|
);
|
||||||
|
|
||||||
|
for file_path in &entry.output_files {
|
||||||
|
let path = PathBuf::from(file_path);
|
||||||
|
if path.exists() {
|
||||||
|
// Move to OS trash using the trash crate
|
||||||
|
match trash::delete(&path) {
|
||||||
|
Ok(()) => {
|
||||||
|
total_trashed += 1;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(" Failed to trash {}: {}", path.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{} files moved to trash", total_trashed);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to read history: {}", e);
|
eprintln!("Failed to read history: {}", e);
|
||||||
@@ -439,6 +515,183 @@ fn cmd_undo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cmd_watch_add(path: &str, preset_name: &str, recursive: bool) {
|
||||||
|
// Verify the preset exists
|
||||||
|
let _preset = find_preset(preset_name);
|
||||||
|
let watch_path = PathBuf::from(path);
|
||||||
|
if !watch_path.exists() {
|
||||||
|
eprintln!("Watch folder does not exist: {}", path);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save watch folder config
|
||||||
|
let watch = pixstrip_core::watcher::WatchFolder {
|
||||||
|
path: watch_path,
|
||||||
|
preset_name: preset_name.to_string(),
|
||||||
|
recursive,
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store in config
|
||||||
|
let config_dir = dirs::config_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("~/.config"))
|
||||||
|
.join("pixstrip");
|
||||||
|
let watches_path = config_dir.join("watches.json");
|
||||||
|
let mut watches: Vec<pixstrip_core::watcher::WatchFolder> = if watches_path.exists() {
|
||||||
|
std::fs::read_to_string(&watches_path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| serde_json::from_str(&s).ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Don't add duplicate paths
|
||||||
|
if watches.iter().any(|w| w.path == watch.path) {
|
||||||
|
println!("Watch folder already configured: {}", path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
watches.push(watch);
|
||||||
|
let _ = std::fs::create_dir_all(&config_dir);
|
||||||
|
let _ = std::fs::write(&watches_path, serde_json::to_string_pretty(&watches).unwrap());
|
||||||
|
|
||||||
|
println!("Added watch: {} -> preset '{}'", path, preset_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_watch_list() {
|
||||||
|
let config_dir = dirs::config_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("~/.config"))
|
||||||
|
.join("pixstrip");
|
||||||
|
let watches_path = config_dir.join("watches.json");
|
||||||
|
|
||||||
|
let watches: Vec<pixstrip_core::watcher::WatchFolder> = if watches_path.exists() {
|
||||||
|
std::fs::read_to_string(&watches_path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| serde_json::from_str(&s).ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
if watches.is_empty() {
|
||||||
|
println!("No watch folders configured.");
|
||||||
|
println!("Use 'pixstrip watch add <path> --preset <name>' to add one.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Configured watch folders:");
|
||||||
|
for watch in &watches {
|
||||||
|
let recursive_str = if watch.recursive { " (recursive)" } else { "" };
|
||||||
|
let status = if watch.active { "active" } else { "inactive" };
|
||||||
|
println!(
|
||||||
|
" {} -> '{}' [{}]{}",
|
||||||
|
watch.path.display(),
|
||||||
|
watch.preset_name,
|
||||||
|
status,
|
||||||
|
recursive_str
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_watch_remove(path: &str) {
|
||||||
|
let config_dir = dirs::config_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("~/.config"))
|
||||||
|
.join("pixstrip");
|
||||||
|
let watches_path = config_dir.join("watches.json");
|
||||||
|
|
||||||
|
let mut watches: Vec<pixstrip_core::watcher::WatchFolder> = if watches_path.exists() {
|
||||||
|
std::fs::read_to_string(&watches_path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| serde_json::from_str(&s).ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let original_len = watches.len();
|
||||||
|
let target = PathBuf::from(path);
|
||||||
|
watches.retain(|w| w.path != target);
|
||||||
|
|
||||||
|
if watches.len() == original_len {
|
||||||
|
println!("Watch folder not found: {}", path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = std::fs::write(&watches_path, serde_json::to_string_pretty(&watches).unwrap());
|
||||||
|
println!("Removed watch folder: {}", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_watch_start() {
|
||||||
|
let config_dir = dirs::config_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("~/.config"))
|
||||||
|
.join("pixstrip");
|
||||||
|
let watches_path = config_dir.join("watches.json");
|
||||||
|
|
||||||
|
let watches: Vec<pixstrip_core::watcher::WatchFolder> = if watches_path.exists() {
|
||||||
|
std::fs::read_to_string(&watches_path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| serde_json::from_str(&s).ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let active: Vec<_> = watches.iter().filter(|w| w.active).collect();
|
||||||
|
if active.is_empty() {
|
||||||
|
println!("No active watch folders. Use 'pixstrip watch add' first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Starting watch on {} folder(s)...", active.len());
|
||||||
|
for w in &active {
|
||||||
|
println!(" {} -> '{}'", w.path.display(), w.preset_name);
|
||||||
|
}
|
||||||
|
println!("Press Ctrl+C to stop.");
|
||||||
|
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
let mut watchers = Vec::new();
|
||||||
|
|
||||||
|
for watch in &active {
|
||||||
|
let watcher = pixstrip_core::watcher::FolderWatcher::new();
|
||||||
|
if let Err(e) = watcher.start(watch, tx.clone()) {
|
||||||
|
eprintln!("Failed to start watching {}: {}", watch.path.display(), e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
watchers.push((watcher, watch.preset_name.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process incoming files
|
||||||
|
for event in &rx {
|
||||||
|
match event {
|
||||||
|
pixstrip_core::watcher::WatchEvent::NewImage(path) => {
|
||||||
|
println!("New image: {}", path.display());
|
||||||
|
// Find which watcher this came from and use its preset
|
||||||
|
if let Some((_, preset_name)) = watchers.first() {
|
||||||
|
let preset = find_preset(preset_name);
|
||||||
|
let input_dir = path.parent().unwrap_or_else(|| std::path::Path::new(".")).to_path_buf();
|
||||||
|
let output_dir = input_dir.join("processed");
|
||||||
|
let mut job = preset.to_job(&input_dir, &output_dir);
|
||||||
|
job.add_source(&path);
|
||||||
|
|
||||||
|
let executor = PipelineExecutor::new();
|
||||||
|
match executor.execute(&job, |_| {}) {
|
||||||
|
Ok(r) => println!(" Processed: {} -> {}", format_bytes(r.total_input_bytes), format_bytes(r.total_output_bytes)),
|
||||||
|
Err(e) => eprintln!(" Failed: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pixstrip_core::watcher::WatchEvent::Error(err) => {
|
||||||
|
eprintln!("Watch error: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (w, _) in &watchers {
|
||||||
|
w.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Helpers ---
|
// --- Helpers ---
|
||||||
|
|
||||||
fn find_preset(name: &str) -> Preset {
|
fn find_preset(name: &str) -> Preset {
|
||||||
|
|||||||
@@ -32,17 +32,32 @@ pub struct BatchResult {
|
|||||||
|
|
||||||
pub struct PipelineExecutor {
|
pub struct PipelineExecutor {
|
||||||
cancel_flag: Arc<AtomicBool>,
|
cancel_flag: Arc<AtomicBool>,
|
||||||
|
pause_flag: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PipelineExecutor {
|
impl PipelineExecutor {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||||
|
pause_flag: Arc::new(AtomicBool::new(false)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_cancel(cancel_flag: Arc<AtomicBool>) -> Self {
|
pub fn with_cancel(cancel_flag: Arc<AtomicBool>) -> Self {
|
||||||
Self { cancel_flag }
|
Self {
|
||||||
|
cancel_flag,
|
||||||
|
pause_flag: Arc::new(AtomicBool::new(false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_cancel_and_pause(
|
||||||
|
cancel_flag: Arc<AtomicBool>,
|
||||||
|
pause_flag: Arc<AtomicBool>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
cancel_flag,
|
||||||
|
pause_flag,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute<F>(&self, job: &ProcessingJob, mut on_progress: F) -> Result<BatchResult>
|
pub fn execute<F>(&self, job: &ProcessingJob, mut on_progress: F) -> Result<BatchResult>
|
||||||
@@ -71,6 +86,18 @@ impl PipelineExecutor {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait while paused (check every 100ms)
|
||||||
|
while self.pause_flag.load(Ordering::Relaxed) {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
if self.cancel_flag.load(Ordering::Relaxed) {
|
||||||
|
result.cancelled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result.cancelled {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
let file_name = source
|
let file_name = source
|
||||||
.path
|
.path
|
||||||
.file_name()
|
.file_name()
|
||||||
|
|||||||
@@ -289,6 +289,9 @@ fn build_ui(app: &adw::Application) {
|
|||||||
);
|
);
|
||||||
ui.step_indicator.set_current(0);
|
ui.step_indicator.set_current(0);
|
||||||
|
|
||||||
|
// Apply saved accessibility settings
|
||||||
|
apply_accessibility_settings();
|
||||||
|
|
||||||
window.present();
|
window.present();
|
||||||
|
|
||||||
crate::welcome::show_welcome_if_first_launch(&window);
|
crate::welcome::show_welcome_if_first_launch(&window);
|
||||||
@@ -740,9 +743,49 @@ fn show_history_dialog(window: &adw::ApplicationWindow) {
|
|||||||
.subtitle(&subtitle)
|
.subtitle(&subtitle)
|
||||||
.build();
|
.build();
|
||||||
row.add_prefix(>k::Image::from_icon_name("image-x-generic-symbolic"));
|
row.add_prefix(>k::Image::from_icon_name("image-x-generic-symbolic"));
|
||||||
|
|
||||||
|
// Undo button - moves output files to trash
|
||||||
|
if !entry.output_files.is_empty() {
|
||||||
|
let undo_btn = gtk::Button::builder()
|
||||||
|
.icon_name("edit-undo-symbolic")
|
||||||
|
.tooltip_text("Undo - move outputs to trash")
|
||||||
|
.valign(gtk::Align::Center)
|
||||||
|
.build();
|
||||||
|
undo_btn.add_css_class("flat");
|
||||||
|
let files = entry.output_files.clone();
|
||||||
|
undo_btn.connect_clicked(move |btn| {
|
||||||
|
let mut trashed = 0;
|
||||||
|
for file_path in &files {
|
||||||
|
let gfile = gtk::gio::File::for_path(file_path);
|
||||||
|
if gfile.trash(gtk::gio::Cancellable::NONE).is_ok() {
|
||||||
|
trashed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
btn.set_sensitive(false);
|
||||||
|
btn.set_tooltip_text(Some(&format!("{} files moved to trash", trashed)));
|
||||||
|
});
|
||||||
|
row.add_suffix(&undo_btn);
|
||||||
|
}
|
||||||
|
|
||||||
group.add(&row);
|
group.add(&row);
|
||||||
}
|
}
|
||||||
content.append(&group);
|
content.append(&group);
|
||||||
|
|
||||||
|
// Clear history button
|
||||||
|
let clear_btn = gtk::Button::builder()
|
||||||
|
.label("Clear History")
|
||||||
|
.halign(gtk::Align::Center)
|
||||||
|
.margin_top(12)
|
||||||
|
.margin_bottom(12)
|
||||||
|
.build();
|
||||||
|
clear_btn.add_css_class("destructive-action");
|
||||||
|
clear_btn.connect_clicked(move |btn| {
|
||||||
|
let history = pixstrip_core::storage::HistoryStore::new();
|
||||||
|
let _ = history.clear();
|
||||||
|
btn.set_label("History Cleared");
|
||||||
|
btn.set_sensitive(false);
|
||||||
|
});
|
||||||
|
content.append(&clear_btn);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let error = adw::StatusPage::builder()
|
let error = adw::StatusPage::builder()
|
||||||
@@ -906,16 +949,19 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
|
|||||||
let progress_bar = find_widget_by_type::<gtk::ProgressBar>(&processing_page);
|
let progress_bar = find_widget_by_type::<gtk::ProgressBar>(&processing_page);
|
||||||
let cancel_flag = Arc::new(AtomicBool::new(false));
|
let cancel_flag = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
|
let pause_flag = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
// Find cancel button and wire it; also wire pause button
|
// Find cancel button and wire it; also wire pause button
|
||||||
wire_cancel_button(&processing_page, cancel_flag.clone());
|
wire_cancel_button(&processing_page, cancel_flag.clone());
|
||||||
wire_pause_button(&processing_page);
|
wire_pause_button(&processing_page, pause_flag.clone());
|
||||||
|
|
||||||
// Run processing in a background thread
|
// Run processing in a background thread
|
||||||
let (tx, rx) = std::sync::mpsc::channel::<ProcessingMessage>();
|
let (tx, rx) = std::sync::mpsc::channel::<ProcessingMessage>();
|
||||||
|
|
||||||
let cancel = cancel_flag.clone();
|
let cancel = cancel_flag.clone();
|
||||||
|
let pause = pause_flag.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let executor = pixstrip_core::executor::PipelineExecutor::with_cancel(cancel);
|
let executor = pixstrip_core::executor::PipelineExecutor::with_cancel_and_pause(cancel, pause);
|
||||||
let result = executor.execute(&job, |update| {
|
let result = executor.execute(&job, |update| {
|
||||||
let _ = tx.send(ProcessingMessage::Progress {
|
let _ = tx.send(ProcessingMessage::Progress {
|
||||||
current: update.current,
|
current: update.current,
|
||||||
@@ -998,8 +1044,30 @@ fn show_results(
|
|||||||
ui.next_button.set_label("Process More");
|
ui.next_button.set_label("Process More");
|
||||||
ui.next_button.set_visible(true);
|
ui.next_button.set_visible(true);
|
||||||
|
|
||||||
// Save history
|
// Save history with output paths for undo support
|
||||||
let history = pixstrip_core::storage::HistoryStore::new();
|
let history = pixstrip_core::storage::HistoryStore::new();
|
||||||
|
let output_dir_str = ui.state.output_dir.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.display().to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let input_dir_str = ui.state.loaded_files.borrow()
|
||||||
|
.first()
|
||||||
|
.and_then(|p| p.parent())
|
||||||
|
.map(|p| p.display().to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Collect actual output files from the output directory
|
||||||
|
let output_files: Vec<String> = if let Some(ref dir) = *ui.state.output_dir.borrow() {
|
||||||
|
std::fs::read_dir(dir)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.map(|e| e.path().display().to_string())
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
let _ = history.add(pixstrip_core::storage::HistoryEntry {
|
let _ = history.add(pixstrip_core::storage::HistoryEntry {
|
||||||
timestamp: format!(
|
timestamp: format!(
|
||||||
"{}",
|
"{}",
|
||||||
@@ -1008,8 +1076,8 @@ fn show_results(
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.as_secs()
|
.as_secs()
|
||||||
),
|
),
|
||||||
input_dir: String::new(),
|
input_dir: input_dir_str,
|
||||||
output_dir: String::new(),
|
output_dir: output_dir_str,
|
||||||
preset_name: None,
|
preset_name: None,
|
||||||
total: result.total,
|
total: result.total,
|
||||||
succeeded: result.succeeded,
|
succeeded: result.succeeded,
|
||||||
@@ -1017,7 +1085,7 @@ fn show_results(
|
|||||||
total_input_bytes: result.total_input_bytes,
|
total_input_bytes: result.total_input_bytes,
|
||||||
total_output_bytes: result.total_output_bytes,
|
total_output_bytes: result.total_output_bytes,
|
||||||
elapsed_ms: result.elapsed_ms,
|
elapsed_ms: result.elapsed_ms,
|
||||||
output_files: vec![],
|
output_files,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show toast
|
// Show toast
|
||||||
@@ -1033,7 +1101,30 @@ fn show_results(
|
|||||||
};
|
};
|
||||||
let toast = adw::Toast::new(&savings);
|
let toast = adw::Toast::new(&savings);
|
||||||
toast.set_timeout(5);
|
toast.set_timeout(5);
|
||||||
ui.toast_overlay.add_toast(toast);
|
ui.toast_overlay.add_toast(toast.clone());
|
||||||
|
|
||||||
|
// Desktop notification (if enabled in settings)
|
||||||
|
let config_store = pixstrip_core::storage::ConfigStore::new();
|
||||||
|
let config = config_store.load().unwrap_or_default();
|
||||||
|
if config.notify_on_completion {
|
||||||
|
let notification = gtk::gio::Notification::new("Pixstrip - Processing Complete");
|
||||||
|
notification.set_body(Some(&savings));
|
||||||
|
notification.set_priority(gtk::gio::NotificationPriority::Normal);
|
||||||
|
if let Some(app) = gtk::gio::Application::default() {
|
||||||
|
app.send_notification(Some("batch-complete"), ¬ification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-open output folder if enabled
|
||||||
|
if config.auto_open_output {
|
||||||
|
let output = ui.state.output_dir.borrow().clone();
|
||||||
|
if let Some(dir) = output {
|
||||||
|
let _ = gtk::gio::AppInfo::launch_default_for_uri(
|
||||||
|
&format!("file://{}", dir.display()),
|
||||||
|
gtk::gio::AppLaunchContext::NONE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_results_stats(
|
fn update_results_stats(
|
||||||
@@ -1164,19 +1255,22 @@ fn wire_cancel_button(page: &adw::NavigationPage, cancel_flag: Arc<AtomicBool>)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wire_pause_button(page: &adw::NavigationPage) {
|
fn wire_pause_button(page: &adw::NavigationPage, pause_flag: Arc<AtomicBool>) {
|
||||||
walk_widgets(&page.child(), &|widget| {
|
walk_widgets(&page.child(), &|widget| {
|
||||||
if let Some(button) = widget.downcast_ref::<gtk::Button>()
|
if let Some(button) = widget.downcast_ref::<gtk::Button>()
|
||||||
&& button.label().as_deref() == Some("Pause")
|
&& button.label().as_deref() == Some("Pause")
|
||||||
{
|
{
|
||||||
// Pause is cosmetic for now - we show a toast explaining it pauses after current image
|
let flag = pause_flag.clone();
|
||||||
button.connect_clicked(move |btn| {
|
button.connect_clicked(move |btn| {
|
||||||
if btn.label().as_deref() == Some("Pause") {
|
if btn.label().as_deref() == Some("Pause") {
|
||||||
btn.set_label("Paused");
|
flag.store(true, Ordering::Relaxed);
|
||||||
btn.add_css_class("warning");
|
btn.set_label("Resume");
|
||||||
|
btn.add_css_class("suggested-action");
|
||||||
|
btn.remove_css_class("flat");
|
||||||
} else {
|
} else {
|
||||||
|
flag.store(false, Ordering::Relaxed);
|
||||||
btn.set_label("Pause");
|
btn.set_label("Pause");
|
||||||
btn.remove_css_class("warning");
|
btn.remove_css_class("suggested-action");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1384,19 +1478,85 @@ fn build_preset_from_config(cfg: &JobConfig, name: &str) -> pixstrip_core::prese
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let rotation = match cfg.rotation {
|
||||||
|
1 => Some(pixstrip_core::operations::Rotation::Cw90),
|
||||||
|
2 => Some(pixstrip_core::operations::Rotation::Cw180),
|
||||||
|
3 => Some(pixstrip_core::operations::Rotation::Cw270),
|
||||||
|
4 => Some(pixstrip_core::operations::Rotation::AutoOrient),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let flip = match cfg.flip {
|
||||||
|
1 => Some(pixstrip_core::operations::Flip::Horizontal),
|
||||||
|
2 => Some(pixstrip_core::operations::Flip::Vertical),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let watermark = if cfg.watermark_enabled {
|
||||||
|
let position = match cfg.watermark_position {
|
||||||
|
0 => pixstrip_core::operations::WatermarkPosition::TopLeft,
|
||||||
|
1 => pixstrip_core::operations::WatermarkPosition::TopCenter,
|
||||||
|
2 => pixstrip_core::operations::WatermarkPosition::TopRight,
|
||||||
|
3 => pixstrip_core::operations::WatermarkPosition::MiddleLeft,
|
||||||
|
4 => pixstrip_core::operations::WatermarkPosition::Center,
|
||||||
|
5 => pixstrip_core::operations::WatermarkPosition::MiddleRight,
|
||||||
|
6 => pixstrip_core::operations::WatermarkPosition::BottomLeft,
|
||||||
|
7 => pixstrip_core::operations::WatermarkPosition::BottomCenter,
|
||||||
|
_ => pixstrip_core::operations::WatermarkPosition::BottomRight,
|
||||||
|
};
|
||||||
|
if cfg.watermark_use_image {
|
||||||
|
cfg.watermark_image_path.as_ref().map(|path| {
|
||||||
|
pixstrip_core::operations::WatermarkConfig::Image {
|
||||||
|
path: path.clone(),
|
||||||
|
position,
|
||||||
|
opacity: cfg.watermark_opacity,
|
||||||
|
scale: 0.2,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if !cfg.watermark_text.is_empty() {
|
||||||
|
Some(pixstrip_core::operations::WatermarkConfig::Text {
|
||||||
|
text: cfg.watermark_text.clone(),
|
||||||
|
position,
|
||||||
|
font_size: cfg.watermark_font_size,
|
||||||
|
opacity: cfg.watermark_opacity,
|
||||||
|
color: [255, 255, 255, 255],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let rename = if cfg.rename_enabled {
|
||||||
|
Some(pixstrip_core::operations::RenameConfig {
|
||||||
|
prefix: cfg.rename_prefix.clone(),
|
||||||
|
suffix: cfg.rename_suffix.clone(),
|
||||||
|
counter_start: cfg.rename_counter_start,
|
||||||
|
counter_padding: cfg.rename_counter_padding,
|
||||||
|
template: if cfg.rename_template.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(cfg.rename_template.clone())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
pixstrip_core::preset::Preset {
|
pixstrip_core::preset::Preset {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
description: build_preset_description(cfg),
|
description: build_preset_description(cfg),
|
||||||
icon: "document-save-symbolic".into(),
|
icon: "document-save-symbolic".into(),
|
||||||
is_custom: true,
|
is_custom: true,
|
||||||
resize,
|
resize,
|
||||||
rotation: None,
|
rotation,
|
||||||
flip: None,
|
flip,
|
||||||
convert,
|
convert,
|
||||||
compress,
|
compress,
|
||||||
metadata,
|
metadata,
|
||||||
watermark: None,
|
watermark,
|
||||||
rename: None,
|
rename,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1452,6 +1612,43 @@ fn update_output_summary(ui: &WizardUi) {
|
|||||||
};
|
};
|
||||||
ops.push(mode.to_string());
|
ops.push(mode.to_string());
|
||||||
}
|
}
|
||||||
|
if cfg.watermark_enabled {
|
||||||
|
if cfg.watermark_use_image {
|
||||||
|
ops.push("Image watermark".to_string());
|
||||||
|
} else if !cfg.watermark_text.is_empty() {
|
||||||
|
ops.push(format!("Watermark: \"{}\"", cfg.watermark_text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cfg.rename_enabled {
|
||||||
|
if !cfg.rename_template.is_empty() {
|
||||||
|
ops.push(format!("Rename: {}", cfg.rename_template));
|
||||||
|
} else if !cfg.rename_prefix.is_empty() || !cfg.rename_suffix.is_empty() {
|
||||||
|
ops.push(format!(
|
||||||
|
"Rename: {}...{}",
|
||||||
|
cfg.rename_prefix, cfg.rename_suffix
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
ops.push("Sequential rename".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cfg.rotation > 0 {
|
||||||
|
let rot = match cfg.rotation {
|
||||||
|
1 => "Rotate 90",
|
||||||
|
2 => "Rotate 180",
|
||||||
|
3 => "Rotate 270",
|
||||||
|
4 => "Auto-orient",
|
||||||
|
_ => "Rotate",
|
||||||
|
};
|
||||||
|
ops.push(rot.to_string());
|
||||||
|
}
|
||||||
|
if cfg.flip > 0 {
|
||||||
|
let fl = match cfg.flip {
|
||||||
|
1 => "Flip horizontal",
|
||||||
|
2 => "Flip vertical",
|
||||||
|
_ => "Flip",
|
||||||
|
};
|
||||||
|
ops.push(fl.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
let summary_text = if ops.is_empty() {
|
let summary_text = if ops.is_empty() {
|
||||||
"No operations configured".to_string()
|
"No operations configured".to_string()
|
||||||
@@ -1569,6 +1766,33 @@ fn show_shortcuts_window(window: &adw::ApplicationWindow) {
|
|||||||
dialog.present(Some(window));
|
dialog.present(Some(window));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_accessibility_settings() {
|
||||||
|
let config_store = pixstrip_core::storage::ConfigStore::new();
|
||||||
|
let config = config_store.load().unwrap_or_default();
|
||||||
|
|
||||||
|
if config.high_contrast {
|
||||||
|
// Use libadwaita's high contrast mode
|
||||||
|
let style_manager = adw::StyleManager::default();
|
||||||
|
style_manager.set_color_scheme(adw::ColorScheme::ForceLight);
|
||||||
|
// High contrast is best achieved via the GTK_THEME env or system
|
||||||
|
// settings; the app respects system high contrast automatically
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings = gtk::Settings::default().unwrap();
|
||||||
|
|
||||||
|
if config.large_text {
|
||||||
|
// Increase font DPI by 25% for large text mode
|
||||||
|
let current_dpi = settings.gtk_xft_dpi();
|
||||||
|
if current_dpi > 0 {
|
||||||
|
settings.set_gtk_xft_dpi(current_dpi * 5 / 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.reduced_motion {
|
||||||
|
settings.set_gtk_enable_animations(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn format_bytes(bytes: u64) -> String {
|
fn format_bytes(bytes: u64) -> String {
|
||||||
if bytes < 1024 {
|
if bytes < 1024 {
|
||||||
format!("{} B", bytes)
|
format!("{} B", bytes)
|
||||||
|
|||||||
Reference in New Issue
Block a user