diff --git a/package.json b/package.json index a15379a79..a434b92fc 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "build": "vite build", "tauri": "tauri", "start": "tauri dev", + "bindings:gen": "cd src-tauri && RAPIDRAW_EXPORT_BINDINGS_ONLY=1 cargo run --quiet", + "check:ipc": "./scripts/check-ipc-contract.sh", "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "prettier --write .", diff --git a/scripts/check-ipc-contract.sh b/scripts/check-ipc-contract.sh new file mode 100644 index 000000000..92597a0a4 --- /dev/null +++ b/scripts/check-ipc-contract.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +backend_cmds="$TMP_DIR/backend_cmds.txt" +front_enum_cmds="$TMP_DIR/front_enum_cmds.txt" +front_literal_cmds="$TMP_DIR/front_literal_cmds.txt" +front_event_methods="$TMP_DIR/front_event_methods.txt" +backend_events="$TMP_DIR/backend_events.txt" +front_events="$TMP_DIR/front_events.txt" + +rg -n "#\\[tauri::command\\]" src-tauri/src -A2 --no-heading \ + | rg -o "fn [a-zA-Z0-9_]+" \ + | awk '{print $2}' \ + | sort -u > "$backend_cmds" + +if rg -q "export enum Invokes" src/components/ui/AppProperties.tsx; then + sed -n '/export enum Invokes {/,/^}/p' src/components/ui/AppProperties.tsx \ + | rg -o "'[^']+'" \ + | tr -d "'" \ + | sort -u > "$front_enum_cmds" +else + : > "$front_enum_cmds" +fi + +if rg -q "invoke\\(\\s*['\"][^'\"]+['\"]" src --glob '!src-tauri/**'; then + rg -n "invoke\\(\\s*['\"][^'\"]+['\"]" src --glob '!src-tauri/**' -o \ + | sed -E "s/.*invoke\\(\\s*['\"]([^'\"]+)['\"].*/\\1/" \ + | sort -u > "$front_literal_cmds" +else + : > "$front_literal_cmds" +fi + +if [[ -f src/bindings.ts ]]; then + sed -n '/}>({/,/^})/p' src/bindings.ts \ + | sed -nE 's/^[[:space:]]*[A-Za-z0-9_]+:[[:space:]]*"([^"]+)".*/\1/p' \ + | sort -u > "$backend_events" +else + rg --files src-tauri/src \ + | xargs perl -0777 -ne 'while (/\.emit(?:_to|_filter)?\(\s*"([^"]+)"/g) { print "$1\n"; }' \ + | sort -u > "$backend_events" +fi + +rg --files src --glob '!src-tauri/**' \ + | xargs perl -ne 'while (/(?:listen|once)\(\s*["\x27]([^"\x27]+)["\x27]/g) { print "$1\n"; } while (/events\.([A-Za-z0-9_]+)\.(?:listen|once)\s*\(/g) { print "__method__:$1\n"; }' \ + | sort -u > "$front_event_methods" + +grep -v '^__method__:' "$front_event_methods" > "$front_events" || true + +grep '^__method__:' "$front_event_methods" | sed 's/^__method__://' | while read -r method; do + mapped="$(sed -nE "s/^[[:space:]]*${method}:[[:space:]]*\"([^\"]+)\".*/\\1/p" src/bindings.ts | head -n1)" + if [[ -n "$mapped" ]]; then + echo "$mapped" >> "$front_events" + fi +done + +sort -u "$front_events" -o "$front_events" + +if [[ -s "$front_enum_cmds" ]]; then + echo "=== Commands in frontend enum but missing in backend ===" + comm -23 "$front_enum_cmds" "$backend_cmds" || true + echo + + echo "=== Commands in backend but missing in frontend enum ===" + comm -13 "$front_enum_cmds" "$backend_cmds" || true + echo +else + echo "=== Frontend enum command check skipped (Invokes enum removed) ===" + echo +fi + +echo "=== Literal invoke commands (not enum) ===" +cat "$front_literal_cmds" +echo + +echo "=== Events listened in frontend but not emitted by backend ===" +comm -23 "$front_events" "$backend_events" || true +echo + +echo "=== Events emitted by backend but not listened in frontend ===" +comm -13 "$front_events" "$backend_events" || true +echo + +echo "IPC contract check complete." diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 956355f61..a809578f8 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "RapidRAW" version = "0.0.0" @@ -49,6 +55,8 @@ dependencies = [ "serde_bytes", "serde_json", "sha2", + "specta", + "specta-typescript", "tauri", "tauri-build", "tauri-plugin-dialog", @@ -57,6 +65,7 @@ dependencies = [ "tauri-plugin-process", "tauri-plugin-shell", "tauri-plugin-single-instance", + "tauri-specta", "tempfile", "tokenizers", "tokio", @@ -6572,6 +6581,52 @@ dependencies = [ "system-deps", ] +[[package]] +name = "specta" +version = "2.0.0-rc.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab7f01e9310a820edd31c80fde3cae445295adde21a3f9416517d7d65015b971" +dependencies = [ + "paste", + "serde_json", + "specta-macros", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "specta-macros" +version = "2.0.0-rc.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0074b9e30ed84c6924eb63ad8d2fe71cdc82628525d84b1fcb1f2fd40676517" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "specta-serde" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77216504061374659e7245eac53d30c7b3e5fe64b88da97c753e7184b0781e63" +dependencies = [ + "specta", + "thiserror 1.0.69", +] + +[[package]] +name = "specta-typescript" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3220a0c365e51e248ac98eab5a6a32f544ff6f961906f09d3ee10903a4f52b2d" +dependencies = [ + "specta", + "specta-serde", + "thiserror 1.0.69", +] + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -6859,6 +6914,7 @@ dependencies = [ "serde_json", "serde_repr", "serialize-to-javascript", + "specta", "swift-rs", "tauri-build", "tauri-macros", @@ -7111,6 +7167,34 @@ dependencies = [ "wry", ] +[[package]] +name = "tauri-specta" +version = "2.0.0-rc.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23c0132dd3cf6064e5cd919b82b3f47780e9280e7b5910babfe139829b76655" +dependencies = [ + "heck 0.5.0", + "serde", + "serde_json", + "specta", + "specta-typescript", + "tauri", + "tauri-specta-macros", + "thiserror 2.0.17", +] + +[[package]] +name = "tauri-specta-macros" +version = "2.0.0-rc.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a4aa93823e07859546aa796b8a5d608190cd8037a3a5dce3eb63d491c34bda8" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "tauri-utils" version = "2.8.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 977388b3b..15c6115c0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -8,7 +8,7 @@ rust-version = "1.92" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tauri = { version = "2.9", features = [ "macos-private-api", "rustls-tls" ] } +tauri = { version = "2.9", features = [ "macos-private-api", "rustls-tls", "specta" ] } tauri-plugin-dialog = "2.4.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -66,6 +66,9 @@ quick-xml = { version = "0.36", features = ["serialize"] } fuzzy-matcher = "0.3.7" image-hdr = { version = "0.6.0", default-features = false } mozjpeg-rs = "0.8.0" +tauri-specta = { version = "=2.0.0-rc.21", features = ["derive", "typescript"] } +specta = { version = "=2.0.0-rc.22", features = ["derive", "function", "serde_json", "tokio"] } +specta-typescript = "0.0.9" [build-dependencies] tauri-build = { version = "2.5", features = [] } diff --git a/src-tauri/src/ai_processing.rs b/src-tauri/src/ai_processing.rs index 593c6bba2..c5511d562 100644 --- a/src-tauri/src/ai_processing.rs +++ b/src-tauri/src/ai_processing.rs @@ -509,7 +509,7 @@ pub fn run_u2netp_model(image: &DynamicImage, u2netp_session: &Mutex) - Ok(final_mask) } -#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[derive(Serialize, Deserialize, Debug, Clone, Default, specta::Type)] #[serde(rename_all = "camelCase")] pub struct AiSubjectMaskParameters { pub start_x: f64, @@ -528,7 +528,7 @@ pub struct AiSubjectMaskParameters { pub orientation_steps: Option, } -#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[derive(Serialize, Deserialize, Debug, Clone, Default, specta::Type)] #[serde(rename_all = "camelCase")] pub struct AiSkyMaskParameters { #[serde(default)] @@ -543,7 +543,7 @@ pub struct AiSkyMaskParameters { pub orientation_steps: Option, } -#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[derive(Serialize, Deserialize, Debug, Clone, Default, specta::Type)] #[serde(rename_all = "camelCase")] pub struct AiForegroundMaskParameters { #[serde(default)] @@ -556,4 +556,4 @@ pub struct AiForegroundMaskParameters { pub flip_vertical: Option, #[serde(default)] pub orientation_steps: Option, -} \ No newline at end of file +} diff --git a/src-tauri/src/culling.rs b/src-tauri/src/culling.rs index 2556305ab..ed5b03b2b 100644 --- a/src-tauri/src/culling.rs +++ b/src-tauri/src/culling.rs @@ -10,7 +10,7 @@ use tauri::{AppHandle, Emitter}; use crate::image_loader; -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub struct CullingSettings { pub similarity_threshold: u32, @@ -19,7 +19,7 @@ pub struct CullingSettings { pub filter_blurry: bool, } -#[derive(Serialize, Debug, Clone)] +#[derive(Serialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub struct ImageAnalysisResult { pub path: String, @@ -31,14 +31,14 @@ pub struct ImageAnalysisResult { pub height: u32, } -#[derive(Serialize, Debug, Clone)] +#[derive(Serialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub struct CullGroup { pub representative: ImageAnalysisResult, pub duplicates: Vec, } -#[derive(Serialize, Debug, Clone, Default)] +#[derive(Serialize, Debug, Clone, Default, specta::Type)] #[serde(rename_all = "camelCase")] pub struct CullingSuggestions { pub similar_groups: Vec, @@ -181,6 +181,7 @@ fn analyze_image( } #[tauri::command] +#[specta::specta] pub async fn cull_images( paths: Vec, settings: CullingSettings, diff --git a/src-tauri/src/file_management.rs b/src-tauri/src/file_management.rs index d9d8c2cab..b3b70b650 100644 --- a/src-tauri/src/file_management.rs +++ b/src-tauri/src/file_management.rs @@ -60,7 +60,7 @@ fn emit_thumbnail_cache_setup_error(app_handle: &AppHandle, path: &str, reason: ); } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] pub struct Preset { pub id: String, pub name: String, @@ -73,14 +73,14 @@ struct ExportPresetFile<'a> { presets: &'a [PresetItem], } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] pub struct PresetFolder { pub id: String, pub name: String, pub children: Vec, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub enum PresetItem { Preset(Preset), @@ -92,14 +92,14 @@ pub struct PresetFile { pub presets: Vec, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub struct SortCriteria { pub key: String, pub order: String, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub struct FilterCriteria { pub rating: u8, @@ -139,20 +139,20 @@ impl fmt::Display for ReadFileError { } } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub struct LastFolderState { pub current_folder_path: String, pub expanded_folders: Vec, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, specta::Type)] pub struct MyLens { pub maker: String, pub model: String, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, specta::Type)] #[serde(rename_all = "camelCase")] pub enum PasteMode { Merge, @@ -174,7 +174,7 @@ fn default_included_adjustments() -> HashSet { .collect() } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub struct CopyPasteSettings { pub mode: PasteMode, @@ -194,7 +194,7 @@ impl Default for CopyPasteSettings { } } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub struct ExportPreset { pub id: String, @@ -285,7 +285,7 @@ fn default_tagging_shortcuts_option() -> Option> { "event".to_string(), ]) } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub struct AppSettings { pub last_root_path: Option, @@ -410,7 +410,7 @@ impl Default for AppSettings { } } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] pub struct ImageFile { path: String, modified: u64, @@ -420,7 +420,7 @@ pub struct ImageFile { is_virtual_copy: bool, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub struct ImportSettings { pub filename_template: String, @@ -463,6 +463,7 @@ pub fn parse_virtual_path(virtual_path: &str) -> (PathBuf, PathBuf) { } #[tauri::command] +#[specta::specta] pub async fn read_exif_for_paths( paths: Vec, ) -> Result>, String> { @@ -486,6 +487,7 @@ pub async fn read_exif_for_paths( } #[tauri::command] +#[specta::specta] pub fn list_images_in_dir(path: String, app_handle: AppHandle) -> Result, String> { let settings = load_settings(app_handle).unwrap_or_default(); let enable_xmp_sync = settings.enable_xmp_sync.unwrap_or(false); @@ -585,6 +587,7 @@ pub fn list_images_in_dir(path: String, app_handle: AppHandle) -> Result Result, String> { let settings = load_settings(app_handle).unwrap_or_default(); let enable_xmp_sync = settings.enable_xmp_sync.unwrap_or(false); @@ -691,7 +694,7 @@ pub fn list_images_recursive(path: String, app_handle: AppHandle) -> Result Result { } #[tauri::command] +#[specta::specta] pub async fn get_folder_tree(path: String) -> Result { match tauri::async_runtime::spawn_blocking(move || get_folder_tree_sync(path)).await { Ok(Ok(folder_node)) => Ok(folder_node), @@ -787,6 +791,7 @@ pub async fn get_folder_tree(path: String) -> Result { } #[tauri::command] +#[specta::specta] pub async fn get_pinned_folder_trees(paths: Vec) -> Result, String> { let result = tauri::async_runtime::spawn_blocking(move || { let results: Vec> = paths @@ -1152,6 +1157,7 @@ fn generate_single_thumbnail_and_cache( } #[tauri::command] +#[specta::specta] pub async fn generate_thumbnails( paths: Vec, app_handle: tauri::AppHandle, @@ -1192,6 +1198,7 @@ pub async fn generate_thumbnails( } #[tauri::command] +#[specta::specta] pub fn generate_thumbnails_progressive( paths: Vec, app_handle: tauri::AppHandle, @@ -1272,6 +1279,7 @@ pub fn generate_thumbnails_progressive( } #[tauri::command] +#[specta::specta] pub fn create_folder(path: String) -> Result<(), String> { let path_obj = Path::new(&path); if let (Some(parent), Some(new_folder_name_os)) = (path_obj.parent(), path_obj.file_name()) { @@ -1293,6 +1301,7 @@ pub fn create_folder(path: String) -> Result<(), String> { } #[tauri::command] +#[specta::specta] pub fn rename_folder(path: String, new_name: String) -> Result<(), String> { let p = Path::new(&path); if !p.is_dir() { @@ -1316,6 +1325,7 @@ pub fn rename_folder(path: String, new_name: String) -> Result<(), String> { } #[tauri::command] +#[specta::specta] pub fn delete_folder(path: String) -> Result<(), String> { if let Err(trash_error) = trash::delete(&path) { log::warn!("Failed to move folder to trash: {}. Falling back to permanent delete.", trash_error); @@ -1326,6 +1336,7 @@ pub fn delete_folder(path: String) -> Result<(), String> { } #[tauri::command] +#[specta::specta] pub fn duplicate_file(path: String) -> Result<(), String> { let (source_path, source_sidecar_path) = parse_virtual_path(&path); if !source_path.is_file() { @@ -1401,6 +1412,7 @@ fn find_all_associated_files(source_image_path: &Path) -> Result, S } #[tauri::command] +#[specta::specta] pub fn copy_files(source_paths: Vec, destination_folder: String) -> Result<(), String> { let dest_path = Path::new(&destination_folder); if !dest_path.is_dir() { @@ -1452,6 +1464,7 @@ pub fn copy_files(source_paths: Vec, destination_folder: String) -> Resu } #[tauri::command] +#[specta::specta] pub fn move_files(source_paths: Vec, destination_folder: String) -> Result<(), String> { let dest_path = Path::new(&destination_folder); if !dest_path.is_dir() { @@ -1506,6 +1519,7 @@ pub fn move_files(source_paths: Vec, destination_folder: String) -> Resu } #[tauri::command] +#[specta::specta] pub fn save_metadata_and_update_thumbnail( path: String, adjustments: Value, @@ -1604,6 +1618,7 @@ pub fn save_metadata_and_update_thumbnail( } #[tauri::command] +#[specta::specta] pub fn apply_adjustments_to_paths( paths: Vec, adjustments: Value, @@ -1700,6 +1715,7 @@ pub fn apply_adjustments_to_paths( } #[tauri::command] +#[specta::specta] pub fn reset_adjustments_for_paths( paths: Vec, app_handle: AppHandle, @@ -1785,6 +1801,7 @@ pub fn reset_adjustments_for_paths( } #[tauri::command] +#[specta::specta] pub fn apply_auto_adjustments_to_paths( paths: Vec, app_handle: AppHandle, @@ -1917,6 +1934,7 @@ pub fn apply_auto_adjustments_to_paths( } #[tauri::command] +#[specta::specta] pub fn set_color_label_for_paths(paths: Vec, color: Option, app_handle: AppHandle) -> Result<(), String> { let settings = load_settings(app_handle.clone()).unwrap_or_default(); let enable_xmp_sync = settings.enable_xmp_sync.unwrap_or(false); @@ -1963,6 +1981,7 @@ pub fn set_color_label_for_paths(paths: Vec, color: Option, app_ } #[tauri::command] +#[specta::specta] pub fn load_metadata(path: String, app_handle: AppHandle) -> Result { let settings = load_settings(app_handle).unwrap_or_default(); let enable_xmp_sync = settings.enable_xmp_sync.unwrap_or(false); @@ -2001,6 +2020,7 @@ fn get_presets_path(app_handle: &AppHandle) -> Result Result, String> { let path = get_presets_path(&app_handle)?; if !path.exists() { @@ -2011,6 +2031,7 @@ pub fn load_presets(app_handle: AppHandle) -> Result, String> { } #[tauri::command] +#[specta::specta] pub fn save_presets(presets: Vec, app_handle: AppHandle) -> Result<(), String> { let path = get_presets_path(&app_handle)?; let json_string = serde_json::to_string_pretty(&presets).map_err(|e| e.to_string())?; @@ -2031,6 +2052,7 @@ fn get_settings_path(app_handle: &AppHandle) -> Result Result { let path = get_settings_path(&app_handle)?; @@ -2073,6 +2095,7 @@ pub fn load_settings(app_handle: AppHandle) -> Result { } #[tauri::command] +#[specta::specta] pub fn save_settings(settings: AppSettings, app_handle: AppHandle) -> Result<(), String> { let path = get_settings_path(&app_handle)?; let json_string = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?; @@ -2080,6 +2103,7 @@ pub fn save_settings(settings: AppSettings, app_handle: AppHandle) -> Result<(), } #[tauri::command] +#[specta::specta] pub fn handle_import_presets_from_file( file_path: String, app_handle: AppHandle, @@ -2135,6 +2159,7 @@ pub fn handle_import_presets_from_file( } #[tauri::command] +#[specta::specta] pub fn handle_import_legacy_presets_from_file( file_path: String, app_handle: AppHandle, @@ -2188,6 +2213,7 @@ pub fn handle_import_legacy_presets_from_file( } #[tauri::command] +#[specta::specta] pub fn handle_export_presets_to_file( presets_to_export: Vec, file_path: String, @@ -2203,6 +2229,7 @@ pub fn handle_export_presets_to_file( } #[tauri::command] +#[specta::specta] pub fn save_community_preset( name: String, adjustments: Value, @@ -2252,6 +2279,7 @@ pub fn save_community_preset( } #[tauri::command] +#[specta::specta] pub fn clear_all_sidecars(root_path: String) -> Result { if !Path::new(&root_path).exists() { return Err(format!("Root path does not exist: {}", root_path)); @@ -2279,6 +2307,7 @@ pub fn clear_all_sidecars(root_path: String) -> Result { } #[tauri::command] +#[specta::specta] pub fn clear_thumbnail_cache(app_handle: AppHandle) -> Result<(), String> { let cache_dir = app_handle .path() @@ -2298,6 +2327,7 @@ pub fn clear_thumbnail_cache(app_handle: AppHandle) -> Result<(), String> { } #[tauri::command] +#[specta::specta] pub fn show_in_finder(path: String) -> Result<(), String> { let (source_path, _) = parse_virtual_path(&path); let source_path_str = source_path.to_string_lossy().to_string(); @@ -2334,6 +2364,7 @@ pub fn show_in_finder(path: String) -> Result<(), String> { } #[tauri::command] +#[specta::specta] pub fn delete_files_from_disk(paths: Vec) -> Result<(), String> { let mut files_to_trash = HashSet::new(); @@ -2379,6 +2410,7 @@ pub fn delete_files_from_disk(paths: Vec) -> Result<(), String> { } #[tauri::command] +#[specta::specta] pub fn delete_files_with_associated(paths: Vec) -> Result<(), String> { if paths.is_empty() { return Ok(()); @@ -2518,6 +2550,7 @@ pub fn get_cached_or_generate_thumbnail_image( } #[tauri::command] +#[specta::specta] pub async fn import_files( source_paths: Vec, destination_folder: String, @@ -2649,6 +2682,7 @@ pub fn generate_filename_from_template( } #[tauri::command] +#[specta::specta] pub fn rename_files(paths: Vec, name_template: String) -> Result, String> { if paths.is_empty() { return Ok(Vec::new()); @@ -2724,6 +2758,7 @@ pub fn rename_files(paths: Vec, name_template: String) -> Result Result { let (source_path, source_sidecar_path) = parse_virtual_path(&source_virtual_path); @@ -2954,4 +2989,4 @@ pub fn sync_metadata_to_xmp(source_path: &Path, metadata: &ImageMetadata, create let _ = fs::write(&xmp_file, content); } } -} \ No newline at end of file +} diff --git a/src-tauri/src/image_processing.rs b/src-tauri/src/image_processing.rs index 0076724ec..0a47d5072 100644 --- a/src-tauri/src/image_processing.rs +++ b/src-tauri/src/image_processing.rs @@ -14,7 +14,7 @@ use std::sync::Arc; pub use crate::gpu_processing::{get_or_init_gpu_context, process_and_get_dynamic_image}; use crate::{load_settings, mask_generation::MaskDefinition, AppState}; -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] pub struct ImageMetadata { pub version: u32, pub rating: u8, @@ -34,7 +34,7 @@ impl Default for ImageMetadata { } } -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, specta::Type)] pub struct Crop { pub x: f64, pub y: f64, @@ -42,7 +42,7 @@ pub struct Crop { pub height: f64, } -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, specta::Type)] pub struct GeometryParams { pub distortion: f32, pub vertical: f32, @@ -1922,7 +1922,7 @@ fn apply_gentle_detail_enhance( }); } -#[derive(Serialize, Clone)] +#[derive(Serialize, Clone, specta::Type)] pub struct HistogramData { red: Vec, green: Vec, @@ -1931,6 +1931,7 @@ pub struct HistogramData { } #[tauri::command] +#[specta::specta] pub fn generate_histogram( state: tauri::State, app_handle: tauri::AppHandle, @@ -2064,7 +2065,7 @@ fn normalize_histogram_range(histogram: &mut Vec, percentile_clip: f32) { } } -#[derive(Serialize, Clone)] +#[derive(Serialize, Clone, specta::Type)] pub struct WaveformData { red: Vec, green: Vec, @@ -2075,6 +2076,7 @@ pub struct WaveformData { } #[tauri::command] +#[specta::specta] pub fn generate_waveform( state: tauri::State, app_handle: tauri::AppHandle, @@ -2351,6 +2353,7 @@ pub fn auto_results_to_json(results: &AutoAdjustmentResults) -> serde_json::Valu } #[tauri::command] +#[specta::specta] pub fn calculate_auto_adjustments( state: tauri::State, ) -> Result { @@ -2366,4 +2369,4 @@ pub fn calculate_auto_adjustments( let results = perform_auto_analysis(&original_image); Ok(auto_results_to_json(&results)) -} \ No newline at end of file +} diff --git a/src-tauri/src/lens_correction.rs b/src-tauri/src/lens_correction.rs index 731264e9d..2ec26dedc 100644 --- a/src-tauri/src/lens_correction.rs +++ b/src-tauri/src/lens_correction.rs @@ -145,7 +145,7 @@ pub struct MultiName { value: String, } -#[derive(Serialize)] +#[derive(Serialize, specta::Type)] pub struct LensDistortionParams { k1: f64, k2: f64, @@ -509,6 +509,7 @@ pub fn load_lensfun_db(app_handle: &tauri::AppHandle) -> LensDatabase { } #[tauri::command] +#[specta::specta] pub fn get_lensfun_makers(state: State) -> Result, String> { let db_guard = state.lens_db.lock().map_err(|e| format!("Lock poisoned: {}", e))?; if let Some(db) = &*db_guard { @@ -526,6 +527,7 @@ pub fn get_lensfun_makers(state: State) -> Result, String> } #[tauri::command] +#[specta::specta] pub fn get_lensfun_lenses_for_maker(maker: String, state: State) -> Result, String> { let db_guard = state.lens_db.lock().map_err(|e| format!("Lock poisoned: {}", e))?; if let Some(db) = &*db_guard { @@ -544,6 +546,7 @@ pub fn get_lensfun_lenses_for_maker(maker: String, state: State) -> Re } #[tauri::command] +#[specta::specta] pub fn autodetect_lens(maker: String, model: String, state: State) -> Result, String> { let clean_maker = maker.trim().trim_matches('"').to_string(); let clean_model = model.trim().trim_matches('"').to_string(); @@ -628,6 +631,7 @@ pub fn autodetect_lens(maker: String, model: String, state: State) -> } #[tauri::command] +#[specta::specta] pub fn get_lens_distortion_params( maker: String, model: String, @@ -647,4 +651,4 @@ pub fn get_lens_distortion_params( } } Ok(None) -} \ No newline at end of file +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 833a9b4e2..2ce9838e7 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -56,7 +56,9 @@ use rayon::prelude::*; use reqwest; use serde::{Deserialize, Serialize}; use serde_json::Value; -use tauri::{Emitter, Manager, ipc::Response}; +use specta_typescript::{BigIntExportBehavior, Typescript}; +use tauri::{Emitter, Manager}; +use tauri_specta::{Builder as SpectaBuilder, ErrorHandlingMode, Event, collect_commands, collect_events}; use tempfile::NamedTempFile; use tokio::sync::Mutex as TokioMutex; use tokio::task::JoinHandle; @@ -135,6 +137,124 @@ struct PreviewUpdatePayload { data: Vec, } +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(rename_all = "camelCase")] +struct AiConnectorStatusUpdate { + connected: bool, +} + +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct PreviewUpdateFinal(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct PreviewUpdateUncropped(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct HistogramUpdate(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct OpenWithFile(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct WaveformUpdate(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct ThumbnailGenerated(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct AiModelDownloadStart(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct AiModelDownloadFinish(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct IndexingStarted(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct IndexingProgress(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct IndexingError(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct IndexingFinished(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct BatchExportProgress(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct ExportComplete(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct ExportCompleteWithErrors(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct ExportError(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct ExportCancelled(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct ImportStart(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct ImportProgress(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct ImportComplete(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct ImportError(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct ThumbnailGenerationError(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct ThumbnailProgress(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct ThumbnailGenerationComplete(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct DenoiseProgress(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct DenoiseComplete(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct DenoiseError(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct PanoramaProgress(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct PanoramaComplete(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct PanoramaError(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct PanoramaWarning(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct HdrProgress(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct HdrComplete(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct HdrError(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct CullingStart(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct CullingProgress(serde_json::Value); +#[derive(Serialize, Deserialize, Clone, Debug, specta::Type, tauri_specta::Event)] +#[serde(transparent)] +struct CullingComplete(serde_json::Value); + pub struct AppState { window_setup_complete: AtomicBool, original_image: Mutex>, @@ -162,7 +282,7 @@ pub struct AppState { pub load_image_generation: Arc, } -#[derive(serde::Serialize)] +#[derive(serde::Serialize, specta::Type)] struct LoadImageResult { width: u32, height: u32, @@ -171,7 +291,7 @@ struct LoadImageResult { is_raw: bool, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] enum ResizeMode { LongEdge, @@ -180,7 +300,7 @@ enum ResizeMode { Height, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] struct ResizeOptions { mode: ResizeMode, @@ -188,7 +308,7 @@ struct ResizeOptions { dont_enlarge: bool, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] struct ExportSettings { jpeg_quality: u8, @@ -201,19 +321,19 @@ struct ExportSettings { export_masks: bool, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] pub struct CommunityPreset { pub name: String, pub creator: String, pub adjustments: Value, } -#[derive(Serialize)] +#[derive(Serialize, specta::Type)] struct LutParseResult { size: u32, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub enum WatermarkAnchor { TopLeft, @@ -227,7 +347,7 @@ pub enum WatermarkAnchor { BottomRight, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub struct WatermarkSettings { path: String, @@ -237,7 +357,7 @@ pub struct WatermarkSettings { opacity: f32, } -#[derive(serde::Serialize)] +#[derive(serde::Serialize, specta::Type)] struct ImageDimensions { width: u32, height: u32, @@ -511,6 +631,7 @@ fn get_or_load_lut(state: &tauri::State, path: &str) -> Result, @@ -637,6 +758,7 @@ async fn load_image( } #[tauri::command] +#[specta::specta] fn get_image_dimensions(path: String) -> Result { let (source_path, _) = parse_virtual_path(&path); image::image_dimensions(&source_path) @@ -645,6 +767,7 @@ fn get_image_dimensions(path: String) -> Result { } #[tauri::command] +#[specta::specta] fn cancel_thumbnail_generation(state: tauri::State) -> Result<(), String> { state .thumbnail_cancellation_token @@ -953,6 +1076,7 @@ fn start_preview_worker(app_handle: tauri::AppHandle) { } #[tauri::command] +#[specta::specta] fn apply_adjustments( js_adjustments: serde_json::Value, is_interactive: bool, @@ -970,6 +1094,7 @@ fn apply_adjustments( } #[tauri::command] +#[specta::specta] fn generate_uncropped_preview( js_adjustments: serde_json::Value, state: tauri::State, @@ -1083,11 +1208,12 @@ fn generate_uncropped_preview( } #[tauri::command] +#[specta::specta] fn generate_original_transformed_preview( js_adjustments: serde_json::Value, state: tauri::State, app_handle: tauri::AppHandle, -) -> Result { +) -> Result, String> { let loaded_image = state .original_image .lock() @@ -1124,10 +1250,11 @@ fn generate_original_transformed_preview( .encode_rgb(&rgb_pixels, width as u32, height as u32) .map_err(|e| format!("Failed to encode with mozjpeg-rs: {}", e))?; - Ok(Response::new(bytes)) + Ok(bytes) } #[tauri::command] +#[specta::specta] async fn preview_geometry_transform( params: GeometryParams, js_adjustments: serde_json::Value, @@ -1327,6 +1454,7 @@ fn get_full_image_for_processing( } #[tauri::command] +#[specta::specta] async fn generate_fullscreen_preview( js_adjustments: serde_json::Value, app_handle: tauri::AppHandle, @@ -1688,6 +1816,7 @@ fn export_masks_for_image( } #[tauri::command] +#[specta::specta] async fn export_image( original_path: String, output_path: String, @@ -1767,6 +1896,7 @@ async fn export_image( } #[tauri::command] +#[specta::specta] async fn batch_export_images( output_folder: String, paths: Vec, @@ -1971,11 +2101,13 @@ async fn batch_export_images( } #[tauri::command] -fn cancel_export(state: tauri::State) -> Result<(), String> { +#[specta::specta] +fn cancel_export(state: tauri::State, app_handle: tauri::AppHandle) -> Result<(), String> { match state.export_task_handle.lock().unwrap().take() { Some(handle) => { handle.abort(); println!("Export task cancellation requested."); + let _ = app_handle.emit("export-cancelled", ()); } _ => { return Err("No export task is currently running.".to_string()); @@ -1985,6 +2117,7 @@ fn cancel_export(state: tauri::State) -> Result<(), String> { } #[tauri::command] +#[specta::specta] async fn estimate_export_size( js_adjustments: Value, export_settings: ExportSettings, @@ -2089,6 +2222,7 @@ async fn estimate_export_size( } #[tauri::command] +#[specta::specta] async fn estimate_batch_export_size( paths: Vec, export_settings: ExportSettings, @@ -2250,6 +2384,7 @@ async fn estimate_batch_export_size( } #[tauri::command] +#[specta::specta] fn generate_mask_overlay( mask_def: MaskDefinition, width: u32, @@ -2284,6 +2419,7 @@ fn generate_mask_overlay( } #[tauri::command] +#[specta::specta] async fn generate_ai_foreground_mask( js_adjustments: serde_json::Value, rotation: f32, @@ -2318,6 +2454,7 @@ async fn generate_ai_foreground_mask( } #[tauri::command] +#[specta::specta] async fn generate_ai_sky_mask( js_adjustments: serde_json::Value, rotation: f32, @@ -2351,6 +2488,7 @@ async fn generate_ai_sky_mask( } #[tauri::command] +#[specta::specta] async fn generate_ai_subject_mask( js_adjustments: serde_json::Value, path: String, @@ -2507,10 +2645,11 @@ async fn generate_ai_subject_mask( } #[tauri::command] +#[specta::specta] fn generate_preset_preview( js_adjustments: serde_json::Value, state: tauri::State, -) -> Result { +) -> Result, String> { let context = get_or_init_gpu_context(&state)?; let loaded_image = state @@ -2562,15 +2701,17 @@ fn generate_preset_preview( .write_with_encoder(JpegEncoder::new_with_quality(&mut buf, 50)) .map_err(|e| e.to_string())?; - Ok(Response::new(buf.into_inner())) + Ok(buf.into_inner()) } #[tauri::command] +#[specta::specta] fn update_window_effect(theme: String, window: tauri::Window) { apply_window_effect(theme, window); } #[tauri::command] +#[specta::specta] async fn check_ai_connector_status(app_handle: tauri::AppHandle) { let settings = load_settings(app_handle.clone()).unwrap_or_default(); let is_connected = if let Some(address) = settings.ai_connector_address { @@ -2578,13 +2719,14 @@ async fn check_ai_connector_status(app_handle: tauri::AppHandle) { } else { false }; - let _ = app_handle.emit( - "ai-connector-status-update", - serde_json::json!({ "connected": is_connected }), - ); + let _ = AiConnectorStatusUpdate { + connected: is_connected, + } + .emit(&app_handle); } #[tauri::command] +#[specta::specta] async fn test_ai_connector_connection(address: String) -> Result<(), String> { match ai_connector::check_status(&address).await { Ok(true) => Ok(()), @@ -2604,6 +2746,7 @@ fn calculate_dynamic_patch_radius(width: u32, height: u32) -> u32 { } #[tauri::command] +#[specta::specta] async fn invoke_generative_replace_with_mask_def( path: String, patch_definition: AiPatchDefinition, @@ -2769,6 +2912,7 @@ async fn invoke_generative_replace_with_mask_def( } #[tauri::command] +#[specta::specta] fn get_supported_file_types() -> Result { let raw_extensions: Vec<&str> = crate::formats::RAW_EXTENSIONS .iter() @@ -2783,6 +2927,7 @@ fn get_supported_file_types() -> Result { } #[tauri::command] +#[specta::specta] async fn fetch_community_presets() -> Result, String> { let client = reqwest::Client::new(); let url = "https://raw.githubusercontent.com/CyberTimon/RapidRAW-Presets/main/manifest.json"; @@ -2807,6 +2952,7 @@ async fn fetch_community_presets() -> Result, String> { } #[tauri::command] +#[specta::specta] async fn generate_all_community_previews( image_paths: Vec, presets: Vec, @@ -2939,6 +3085,7 @@ async fn generate_all_community_previews( } #[tauri::command] +#[specta::specta] async fn save_temp_file(bytes: Vec) -> Result { let mut temp_file = NamedTempFile::new().map_err(|e| e.to_string())?; temp_file.write_all(&bytes).map_err(|e| e.to_string())?; @@ -2947,6 +3094,7 @@ async fn save_temp_file(bytes: Vec) -> Result { } #[tauri::command] +#[specta::specta] async fn stitch_panorama( paths: Vec, app_handle: tauri::AppHandle, @@ -3019,6 +3167,7 @@ async fn stitch_panorama( } #[tauri::command] +#[specta::specta] async fn save_panorama( first_path_str: String, state: tauri::State<'_, AppState>, @@ -3060,13 +3209,20 @@ async fn save_panorama( } #[tauri::command] +#[specta::specta] async fn merge_hdr( paths: Vec, app_handle: tauri::AppHandle, state: tauri::State<'_, AppState>, ) -> Result<(), String> { + let emit_hdr_error = |message: &str| { + let _ = app_handle.emit("hdr-error", message.to_string()); + }; + if paths.len() < 2 { - return Err("Please select at least two images to merge.".to_string()); + let msg = "Please select at least two images to merge.".to_string(); + emit_hdr_error(&msg); + return Err(msg); } let hdr_result_handle = state.hdr_result.clone(); @@ -3112,20 +3268,26 @@ async fn merge_hdr( Ok((path.clone(), dynamic_image, exposure, gains)) }) - .collect::, String>>()?; + .collect::, String>>() + .map_err(|e| { + emit_hdr_error(&e); + e + })?; if let Some((first_path, first_img, _, _)) = loaded_items.first() { let (width, height) = (first_img.width(), first_img.height()); for (path, img, _, _) in loaded_items.iter().skip(1) { if img.width() != width || img.height() != height { - return Err(format!( + let msg = format!( "Dimension mismatch detected.\n\nBase image ({}): {}x{}\nTarget image ({}): {}x{}\n\nHDR merge requires all images to be exactly the same size.", Path::new(first_path).file_name().unwrap_or_default().to_string_lossy(), width, height, Path::new(path).file_name().unwrap_or_default().to_string_lossy(), img.width(), img.height() - )); + ); + emit_hdr_error(&msg); + return Err(msg); } } } @@ -3136,15 +3298,25 @@ async fn merge_hdr( HDRInput::with_image(img, *exposure, *gains) .map_err(|e| format!("Failed to prepare HDR input for {}: {}", path, e)) }) - .collect::, String>>()?; + .collect::, String>>() + .map_err(|e| { + emit_hdr_error(&e); + e + })?; log::info!("Starting HDR merge of {} images", images.len()); - let hdr_merged = hdr_merge_images(&mut images.into()).map_err(|e| e.to_string())?; + let hdr_merged = hdr_merge_images(&mut images.into()).map_err(|e| { + let msg = e.to_string(); + emit_hdr_error(&msg); + msg + })?; log::info!("HDR merge completed"); let mut buf = Cursor::new(Vec::new()); if let Err(e) = hdr_merged.to_rgb8().write_to(&mut buf, ImageFormat::Png) { - return Err(format!("Failed to encode hdr preview: {}", e)); + let msg = format!("Failed to encode hdr preview: {}", e); + emit_hdr_error(&msg); + return Err(msg); } let base64_str = general_purpose::STANDARD.encode(buf.get_ref()); @@ -3164,6 +3336,7 @@ async fn merge_hdr( } #[tauri::command] +#[specta::specta] async fn save_hdr( first_path_str: String, state: tauri::State<'_, AppState>, @@ -3206,6 +3379,7 @@ async fn save_hdr( } #[tauri::command] +#[specta::specta] async fn apply_denoising( path: String, intensity: f32, @@ -3232,6 +3406,7 @@ async fn apply_denoising( } #[tauri::command] +#[specta::specta] async fn save_denoised_image( original_path_str: String, state: tauri::State<'_, AppState>, @@ -3275,6 +3450,7 @@ async fn save_denoised_image( } #[tauri::command] +#[specta::specta] async fn save_collage(base64_data: String, first_path_str: String) -> Result { let data_url_prefix = "data:image/png;base64,"; if !base64_data.starts_with(data_url_prefix) { @@ -3305,12 +3481,13 @@ async fn save_collage(base64_data: String, first_path_str: String) -> Result, app_handle: tauri::AppHandle, -) -> Result { +) -> Result, String> { let context = get_or_init_gpu_context(&state)?; let (source_path, _) = parse_virtual_path(&path); let source_path_str = source_path.to_string_lossy().to_string(); @@ -3383,10 +3560,11 @@ fn generate_preview_for_path( .encode_rgb(&rgb_pixels, width as u32, height as u32) .map_err(|e| format!("Failed to encode with mozjpeg-rs: {}", e))?; - Ok(Response::new(bytes)) + Ok(bytes) } #[tauri::command] +#[specta::specta] async fn load_and_parse_lut( path: String, state: tauri::State<'_, AppState>, @@ -3516,6 +3694,7 @@ fn setup_logging(app_handle: &tauri::AppHandle) { } #[tauri::command] +#[specta::specta] fn get_log_file_path(app_handle: tauri::AppHandle) -> Result { let log_dir = app_handle.path().app_log_dir().map_err(|e| e.to_string())?; let log_file_path = log_dir.join("app.log"); @@ -3523,6 +3702,7 @@ fn get_log_file_path(app_handle: tauri::AppHandle) -> Result { } #[tauri::command] +#[specta::specta] fn frontend_log(level: String, message: String) -> Result<(), String> { let trimmed = message.trim(); if trimmed.is_empty() { @@ -3553,6 +3733,7 @@ fn handle_file_open(app_handle: &tauri::AppHandle, path: PathBuf) { } #[tauri::command] +#[specta::specta] fn frontend_ready( app_handle: tauri::AppHandle, window: tauri::Window, @@ -3618,6 +3799,162 @@ fn frontend_ready( } fn main() { + let specta_builder = SpectaBuilder::::new() + .commands(collect_commands![ + load_image, + apply_adjustments, + export_image, + batch_export_images, + cancel_export, + estimate_export_size, + estimate_batch_export_size, + generate_fullscreen_preview, + generate_preview_for_path, + generate_original_transformed_preview, + generate_preset_preview, + generate_uncropped_preview, + preview_geometry_transform, + generate_mask_overlay, + generate_ai_subject_mask, + generate_ai_foreground_mask, + generate_ai_sky_mask, + update_window_effect, + check_ai_connector_status, + test_ai_connector_connection, + invoke_generative_replace_with_mask_def, + get_supported_file_types, + get_log_file_path, + frontend_log, + save_collage, + stitch_panorama, + save_panorama, + merge_hdr, + save_hdr, + apply_denoising, + save_denoised_image, + load_and_parse_lut, + fetch_community_presets, + generate_all_community_previews, + save_temp_file, + get_image_dimensions, + frontend_ready, + cancel_thumbnail_generation, + image_processing::generate_histogram, + image_processing::generate_waveform, + image_processing::calculate_auto_adjustments, + file_management::read_exif_for_paths, + file_management::list_images_in_dir, + file_management::list_images_recursive, + file_management::get_folder_tree, + file_management::get_pinned_folder_trees, + file_management::generate_thumbnails, + file_management::generate_thumbnails_progressive, + file_management::create_folder, + file_management::delete_folder, + file_management::copy_files, + file_management::move_files, + file_management::rename_folder, + file_management::rename_files, + file_management::duplicate_file, + file_management::show_in_finder, + file_management::delete_files_from_disk, + file_management::delete_files_with_associated, + file_management::save_metadata_and_update_thumbnail, + file_management::apply_adjustments_to_paths, + file_management::load_metadata, + file_management::load_presets, + file_management::save_presets, + file_management::load_settings, + file_management::save_settings, + file_management::reset_adjustments_for_paths, + file_management::apply_auto_adjustments_to_paths, + file_management::handle_import_presets_from_file, + file_management::handle_import_legacy_presets_from_file, + file_management::handle_export_presets_to_file, + file_management::save_community_preset, + file_management::clear_all_sidecars, + file_management::clear_thumbnail_cache, + file_management::set_color_label_for_paths, + file_management::import_files, + file_management::create_virtual_copy, + tagging::start_background_indexing, + tagging::clear_ai_tags, + tagging::clear_all_tags, + tagging::add_tag_for_paths, + tagging::remove_tag_for_paths, + culling::cull_images, + lens_correction::get_lensfun_makers, + lens_correction::get_lensfun_lenses_for_maker, + lens_correction::autodetect_lens, + lens_correction::get_lens_distortion_params, + negative_conversion::preview_negative_conversion, + negative_conversion::convert_negative_full, + negative_conversion::save_converted_negative, + ]) + .events(collect_events![ + AiConnectorStatusUpdate, + PreviewUpdateFinal, + PreviewUpdateUncropped, + HistogramUpdate, + OpenWithFile, + WaveformUpdate, + ThumbnailGenerated, + AiModelDownloadStart, + AiModelDownloadFinish, + IndexingStarted, + IndexingProgress, + IndexingError, + IndexingFinished, + BatchExportProgress, + ExportComplete, + ExportCompleteWithErrors, + ExportError, + ExportCancelled, + ImportStart, + ImportProgress, + ImportComplete, + ImportError, + ThumbnailGenerationError, + ThumbnailProgress, + ThumbnailGenerationComplete, + DenoiseProgress, + DenoiseComplete, + DenoiseError, + PanoramaProgress, + PanoramaComplete, + PanoramaError, + PanoramaWarning, + HdrProgress, + HdrComplete, + HdrError, + CullingStart, + CullingProgress, + CullingComplete + ]) + .error_handling(ErrorHandlingMode::Throw); + + #[cfg(debug_assertions)] + { + specta_builder + .export( + Typescript::default().bigint(BigIntExportBehavior::Number), + "../src/bindings.ts", + ) + .expect("Failed to export TypeScript bindings"); + } + + if std::env::var("RAPIDRAW_EXPORT_BINDINGS_ONLY").as_deref() == Ok("1") { + specta_builder + .export( + Typescript::default().bigint(BigIntExportBehavior::Number), + "../src/bindings.ts", + ) + .expect("Failed to export TypeScript bindings"); + return; + } + + let specta_invoke_handler = specta_builder.invoke_handler(); + tauri::Builder::default() .plugin(tauri_plugin_single_instance::init(|app, argv, _cwd| { log::info!("New instance launched with args: {:?}. Focusing main window.", argv); @@ -3642,7 +3979,10 @@ fn main() { .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_shell::init()) - .setup(|app| { + .invoke_handler(specta_invoke_handler) + .setup(move |app| { + specta_builder.mount_events(app); + #[cfg(any(windows, target_os = "linux"))] { if let Some(arg) = std::env::args().nth(1) { @@ -3861,97 +4201,6 @@ fn main() { lens_db: Mutex::new(None), load_image_generation: Arc::new(AtomicUsize::new(0)), }) - .invoke_handler(tauri::generate_handler![ - load_image, - apply_adjustments, - export_image, - batch_export_images, - cancel_export, - estimate_export_size, - estimate_batch_export_size, - generate_fullscreen_preview, - generate_preview_for_path, - generate_original_transformed_preview, - generate_preset_preview, - generate_uncropped_preview, - preview_geometry_transform, - generate_mask_overlay, - generate_ai_subject_mask, - generate_ai_foreground_mask, - generate_ai_sky_mask, - update_window_effect, - check_ai_connector_status, - test_ai_connector_connection, - invoke_generative_replace_with_mask_def, - get_supported_file_types, - get_log_file_path, - frontend_log, - save_collage, - stitch_panorama, - save_panorama, - merge_hdr, - save_hdr, - apply_denoising, - save_denoised_image, - load_and_parse_lut, - fetch_community_presets, - generate_all_community_previews, - save_temp_file, - get_image_dimensions, - frontend_ready, - cancel_thumbnail_generation, - image_processing::generate_histogram, - image_processing::generate_waveform, - image_processing::calculate_auto_adjustments, - file_management::read_exif_for_paths, - file_management::list_images_in_dir, - file_management::list_images_recursive, - file_management::get_folder_tree, - file_management::get_pinned_folder_trees, - file_management::generate_thumbnails, - file_management::generate_thumbnails_progressive, - file_management::create_folder, - file_management::delete_folder, - file_management::copy_files, - file_management::move_files, - file_management::rename_folder, - file_management::rename_files, - file_management::duplicate_file, - file_management::show_in_finder, - file_management::delete_files_from_disk, - file_management::delete_files_with_associated, - file_management::save_metadata_and_update_thumbnail, - file_management::apply_adjustments_to_paths, - file_management::load_metadata, - file_management::load_presets, - file_management::save_presets, - file_management::load_settings, - file_management::save_settings, - file_management::reset_adjustments_for_paths, - file_management::apply_auto_adjustments_to_paths, - file_management::handle_import_presets_from_file, - file_management::handle_import_legacy_presets_from_file, - file_management::handle_export_presets_to_file, - file_management::save_community_preset, - file_management::clear_all_sidecars, - file_management::clear_thumbnail_cache, - file_management::set_color_label_for_paths, - file_management::import_files, - file_management::create_virtual_copy, - tagging::start_background_indexing, - tagging::clear_ai_tags, - tagging::clear_all_tags, - tagging::add_tag_for_paths, - tagging::remove_tag_for_paths, - culling::cull_images, - lens_correction::get_lensfun_makers, - lens_correction::get_lensfun_lenses_for_maker, - lens_correction::autodetect_lens, - lens_correction::get_lens_distortion_params, - negative_conversion::preview_negative_conversion, - negative_conversion::convert_negative_full, - negative_conversion::save_converted_negative, - ]) .build(tauri::generate_context!()) .expect("error while building tauri application") .run(#[allow(unused_variables)] |app_handle, event| { @@ -3974,4 +4223,4 @@ fn main() { _ => {} } }); -} \ No newline at end of file +} diff --git a/src-tauri/src/mask_generation.rs b/src-tauri/src/mask_generation.rs index c20d734fa..b2e140176 100644 --- a/src-tauri/src/mask_generation.rs +++ b/src-tauri/src/mask_generation.rs @@ -9,14 +9,14 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::f32::consts::PI; -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, specta::Type)] #[serde(rename_all = "camelCase")] pub enum SubMaskMode { Additive, Subtractive, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub struct SubMask { pub id: String, @@ -35,7 +35,7 @@ fn default_opacity() -> f32 { 100.0 } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub struct MaskDefinition { pub id: String, @@ -48,14 +48,14 @@ pub struct MaskDefinition { pub sub_masks: Vec, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub struct PatchData { pub color: String, pub mask: String, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, specta::Type)] #[serde(rename_all = "camelCase")] pub struct AiPatchDefinition { pub id: String, @@ -749,4 +749,4 @@ pub fn generate_mask_bitmap( } Some(final_mask) -} \ No newline at end of file +} diff --git a/src-tauri/src/negative_conversion.rs b/src-tauri/src/negative_conversion.rs index 2189d3ea8..bcd38a3d8 100644 --- a/src-tauri/src/negative_conversion.rs +++ b/src-tauri/src/negative_conversion.rs @@ -16,7 +16,7 @@ use serde::{Deserialize, Serialize}; use crate::image_processing::downscale_f32_image; use crate::AppState; -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, specta::Type)] pub struct NegativeConversionParams { pub red_weight: f32, pub green_weight: f32, @@ -168,6 +168,7 @@ fn run_pipeline( } #[tauri::command] +#[specta::specta] pub async fn preview_negative_conversion( path: String, params: NegativeConversionParams, @@ -246,6 +247,7 @@ pub async fn preview_negative_conversion( } #[tauri::command] +#[specta::specta] pub async fn convert_negative_full( path: String, params: NegativeConversionParams, @@ -311,6 +313,7 @@ pub async fn convert_negative_full( } #[tauri::command] +#[specta::specta] pub async fn save_converted_negative( original_path_str: String, state: tauri::State<'_, AppState>, @@ -332,4 +335,4 @@ pub async fn save_converted_negative( processed_image.to_rgb16().save(&out_path).map_err(|e| e.to_string())?; Ok(out_path.to_string_lossy().to_string()) -} \ No newline at end of file +} diff --git a/src-tauri/src/tagging.rs b/src-tauri/src/tagging.rs index d691643a4..852a95cee 100644 --- a/src-tauri/src/tagging.rs +++ b/src-tauri/src/tagging.rs @@ -251,6 +251,7 @@ pub fn generate_tags_with_clip( } #[tauri::command] +#[specta::specta] pub async fn start_background_indexing( folder_path: String, app_handle: AppHandle, @@ -459,6 +460,7 @@ fn modify_tags_for_path(path_str: &str, modify_fn: impl Fn(&mut Vec)) -> } #[tauri::command] +#[specta::specta] pub fn add_tag_for_paths(paths: Vec, tag: String) -> Result<(), String> { paths.par_iter().for_each(|path| { let tag_clone = tag.clone(); @@ -474,6 +476,7 @@ pub fn add_tag_for_paths(paths: Vec, tag: String) -> Result<(), String> } #[tauri::command] +#[specta::specta] pub fn remove_tag_for_paths(paths: Vec, tag: String) -> Result<(), String> { paths.par_iter().for_each(|path| { let tag_clone = tag.clone(); @@ -487,6 +490,7 @@ pub fn remove_tag_for_paths(paths: Vec, tag: String) -> Result<(), Strin } #[tauri::command] +#[specta::specta] pub fn clear_ai_tags(root_path: String) -> Result { if !Path::new(&root_path).exists() { return Err(format!("Root path does not exist: {}", root_path)); @@ -526,6 +530,7 @@ pub fn clear_ai_tags(root_path: String) -> Result { } #[tauri::command] +#[specta::specta] pub fn clear_all_tags(root_path: String) -> Result { if !Path::new(&root_path).exists() { return Err(format!("Root path does not exist: {}", root_path)); diff --git a/src/App.tsx b/src/App.tsx index 4992d286c..f7514d454 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,5 @@ import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { invoke } from '@tauri-apps/api/core'; -import { listen } from '@tauri-apps/api/event'; import { open } from '@tauri-apps/plugin-dialog'; import { homeDir } from '@tauri-apps/api/path'; import { getCurrentWindow } from '@tauri-apps/api/window'; @@ -95,7 +93,6 @@ import { AppSettings, BrushSettings, FilterCriteria, - Invokes, ImageFile, Option, OPTION_SEPARATOR, @@ -118,6 +115,7 @@ import { } from './components/ui/AppProperties'; import { ChannelConfig } from './components/adjustments/Curves'; import HdrModal from './components/modals/HdrModal'; +import { commands, events } from './bindings'; const CLERK_PUBLISHABLE_KEY = 'pk_test_YnJpZWYtc2Vhc25haWwtMTIuY2xlcmsuYWNjb3VudHMuZGV2JA'; // local dev key @@ -648,9 +646,7 @@ function App() { const generate = async () => { if (showOriginal && selectedImage?.path && !transformedOriginalUrl) { try { - const imageData: Uint8Array = await invoke('generate_original_transformed_preview', { - jsAdjustments: adjustments, - }); + const imageData: Uint8Array = await commands.generateOriginalTransformedPreview(adjustments,); if (isEffectActive) { const blob = new Blob([imageData], { type: 'image/jpeg' }); objectUrl = URL.createObjectURL(blob); @@ -680,14 +676,14 @@ function App() { }, [libraryViewMode]); useEffect(() => { - const unlisten = listen('ai-connector-status-update', (event: any) => { + const unlisten = events.aiConnectorStatusUpdate.listen((event) => { setisAIConnectorConnected(event.payload.connected); }); - invoke(Invokes.CheckAIConnectorStatus); - const interval = setInterval(() => invoke(Invokes.CheckAIConnectorStatus), 10000); + void commands.checkAiConnectorStatus(); + const interval = setInterval(() => void commands.checkAiConnectorStatus(), 10000); return () => { clearInterval(interval); - unlisten.then((f) => f()); + unlisten.then((f) => void f()); }; }, []); @@ -727,12 +723,13 @@ function App() { setIsGeneratingAi(true); try { - const newPatchDataJson: any = await invoke(Invokes.InvokeGenerativeReplaseWithMaskDef, { - currentAdjustments: adjustments, - patchDefinition: patchDefinition, - path: selectedImage.path, - useFastInpaint: useFastInpaint, - }); + const newPatchDataJson: any = await commands.invokeGenerativeReplaceWithMaskDef( + selectedImage.path, + patchDefinition, + adjustments, + useFastInpaint, + null, + ); const newPatchData = JSON.parse(newPatchDataJson); patchesSentToBackend.current.delete(patchId); @@ -813,16 +810,16 @@ function App() { lensVignetteEnabled: adjustments.lensVignetteEnabled, }; - const newMaskParams: any = await invoke(Invokes.GenerateAiSubjectMask, { - jsAdjustments: transformAdjustments, - endPoint: [endPoint.x, endPoint.y], - flipHorizontal: adjustments.flipHorizontal, - flipVertical: adjustments.flipVertical, - orientationSteps: adjustments.orientationSteps, - path: selectedImage.path, - rotation: adjustments.rotation, - startPoint: [startPoint.x, startPoint.y], - }); + const newMaskParams: any = await commands.generateAiSubjectMask( + transformAdjustments, + selectedImage.path, + [startPoint.x, startPoint.y], + [endPoint.x, endPoint.y], + adjustments.rotation, + adjustments.flipHorizontal, + adjustments.flipVertical, + adjustments.orientationSteps, + ); const subMaskToUpdate = adjustments.aiPatches ?.find((p: AiPatch) => p.id === patchId) @@ -843,12 +840,13 @@ function App() { }; const patchDefinitionForBackend = updatedAdjustmentsForBackend.aiPatches.find((p: AiPatch) => p.id === patchId); - const newPatchDataJson: any = await invoke(Invokes.InvokeGenerativeReplaseWithMaskDef, { - currentAdjustments: updatedAdjustmentsForBackend, - patchDefinition: { ...patchDefinitionForBackend, prompt: '' }, - path: selectedImage.path, - useFastInpaint: true, - }); + const newPatchDataJson: any = await commands.invokeGenerativeReplaceWithMaskDef( + selectedImage.path, + { ...patchDefinitionForBackend, prompt: '' }, + updatedAdjustmentsForBackend, + true, + null, + ); const newPatchData = JSON.parse(newPatchDataJson); if (!newPatchData?.color || !newPatchData?.mask) { @@ -958,16 +956,16 @@ function App() { lensTcaEnabled: adjustments.lensTcaEnabled, lensVignetteEnabled: adjustments.lensVignetteEnabled, }; - const newParameters = await invoke(Invokes.GenerateAiSubjectMask, { - jsAdjustments: transformAdjustments, - endPoint: [endPoint.x, endPoint.y], - flipHorizontal: adjustments.flipHorizontal, - flipVertical: adjustments.flipVertical, - orientationSteps: adjustments.orientationSteps, - path: selectedImage.path, - rotation: adjustments.rotation, - startPoint: [startPoint.x, startPoint.y], - }); + const newParameters = await commands.generateAiSubjectMask( + transformAdjustments, + selectedImage.path, + [startPoint.x, startPoint.y], + [endPoint.x, endPoint.y], + adjustments.rotation, + adjustments.flipHorizontal, + adjustments.flipVertical, + adjustments.orientationSteps, + ); const subMask = adjustments.aiPatches ?.flatMap((p: AiPatch) => p.subMasks) @@ -1010,13 +1008,13 @@ function App() { lensTcaEnabled: adjustments.lensTcaEnabled, lensVignetteEnabled: adjustments.lensVignetteEnabled, }; - const newParameters = await invoke(Invokes.GenerateAiForegroundMask, { - jsAdjustments: transformAdjustments, - flipHorizontal: adjustments.flipHorizontal, - flipVertical: adjustments.flipVertical, - orientationSteps: adjustments.orientationSteps, - rotation: adjustments.rotation, - }); + const newParameters = await commands.generateAiForegroundMask( + transformAdjustments, + adjustments.rotation, + adjustments.flipHorizontal, + adjustments.flipVertical, + adjustments.orientationSteps, + ); const subMask = adjustments.aiPatches ?.flatMap((p: AiPatch) => p.subMasks) @@ -1059,13 +1057,13 @@ function App() { lensTcaEnabled: adjustments.lensTcaEnabled, lensVignetteEnabled: adjustments.lensVignetteEnabled, }; - const newParameters = await invoke(Invokes.GenerateAiSkyMask, { - jsAdjustments: transformAdjustments, - flipHorizontal: adjustments.flipHorizontal, - flipVertical: adjustments.flipVertical, - orientationSteps: adjustments.orientationSteps, - rotation: adjustments.rotation, - }); + const newParameters = await commands.generateAiSkyMask( + transformAdjustments, + adjustments.rotation, + adjustments.flipHorizontal, + adjustments.flipVertical, + adjustments.orientationSteps, + ); const subMask = adjustments.aiPatches ?.flatMap((p: AiPatch) => p.subMasks) @@ -1340,10 +1338,14 @@ function App() { } try { +<<<<<<< refactor/tauri-specta + await commands.applyAdjustments(payload, dragging); +======= await invoke(Invokes.ApplyAdjustments, { jsAdjustments: payload, isInteractive: dragging, }); +>>>>>>> main } catch (err) { console.error('Failed to invoke apply_adjustments:', err); } @@ -1363,7 +1365,7 @@ function App() { if (!selectedImage?.isReady) { return; } - invoke(Invokes.GenerateUncroppedPreview, { jsAdjustments: currentAdjustments }).catch((err) => + commands.generateUncroppedPreview(currentAdjustments).catch((err) => console.error('Failed to generate uncropped preview:', err), ); }, 50), @@ -1372,7 +1374,7 @@ function App() { const debouncedSave = useCallback( debounce((path, adjustmentsToSave) => { - invoke(Invokes.SaveMetadataAndUpdateThumbnail, { path, adjustments: adjustmentsToSave }).catch((err) => { + commands.saveMetadataAndUpdateThumbnail(path, adjustmentsToSave).catch((err) => { console.error('Auto-save failed:', err); setError(`Failed to save changes: ${err}`); }); @@ -1422,7 +1424,7 @@ function App() { const handleLutSelect = useCallback( async (path: string) => { try { - const result: LutData = await invoke('load_and_parse_lut', { path }); + const result: LutData = await commands.loadAndParseLut(path); const name = path.split(/[\\/]/).pop() || 'LUT'; setAdjustments((prev: Partial) => ({ ...prev, @@ -1472,7 +1474,7 @@ function App() { const { searchCriteria: _searchCriteria, ...settingsToSave } = newSettings as any; setAppSettings(newSettings); - invoke(Invokes.SaveSettings, { settings: settingsToSave }).catch((err) => { + commands.saveSettings(settingsToSave).catch((err) => { console.error('Failed to save settings:', err); }); }, @@ -1480,7 +1482,7 @@ function App() { ); useEffect(() => { - invoke(Invokes.LoadSettings) + commands.loadSettings() .then(async (settings: any) => { if ( !settings.copyPasteSettings || @@ -1522,7 +1524,7 @@ function App() { } if (settings?.pinnedFolders && settings.pinnedFolders.length > 0) { try { - const trees = await invoke(Invokes.GetPinnedFolderTrees, { paths: settings.pinnedFolders }); + const trees = await commands.getPinnedFolderTrees(settings.pinnedFolders); setPinnedFolderTrees(trees); } catch (err) { console.error('Failed to load pinned folder trees:', err); @@ -1535,18 +1537,30 @@ function App() { const command = settings.libraryViewMode === LibraryViewMode.Recursive - ? Invokes.ListImagesRecursive - : Invokes.ListImagesInDir; + ? 'list_images_recursive' + : 'list_images_in_dir'; preloadedDataRef.current = { rootPath: root, currentPath: currentPath, +<<<<<<< refactor/tauri-specta + tree: commands.getFolderTree(root), + images: + command === 'list_images_recursive' + ? commands.listImagesRecursive(currentPath) + : commands.listImagesInDir(currentPath), + }; + } + + commands.frontendReady().catch(e => console.error("Failed to notify backend of readiness:", e)); +======= tree: invoke(Invokes.GetFolderTree, { path: root }), images: invoke(command, { path: currentPath }), }; } invoke('frontend_ready').catch((e) => console.error('Failed to notify backend of readiness:', e)); +>>>>>>> main }) .catch((err) => { console.error('Failed to load settings:', err); @@ -1598,7 +1612,7 @@ function App() { }, [libraryViewMode, appSettings, handleSettingsChange]); useEffect(() => { - invoke(Invokes.GetSupportedFileTypes) + commands.getSupportedFileTypes() .then((types: any) => setSupportedTypes(types)) .catch((err) => console.error('Failed to load supported file types:', err)); }, []); @@ -1657,7 +1671,7 @@ function App() { }); const isLight = [Theme.Light, Theme.Snow, Theme.Arctic].includes(effectThemeForWindow); - invoke(Invokes.UpdateWindowEffect, { theme: isLight ? Theme.Light : Theme.Dark }); + commands.updateWindowEffect(isLight ? Theme.Light : Theme.Dark); }, [theme, adaptivePalette]); useEffect(() => { @@ -1675,7 +1689,7 @@ function App() { const refreshAllFolderTrees = useCallback(async () => { if (rootPath) { try { - const treeData = await invoke(Invokes.GetFolderTree, { path: rootPath }); + const treeData = await commands.getFolderTree(rootPath); setFolderTree(treeData); } catch (err) { console.error('Failed to refresh main folder tree:', err); @@ -1686,7 +1700,7 @@ function App() { const currentPins = appSettings?.pinnedFolders || []; if (currentPins.length > 0) { try { - const trees = await invoke(Invokes.GetPinnedFolderTrees, { paths: currentPins }); + const trees = await commands.getPinnedFolderTrees(currentPins); setPinnedFolderTrees(trees); } catch (err) { console.error('Failed to refresh pinned folder trees:', err); @@ -1711,6 +1725,15 @@ function App() { handleSettingsChange({ ...appSettings, pinnedFolders: newPins }); +<<<<<<< refactor/tauri-specta + try { + const trees = await commands.getPinnedFolderTrees(newPins); + setPinnedFolderTrees(trees); + } catch (err) { + console.error('Failed to refresh pinned folders:', err); + } + }, [appSettings, handleSettingsChange]); +======= try { const trees = await invoke(Invokes.GetPinnedFolderTrees, { paths: newPins }); setPinnedFolderTrees(trees); @@ -1720,6 +1743,7 @@ function App() { }, [appSettings, handleSettingsChange], ); +>>>>>>> main const handleActiveTreeSectionChange = (section: string | null) => { setActiveTreeSection(section); @@ -1730,7 +1754,7 @@ function App() { const handleSelectSubfolder = useCallback( async (path: string | null, isNewRoot = false, preloadedImages?: ImageFile[]) => { - await invoke('cancel_thumbnail_generation'); + await commands.cancelThumbnailGeneration(); setIsViewLoading(true); setSearchCriteria({ tags: [], text: '', mode: 'OR' }); setLibraryScrollTop(0); @@ -1775,7 +1799,7 @@ function App() { setIsTreeLoading(true); handleSettingsChange({ ...appSettings, lastRootPath: path } as AppSettings); try { - const treeData = await invoke(Invokes.GetFolderTree, { path }); + const treeData = await commands.getFolderTree(path); setFolderTree(treeData); } catch (err) { console.error('Failed to load folder tree:', err); @@ -1797,13 +1821,16 @@ function App() { } const command = - libraryViewMode === LibraryViewMode.Recursive ? Invokes.ListImagesRecursive : Invokes.ListImagesInDir; + libraryViewMode === LibraryViewMode.Recursive ? 'list_images_recursive' : 'list_images_in_dir'; let files: ImageFile[]; if (preloadedImages) { files = preloadedImages; } else { - files = await invoke(command, { path }); + files = + command === 'list_images_recursive' + ? await commands.listImagesRecursive(path) + : await commands.listImagesInDir(path); } const exifSortKeys = ['date_taken', 'iso', 'shutter_speed', 'aperture', 'focal_length']; @@ -1814,7 +1841,7 @@ function App() { const paths = files.map((f: ImageFile) => f.path); if (isExifSortActive) { - const exifDataMap: Record = await invoke(Invokes.ReadExifForPaths, { paths }); + const exifDataMap: Record = await commands.readExifForPaths(paths); const finalImageList = files.map((image) => ({ ...image, exif: exifDataMap[image.path] || image.exif || null, @@ -1822,7 +1849,7 @@ function App() { setImageList(finalImageList); } else { setImageList(files); - invoke(Invokes.ReadExifForPaths, { paths }) + commands.readExifForPaths(paths) .then((exifDataMap: any) => { setImageList((currentImageList) => currentImageList.map((image) => ({ @@ -1839,7 +1866,7 @@ function App() { setImageList(files); } - invoke(Invokes.StartBackgroundIndexing, { folderPath: path }).catch((err) => { + commands.startBackgroundIndexing(path).catch((err) => { console.error('Failed to start background indexing:', err); }); } catch (err) { @@ -1861,9 +1888,12 @@ function App() { if (!currentFolderPath) return; try { const command = - libraryViewMode === LibraryViewMode.Recursive ? Invokes.ListImagesRecursive : Invokes.ListImagesInDir; + libraryViewMode === LibraryViewMode.Recursive ? 'list_images_recursive' : 'list_images_in_dir'; - const files: ImageFile[] = await invoke(command, { path: currentFolderPath }); + const files: ImageFile[] = + command === 'list_images_recursive' + ? await commands.listImagesRecursive(currentFolderPath) + : await commands.listImagesInDir(currentFolderPath); const exifSortKeys = ['date_taken', 'iso', 'shutter_speed', 'aperture', 'focal_length']; const isExifSortActive = exifSortKeys.includes(sortCriteria.key); const shouldReadExif = appSettings?.enableExifReading ?? false; @@ -1872,7 +1902,7 @@ function App() { if (shouldReadExif && files.length > 0 && isExifSortActive) { const paths = files.map((f: ImageFile) => f.path); - freshExifData = await invoke(Invokes.ReadExifForPaths, { paths }); + freshExifData = await commands.readExifForPaths(paths); } setImageList((prevList) => { @@ -1894,7 +1924,7 @@ function App() { if (shouldReadExif && files.length > 0 && !isExifSortActive) { const paths = files.map((f: ImageFile) => f.path); - invoke(Invokes.ReadExifForPaths, { paths }) + commands.readExifForPaths(paths) .then((exifDataMap: any) => { setImageList((currentImageList) => currentImageList.map((image) => { @@ -2073,7 +2103,11 @@ function App() { try { const command = options.includeAssociated ? 'delete_files_with_associated' : 'delete_files_from_disk'; - await invoke(command, { paths: pathsToDelete }); + if (command === 'delete_files_with_associated') { + await commands.deleteFilesWithAssociated(pathsToDelete); + } else { + await commands.deleteFilesFromDisk(pathsToDelete); + } await refreshImageList(); @@ -2208,7 +2242,7 @@ function App() { setAdjustments(newAdjustments); } - invoke(Invokes.ApplyAdjustmentsToPaths, { paths: pathsToUpdate, adjustments: adjustmentsToApply }).catch( + commands.applyAdjustmentsToPaths(pathsToUpdate, adjustmentsToApply).catch( (err) => { console.error('Failed to paste adjustments to multiple images:', err); setError(`Failed to paste adjustments: ${err}`); @@ -2224,7 +2258,7 @@ function App() { return; } try { - const autoAdjustments: Adjustments = await invoke(Invokes.CalculateAutoAdjustments); + const autoAdjustments: Adjustments = await commands.calculateAutoAdjustments(); setAdjustments((prev: Adjustments) => { const newAdjustments = { ...prev, ...autoAdjustments }; newAdjustments.sectionVisibility = { @@ -2273,7 +2307,7 @@ function App() { setLibraryActiveAdjustments((prev) => ({ ...prev, rating: finalRating })); } - invoke(Invokes.ApplyAdjustmentsToPaths, { paths: pathsToRate, adjustments: { rating: finalRating } }).catch( + commands.applyAdjustmentsToPaths(pathsToRate, { rating: finalRating }).catch( (err) => { console.error('Failed to apply rating to paths:', err); setError(`Failed to apply rating: ${err}`); @@ -2308,7 +2342,7 @@ function App() { } const finalColor = color !== null && color === currentColor ? null : color; try { - await invoke(Invokes.SetColorLabelForPaths, { paths: pathsToUpdate, color: finalColor }); + await commands.setColorLabelForPaths(pathsToUpdate, finalColor); setImageList((prevList: Array) => prevList.map((image: ImageFile) => { @@ -2381,9 +2415,9 @@ function App() { } try { if (mode === 'copy') - await invoke(Invokes.CopyFiles, { sourcePaths: copiedFilePaths, destinationFolder: currentFolderPath }); + await commands.copyFiles(copiedFilePaths, currentFolderPath); else { - await invoke(Invokes.MoveFiles, { sourcePaths: copiedFilePaths, destinationFolder: currentFolderPath }); + await commands.moveFiles(copiedFilePaths, currentFolderPath); setCopiedFilePaths([]); } await refreshImageList(); @@ -2405,9 +2439,7 @@ function App() { const request = { cancelled: false }; fullResRequestRef.current = request; - invoke(Invokes.GenerateFullscreenPreview, { - jsAdjustments: currentAdjustments, - }) + commands.generateFullscreenPreview(currentAdjustments,) .then(() => { fullResCacheKeyRef.current = key; if (!request.cancelled) { @@ -2599,7 +2631,7 @@ function App() { useEffect(() => { let isEffectActive = true; const listeners = [ - listen('preview-update-final', (event: any) => { + events.previewUpdateFinal.listen( (event: any) => { if (isEffectActive) { const { path, data } = event.payload; if (path !== selectedImagePathRef.current) return; @@ -2609,7 +2641,7 @@ function App() { setFinalPreviewUrl(url); } }), - listen('preview-update-uncropped', (event: any) => { + events.previewUpdateUncropped.listen( (event: any) => { if (isEffectActive) { const imageData = new Uint8Array(event.payload); const blob = new Blob([imageData], { type: 'image/jpeg' }); @@ -2617,22 +2649,22 @@ function App() { setUncroppedAdjustedPreviewUrl(url); } }), - listen('histogram-update', (event: any) => { + events.histogramUpdate.listen( (event: any) => { if (isEffectActive) { setHistogram(event.payload); } }), - listen('open-with-file', (event: any) => { + events.openWithFile.listen( (event: any) => { if (isEffectActive) { setInitialFileToOpen(event.payload as string); } }), - listen('waveform-update', (event: any) => { + events.waveformUpdate.listen( (event: any) => { if (isEffectActive) { setWaveform(event.payload); } }), - listen('thumbnail-generated', (event: any) => { + events.thumbnailGenerated.listen( (event: any) => { if (isEffectActive) { const { path, data, rating } = event.payload; if (data) { @@ -2643,35 +2675,42 @@ function App() { } } }), - listen('ai-model-download-start', (event: any) => { + events.aiModelDownloadStart.listen( (event: any) => { if (isEffectActive) { setAiModelDownloadStatus(event.payload); } }), - listen('ai-model-download-finish', () => { + events.aiModelDownloadFinish.listen( () => { if (isEffectActive) { setAiModelDownloadStatus(null); } }), - listen('indexing-started', () => { + events.indexingStarted.listen( () => { if (isEffectActive) { setIsIndexing(true); setIndexingProgress({ current: 0, total: 0 }); } }), - listen('indexing-progress', (event: any) => { + events.indexingProgress.listen( (event: any) => { if (isEffectActive) { setIndexingProgress(event.payload); } }), - listen('indexing-finished', () => { + events.indexingError.listen( (event: any) => { + if (isEffectActive) { + setIsIndexing(false); + setIndexingProgress({ current: 0, total: 0 }); + setError(typeof event.payload === 'string' ? event.payload : 'Indexing error'); + } + }), + events.indexingFinished.listen( () => { if (isEffectActive) { setIsIndexing(false); setIndexingProgress({ current: 0, total: 0 }); if (currentFolderPathRef.current) { const refreshImageList = async () => { try { - const list: ImageFile[] = await invoke(Invokes.ListImagesInDir, { path: currentFolderPathRef.current }); + const list: ImageFile[] = await commands.listImagesInDir(currentFolderPathRef.current); if (Array.isArray(list)) { setImageList(list); } @@ -2683,17 +2722,28 @@ function App() { } } }), - listen('batch-export-progress', (event: any) => { + events.batchExportProgress.listen( (event: any) => { if (isEffectActive) { setExportState((prev: ExportState) => ({ ...prev, progress: event.payload })); } }), - listen('export-complete', () => { + events.exportComplete.listen( () => { if (isEffectActive) { setExportState((prev: ExportState) => ({ ...prev, status: Status.Success })); } }), - listen('export-error', (event) => { + events.exportCompleteWithErrors.listen( (event: any) => { + if (isEffectActive) { + const errors = event.payload?.errors ?? 0; + const total = event.payload?.total ?? 0; + setExportState((prev: ExportState) => ({ + ...prev, + status: Status.Error, + errorMessage: `Export completed with errors (${errors}/${total}).`, + })); + } + }), + events.exportError.listen( (event) => { if (isEffectActive) { setExportState((prev: ExportState) => ({ ...prev, @@ -2702,12 +2752,12 @@ function App() { })); } }), - listen('export-cancelled', () => { + events.exportCancelled.listen( () => { if (isEffectActive) { setExportState((prev: ExportState) => ({ ...prev, status: Status.Cancelled })); } }), - listen('import-start', (event: any) => { + events.importStart.listen( (event: any) => { if (isEffectActive) { setImportState({ errorMessage: '', @@ -2717,7 +2767,7 @@ function App() { }); } }), - listen('import-progress', (event: any) => { + events.importProgress.listen( (event: any) => { if (isEffectActive) { setImportState((prev: ImportState) => ({ ...prev, @@ -2726,7 +2776,7 @@ function App() { })); } }), - listen('import-complete', () => { + events.importComplete.listen( () => { if (isEffectActive) { setImportState((prev: ImportState) => ({ ...prev, status: Status.Success })); refreshAllFolderTrees(); @@ -2735,7 +2785,7 @@ function App() { } } }), - listen('import-error', (event) => { + events.importError.listen( (event) => { if (isEffectActive) { setImportState((prev: ImportState) => ({ ...prev, @@ -2744,12 +2794,20 @@ function App() { })); } }), - listen('denoise-progress', (event: any) => { + events.thumbnailGenerationError.listen( (event: any) => { + if (isEffectActive) { + const payload = event.payload; + if (payload?.reason) { + setError(`Thumbnail generation error: ${payload.reason}`); + } + } + }), + events.denoiseProgress.listen( (event: any) => { if (isEffectActive) { setDenoiseModalState((prev) => ({ ...prev, progressMessage: event.payload as string })); } }), - listen('denoise-complete', (event: any) => { + events.denoiseComplete.listen( (event: any) => { if (isEffectActive) { const payload = event.payload; const isObject = typeof payload === 'object' && payload !== null; @@ -2763,7 +2821,7 @@ function App() { })); } }), - listen('denoise-error', (event: any) => { + events.denoiseError.listen( (event: any) => { if (isEffectActive) { setDenoiseModalState((prev) => ({ ...prev, @@ -2803,7 +2861,7 @@ function App() { useEffect(() => { if (libraryActivePath) { - invoke(Invokes.LoadMetadata, { path: libraryActivePath }) + commands.loadMetadata(libraryActivePath) .then((metadata: any) => { if (metadata.adjustments && !metadata.adjustments.is_null) { const normalized: Adjustments = normalizeLoadedAdjustments(metadata.adjustments); @@ -2824,7 +2882,7 @@ function App() { useEffect(() => { let isEffectActive = true; - const unlistenProgress = listen('panorama-progress', (event: any) => { + const unlistenProgress = events.panoramaProgress.listen( (event: any) => { if (isEffectActive) { setPanoramaModalState((prev: PanoramaModalState) => ({ ...prev, @@ -2836,7 +2894,7 @@ function App() { } }); - const unlistenComplete = listen('panorama-complete', (event: any) => { + const unlistenComplete = events.panoramaComplete.listen( (event: any) => { if (isEffectActive) { const { base64 } = event.payload; setPanoramaModalState((prev: PanoramaModalState) => ({ @@ -2848,7 +2906,7 @@ function App() { } }); - const unlistenError = listen('panorama-error', (event: any) => { + const unlistenError = events.panoramaError.listen( (event: any) => { if (isEffectActive) { setPanoramaModalState((prev: PanoramaModalState) => ({ ...prev, @@ -2859,18 +2917,29 @@ function App() { } }); + const unlistenWarning = events.panoramaWarning.listen( (event: any) => { + if (isEffectActive) { + const warning = String(event.payload); + setPanoramaModalState((prev: PanoramaModalState) => ({ + ...prev, + progressMessage: warning, + })); + } + }); + return () => { isEffectActive = false; unlistenProgress.then((f: any) => f()); unlistenComplete.then((f: any) => f()); unlistenError.then((f: any) => f()); + unlistenWarning.then((f: any) => f()); }; }, []); useEffect(() => { let isEffectActive = true; - const unlistenProgress = listen('hdr-progress', (event: any) => { + const unlistenProgress = events.hdrProgress.listen( (event: any) => { if (isEffectActive) { setHdrModalState((prev: HdrModalState) => ({ ...prev, @@ -2882,7 +2951,7 @@ function App() { } }); - const unlistenComplete = listen('hdr-complete', (event: any) => { + const unlistenComplete = events.hdrComplete.listen( (event: any) => { if (isEffectActive) { const { base64 } = event.payload; setHdrModalState((prev: HdrModalState) => ({ @@ -2894,7 +2963,7 @@ function App() { } }); - const unlistenError = listen('hdr-error', (event: any) => { + const unlistenError = events.hdrError.listen( (event: any) => { if (isEffectActive) { setHdrModalState((prev: HdrModalState) => ({ ...prev, @@ -2916,7 +2985,7 @@ function App() { useEffect(() => { let isEffectActive = true; - const unlistenStart = listen('culling-start', (event: any) => { + const unlistenStart = events.cullingStart.listen( (event: any) => { if (isEffectActive) { setCullingModalState({ isOpen: true, @@ -2927,30 +2996,23 @@ function App() { } }); - const unlistenProgress = listen('culling-progress', (event: any) => { + const unlistenProgress = events.cullingProgress.listen( (event: any) => { if (isEffectActive) { setCullingModalState((prev) => ({ ...prev, progress: event.payload })); } }); - const unlistenComplete = listen('culling-complete', (event: any) => { + const unlistenComplete = events.cullingComplete.listen( (event: any) => { if (isEffectActive) { setCullingModalState((prev) => ({ ...prev, progress: null, suggestions: event.payload })); } }); - const unlistenError = listen('culling-error', (event: any) => { - if (isEffectActive) { - setCullingModalState((prev) => ({ ...prev, progress: null, error: String(event.payload) })); - } - }); - return () => { isEffectActive = false; unlistenStart.then((f) => f()); unlistenProgress.then((f) => f()); unlistenComplete.then((f) => f()); - unlistenError.then((f) => f()); }; }, []); @@ -2962,9 +3024,7 @@ function App() { } try { - const savedPath: string = await invoke(Invokes.SavePanorama, { - firstPathStr: panoramaModalState.stitchingSourcePaths[0], - }); + const savedPath: string = await commands.savePanorama(panoramaModalState.stitchingSourcePaths[0],); await refreshImageList(); return savedPath; } catch (err) { @@ -2982,9 +3042,7 @@ function App() { } try { - const savedPath: string = await invoke(Invokes.SaveHdr, { - firstPathStr: hdrModalState.stitchingSourcePaths[0], - }); + const savedPath: string = await commands.saveHdr(hdrModalState.stitchingSourcePaths[0],); await refreshImageList(); return savedPath; } catch (err) { @@ -3005,6 +3063,15 @@ function App() { progressMessage: 'Starting engine...', })); +<<<<<<< refactor/tauri-specta + try { + await commands.applyDenoising(denoiseModalState.targetPath, intensity); + } catch (err) { + setDenoiseModalState(prev => ({ + ...prev, + isProcessing: false, + error: String(err) +======= try { await invoke(Invokes.ApplyDenoising, { path: denoiseModalState.targetPath, @@ -3015,6 +3082,7 @@ function App() { ...prev, isProcessing: false, error: String(err), +>>>>>>> main })); } }, @@ -3022,20 +3090,22 @@ function App() { ); const handleSaveDenoisedImage = async (): Promise => { +<<<<<<< refactor/tauri-specta + if (!denoiseModalState.targetPath) throw new Error("No target path"); + const savedPath = await commands.saveDenoisedImage(denoiseModalState.targetPath); +======= if (!denoiseModalState.targetPath) throw new Error('No target path'); const savedPath = await invoke(Invokes.SaveDenoisedImage, { originalPathStr: denoiseModalState.targetPath, }); +>>>>>>> main await refreshImageList(); return savedPath; }; const handleSaveCollage = async (base64Data: string, firstPath: string): Promise => { try { - const savedPath: string = await invoke(Invokes.SaveCollage, { - base64Data, - firstPathStr: firstPath, - }); + const savedPath: string = await commands.saveCollage(base64Data, firstPath); await refreshImageList(); return savedPath; } catch (err) { @@ -3166,7 +3236,11 @@ function App() { treeData = await preloadedDataRef.current.tree; console.log('Preload cache hit for folder tree.'); } else { +<<<<<<< refactor/tauri-specta + treeData = await commands.getFolderTree(root); +======= treeData = await invoke(Invokes.GetFolderTree, { path: root }); +>>>>>>> main } setFolderTree(treeData); } catch (err) { @@ -3306,7 +3380,7 @@ function App() { useEffect(() => { const invokeWaveForm = async () => { - const waveForm: any = await invoke(Invokes.GenerateWaveform).catch((err) => + const waveForm: any = await commands.generateWaveform().catch((err) => console.error('Failed to generate waveform:', err), ); if (waveForm) { @@ -3325,7 +3399,7 @@ function App() { const loadMetadataEarly = async () => { try { - const metadata: any = await invoke(Invokes.LoadMetadata, { path: selectedImage.path }); + const metadata: any = await commands.loadMetadata(selectedImage.path); if (!isEffectActive) return; let initialAdjusts; @@ -3346,7 +3420,7 @@ function App() { const loadFullImageData = async () => { try { - const loadImageResult: any = await invoke(Invokes.LoadImage, { path: selectedImage.path }); + const loadImageResult: any = await commands.loadImage(selectedImage.path); if (!isEffectActive) { return; } @@ -3437,10 +3511,7 @@ function App() { async (nameTemplate: string) => { if (renameTargetPaths.length > 0 && nameTemplate) { try { - const newPaths: Array = await invoke(Invokes.RenameFiles, { - nameTemplate, - paths: renameTargetPaths, - }); + const newPaths: Array = await commands.renameFiles(renameTargetPaths, nameTemplate); await refreshImageList(); @@ -3477,11 +3548,7 @@ function App() { const handleStartImport = async (settings: AppSettings) => { if (importSourcePaths.length > 0 && importTargetFolder) { - invoke(Invokes.ImportFiles, { - destinationFolder: importTargetFolder, - settings: settings, - sourcePaths: importSourcePaths, - }).catch((err) => { + commands.importFiles(importSourcePaths, importTargetFolder, settings).catch((err) => { console.error('Failed to start import:', err); setImportState({ status: Status.Error, errorMessage: `Failed to start import: ${err}` }); }); @@ -3497,7 +3564,7 @@ function App() { debouncedSetHistory.cancel(); - invoke(Invokes.ResetAdjustmentsForPaths, { paths: pathsToReset }) + commands.resetAdjustmentsForPaths(pathsToReset) .then(() => { if (libraryActivePath && pathsToReset.includes(libraryActivePath)) { setLibraryActiveAdjustments((prev: Adjustments) => ({ ...INITIAL_ADJUSTMENTS, rating: prev.rating })); @@ -3587,7 +3654,7 @@ function App() { const handleCreateVirtualCopy = async (sourcePath: string) => { try { - await invoke(Invokes.CreateVirtualCopy, { sourceVirtualPath: sourcePath }); + await commands.createVirtualCopy(sourcePath); await refreshImageList(); } catch (err) { console.error('Failed to create virtual copy:', err); @@ -3838,7 +3905,7 @@ function App() { const handleCreateVirtualCopy = async (sourcePath: string) => { try { - await invoke(Invokes.CreateVirtualCopy, { sourceVirtualPath: sourcePath }); + await commands.createVirtualCopy(sourcePath); await refreshImageList(); } catch (err) { console.error('Failed to create virtual copy:', err); @@ -3849,12 +3916,10 @@ function App() { const handleApplyAutoAdjustmentsToSelection = () => { if (finalSelection.length === 0) return; - invoke(Invokes.ApplyAutoAdjustmentsToPaths, { paths: finalSelection }) + commands.applyAutoAdjustmentsToPaths(finalSelection) .then(async () => { if (selectedImage && finalSelection.includes(selectedImage.path)) { - const metadata: Metadata = await invoke(Invokes.LoadMetadata, { - path: selectedImage.path, - }); + const metadata: Metadata = await commands.loadMetadata(selectedImage.path,); if (metadata.adjustments && !metadata.adjustments.is_null) { const normalized = normalizeLoadedAdjustments(metadata.adjustments); setLiveAdjustments(normalized); @@ -3862,9 +3927,7 @@ function App() { } } if (libraryActivePath && finalSelection.includes(libraryActivePath)) { - const metadata: Metadata = await invoke(Invokes.LoadMetadata, { - path: libraryActivePath, - }); + const metadata: Metadata = await commands.loadMetadata(libraryActivePath,); if (metadata.adjustments && !metadata.adjustments.is_null) { const normalized = normalizeLoadedAdjustments(metadata.adjustments); setLibraryActiveAdjustments(normalized); @@ -3919,7 +3982,7 @@ function App() { label: 'Copy Adjustments', onClick: async () => { try { - const metadata: any = await invoke(Invokes.LoadMetadata, { path: finalSelection[0] }); + const metadata: any = await commands.loadMetadata(finalSelection[0]); const sourceAdjustments = metadata.adjustments && !metadata.adjustments.is_null ? { ...INITIAL_ADJUSTMENTS, ...metadata.adjustments } @@ -3997,7 +4060,7 @@ function App() { progressMessage: 'Starting panorama process...', stitchingSourcePaths: finalSelection, }); - invoke(Invokes.StitchPanorama, { paths: finalSelection }).catch((err) => { + commands.stitchPanorama(finalSelection).catch((err) => { setPanoramaModalState((prev: PanoramaModalState) => ({ ...prev, error: String(err), @@ -4019,7 +4082,7 @@ function App() { progressMessage: 'Starting hdr process...', stitchingSourcePaths: finalSelection, }); - invoke(Invokes.MergeHdr, { paths: finalSelection }).catch((err) => { + commands.mergeHdr(finalSelection).catch((err) => { setHdrModalState((prev: HdrModalState) => ({ ...prev, error: String(err), @@ -4071,7 +4134,7 @@ function App() { label: 'Duplicate Image', onClick: async () => { try { - await invoke(Invokes.DuplicateFile, { path: finalSelection[0] }); + await commands.duplicateFile(finalSelection[0]); await refreshImageList(); } catch (err) { console.error('Failed to duplicate file:', err); @@ -4122,7 +4185,7 @@ function App() { icon: Folder, label: 'Show in File Explorer', onClick: () => { - invoke(Invokes.ShowInFinder, { path: finalSelection[0] }).catch((err) => + commands.showInFinder(finalSelection[0]).catch((err) => setError(`Could not show file in explorer: ${err}`), ); }, @@ -4136,7 +4199,7 @@ function App() { const handleCreateFolder = async (folderName: string) => { if (folderName && folderName.trim() !== '' && folderActionTarget) { try { - await invoke(Invokes.CreateFolder, { path: `${folderActionTarget}/${folderName.trim()}` }); + await commands.createFolder(`${folderActionTarget}/${folderName.trim()}`); refreshAllFolderTrees(); } catch (err) { setError(`Failed to create folder: ${err}`); @@ -4150,7 +4213,7 @@ function App() { const oldPath = folderActionTarget; const trimmedNewName = newName.trim(); - await invoke(Invokes.RenameFolder, { path: oldPath, newName: trimmedNewName }); + await commands.renameFolder(oldPath, trimmedNewName); const parentDir = getParentDir(oldPath); const separator = oldPath.includes('/') ? '/' : '\\'; @@ -4241,7 +4304,7 @@ function App() { label: copyPastedLabel, onClick: async () => { try { - await invoke(Invokes.CopyFiles, { sourcePaths: copiedFilePaths, destinationFolder: targetPath }); + await commands.copyFiles(copiedFilePaths, targetPath); if (targetPath === currentFolderPath) handleLibraryRefresh(); } catch (err) { setError(`Failed to copy files: ${err}`); @@ -4252,7 +4315,7 @@ function App() { label: movePastedLabel, onClick: async () => { try { - await invoke(Invokes.MoveFiles, { sourcePaths: copiedFilePaths, destinationFolder: targetPath }); + await commands.moveFiles(copiedFilePaths, targetPath); setCopiedFilePaths([]); setMultiSelectedPaths([]); refreshAllFolderTrees(); @@ -4270,7 +4333,7 @@ function App() { icon: Folder, label: 'Show in File Explorer', onClick: () => - invoke(Invokes.ShowInFinder, { path: targetPath }).catch((err) => setError(`Could not show folder: ${err}`)), + commands.showInFinder(targetPath).catch((err) => setError(`Could not show folder: ${err}`)), }, ...(path ? [ @@ -4287,7 +4350,7 @@ function App() { isDestructive: true, onClick: async () => { try { - await invoke(Invokes.DeleteFolder, { path: targetPath }); + await commands.deleteFolder(targetPath); if (currentFolderPath?.startsWith(targetPath)) await handleSelectSubfolder(rootPath); refreshAllFolderTrees(); } catch (err) { @@ -4320,7 +4383,7 @@ function App() { label: copyPastedLabel, onClick: async () => { try { - await invoke(Invokes.CopyFiles, { sourcePaths: copiedFilePaths, destinationFolder: currentFolderPath }); + await commands.copyFiles(copiedFilePaths, currentFolderPath); handleLibraryRefresh(); } catch (err) { setError(`Failed to copy files: ${err}`); @@ -4331,7 +4394,7 @@ function App() { label: movePastedLabel, onClick: async () => { try { - await invoke(Invokes.MoveFiles, { sourcePaths: copiedFilePaths, destinationFolder: currentFolderPath }); + await commands.moveFiles(copiedFilePaths, currentFolderPath); setCopiedFilePaths([]); setMultiSelectedPaths([]); refreshAllFolderTrees(); diff --git a/src/bindings.ts b/src/bindings.ts new file mode 100644 index 000000000..1bb8209af --- /dev/null +++ b/src/bindings.ts @@ -0,0 +1,505 @@ + +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +/** user-defined commands **/ + + +export const commands = { +async loadImage(path: string) : Promise { + return await TAURI_INVOKE("load_image", { path }); +}, +async applyAdjustments(jsAdjustments: JsonValue, isInteractive: boolean) : Promise { + return await TAURI_INVOKE("apply_adjustments", { jsAdjustments, isInteractive }); +}, +async exportImage(originalPath: string, outputPath: string, jsAdjustments: JsonValue, exportSettings: ExportSettings) : Promise { + return await TAURI_INVOKE("export_image", { originalPath, outputPath, jsAdjustments, exportSettings }); +}, +async batchExportImages(outputFolder: string, paths: string[], exportSettings: ExportSettings, outputFormat: string) : Promise { + return await TAURI_INVOKE("batch_export_images", { outputFolder, paths, exportSettings, outputFormat }); +}, +async cancelExport() : Promise { + return await TAURI_INVOKE("cancel_export"); +}, +async estimateExportSize(jsAdjustments: JsonValue, exportSettings: ExportSettings, outputFormat: string) : Promise { + return await TAURI_INVOKE("estimate_export_size", { jsAdjustments, exportSettings, outputFormat }); +}, +async estimateBatchExportSize(paths: string[], exportSettings: ExportSettings, outputFormat: string) : Promise { + return await TAURI_INVOKE("estimate_batch_export_size", { paths, exportSettings, outputFormat }); +}, +async generateFullscreenPreview(jsAdjustments: JsonValue) : Promise { + return await TAURI_INVOKE("generate_fullscreen_preview", { jsAdjustments }); +}, +async generatePreviewForPath(path: string, jsAdjustments: JsonValue) : Promise { + return await TAURI_INVOKE("generate_preview_for_path", { path, jsAdjustments }); +}, +async generateOriginalTransformedPreview(jsAdjustments: JsonValue) : Promise { + return await TAURI_INVOKE("generate_original_transformed_preview", { jsAdjustments }); +}, +async generatePresetPreview(jsAdjustments: JsonValue) : Promise { + return await TAURI_INVOKE("generate_preset_preview", { jsAdjustments }); +}, +async generateUncroppedPreview(jsAdjustments: JsonValue) : Promise { + return await TAURI_INVOKE("generate_uncropped_preview", { jsAdjustments }); +}, +async previewGeometryTransform(params: GeometryParams, jsAdjustments: JsonValue, showLines: boolean) : Promise { + return await TAURI_INVOKE("preview_geometry_transform", { params, jsAdjustments, showLines }); +}, +async generateMaskOverlay(maskDef: MaskDefinition, width: number, height: number, scale: number, cropOffset: [number, number]) : Promise { + return await TAURI_INVOKE("generate_mask_overlay", { maskDef, width, height, scale, cropOffset }); +}, +async generateAiSubjectMask(jsAdjustments: JsonValue, path: string, startPoint: [number, number], endPoint: [number, number], rotation: number, flipHorizontal: boolean, flipVertical: boolean, orientationSteps: number) : Promise { + return await TAURI_INVOKE("generate_ai_subject_mask", { jsAdjustments, path, startPoint, endPoint, rotation, flipHorizontal, flipVertical, orientationSteps }); +}, +async generateAiForegroundMask(jsAdjustments: JsonValue, rotation: number, flipHorizontal: boolean, flipVertical: boolean, orientationSteps: number) : Promise { + return await TAURI_INVOKE("generate_ai_foreground_mask", { jsAdjustments, rotation, flipHorizontal, flipVertical, orientationSteps }); +}, +async generateAiSkyMask(jsAdjustments: JsonValue, rotation: number, flipHorizontal: boolean, flipVertical: boolean, orientationSteps: number) : Promise { + return await TAURI_INVOKE("generate_ai_sky_mask", { jsAdjustments, rotation, flipHorizontal, flipVertical, orientationSteps }); +}, +async updateWindowEffect(theme: string) : Promise { + await TAURI_INVOKE("update_window_effect", { theme }); +}, +async checkAiConnectorStatus() : Promise { + await TAURI_INVOKE("check_ai_connector_status"); +}, +async testAiConnectorConnection(address: string) : Promise { + return await TAURI_INVOKE("test_ai_connector_connection", { address }); +}, +async invokeGenerativeReplaceWithMaskDef(path: string, patchDefinition: AiPatchDefinition, currentAdjustments: JsonValue, useFastInpaint: boolean, token: string | null) : Promise { + return await TAURI_INVOKE("invoke_generative_replace_with_mask_def", { path, patchDefinition, currentAdjustments, useFastInpaint, token }); +}, +async getSupportedFileTypes() : Promise { + return await TAURI_INVOKE("get_supported_file_types"); +}, +async getLogFilePath() : Promise { + return await TAURI_INVOKE("get_log_file_path"); +}, +async frontendLog(level: string, message: string) : Promise { + return await TAURI_INVOKE("frontend_log", { level, message }); +}, +async saveCollage(base64Data: string, firstPathStr: string) : Promise { + return await TAURI_INVOKE("save_collage", { base64Data, firstPathStr }); +}, +async stitchPanorama(paths: string[]) : Promise { + return await TAURI_INVOKE("stitch_panorama", { paths }); +}, +async savePanorama(firstPathStr: string) : Promise { + return await TAURI_INVOKE("save_panorama", { firstPathStr }); +}, +async mergeHdr(paths: string[]) : Promise { + return await TAURI_INVOKE("merge_hdr", { paths }); +}, +async saveHdr(firstPathStr: string) : Promise { + return await TAURI_INVOKE("save_hdr", { firstPathStr }); +}, +async applyDenoising(path: string, intensity: number) : Promise { + return await TAURI_INVOKE("apply_denoising", { path, intensity }); +}, +async saveDenoisedImage(originalPathStr: string) : Promise { + return await TAURI_INVOKE("save_denoised_image", { originalPathStr }); +}, +async loadAndParseLut(path: string) : Promise { + return await TAURI_INVOKE("load_and_parse_lut", { path }); +}, +async fetchCommunityPresets() : Promise { + return await TAURI_INVOKE("fetch_community_presets"); +}, +async generateAllCommunityPreviews(imagePaths: string[], presets: CommunityPreset[]) : Promise> { + return await TAURI_INVOKE("generate_all_community_previews", { imagePaths, presets }); +}, +async saveTempFile(bytes: number[]) : Promise { + return await TAURI_INVOKE("save_temp_file", { bytes }); +}, +async getImageDimensions(path: string) : Promise { + return await TAURI_INVOKE("get_image_dimensions", { path }); +}, +async frontendReady() : Promise { + return await TAURI_INVOKE("frontend_ready"); +}, +async cancelThumbnailGeneration() : Promise { + return await TAURI_INVOKE("cancel_thumbnail_generation"); +}, +async generateHistogram() : Promise { + return await TAURI_INVOKE("generate_histogram"); +}, +async generateWaveform() : Promise { + return await TAURI_INVOKE("generate_waveform"); +}, +async calculateAutoAdjustments() : Promise { + return await TAURI_INVOKE("calculate_auto_adjustments"); +}, +async readExifForPaths(paths: string[]) : Promise }>> { + return await TAURI_INVOKE("read_exif_for_paths", { paths }); +}, +async listImagesInDir(path: string) : Promise { + return await TAURI_INVOKE("list_images_in_dir", { path }); +}, +async listImagesRecursive(path: string) : Promise { + return await TAURI_INVOKE("list_images_recursive", { path }); +}, +async getFolderTree(path: string) : Promise { + return await TAURI_INVOKE("get_folder_tree", { path }); +}, +async getPinnedFolderTrees(paths: string[]) : Promise { + return await TAURI_INVOKE("get_pinned_folder_trees", { paths }); +}, +async generateThumbnails(paths: string[]) : Promise> { + return await TAURI_INVOKE("generate_thumbnails", { paths }); +}, +async generateThumbnailsProgressive(paths: string[]) : Promise { + return await TAURI_INVOKE("generate_thumbnails_progressive", { paths }); +}, +async createFolder(path: string) : Promise { + return await TAURI_INVOKE("create_folder", { path }); +}, +async deleteFolder(path: string) : Promise { + return await TAURI_INVOKE("delete_folder", { path }); +}, +async copyFiles(sourcePaths: string[], destinationFolder: string) : Promise { + return await TAURI_INVOKE("copy_files", { sourcePaths, destinationFolder }); +}, +async moveFiles(sourcePaths: string[], destinationFolder: string) : Promise { + return await TAURI_INVOKE("move_files", { sourcePaths, destinationFolder }); +}, +async renameFolder(path: string, newName: string) : Promise { + return await TAURI_INVOKE("rename_folder", { path, newName }); +}, +async renameFiles(paths: string[], nameTemplate: string) : Promise { + return await TAURI_INVOKE("rename_files", { paths, nameTemplate }); +}, +async duplicateFile(path: string) : Promise { + return await TAURI_INVOKE("duplicate_file", { path }); +}, +async showInFinder(path: string) : Promise { + return await TAURI_INVOKE("show_in_finder", { path }); +}, +async deleteFilesFromDisk(paths: string[]) : Promise { + return await TAURI_INVOKE("delete_files_from_disk", { paths }); +}, +async deleteFilesWithAssociated(paths: string[]) : Promise { + return await TAURI_INVOKE("delete_files_with_associated", { paths }); +}, +async saveMetadataAndUpdateThumbnail(path: string, adjustments: JsonValue) : Promise { + return await TAURI_INVOKE("save_metadata_and_update_thumbnail", { path, adjustments }); +}, +async applyAdjustmentsToPaths(paths: string[], adjustments: JsonValue) : Promise { + return await TAURI_INVOKE("apply_adjustments_to_paths", { paths, adjustments }); +}, +async loadMetadata(path: string) : Promise { + return await TAURI_INVOKE("load_metadata", { path }); +}, +async loadPresets() : Promise { + return await TAURI_INVOKE("load_presets"); +}, +async savePresets(presets: PresetItem[]) : Promise { + return await TAURI_INVOKE("save_presets", { presets }); +}, +async loadSettings() : Promise { + return await TAURI_INVOKE("load_settings"); +}, +async saveSettings(settings: AppSettings) : Promise { + return await TAURI_INVOKE("save_settings", { settings }); +}, +async resetAdjustmentsForPaths(paths: string[]) : Promise { + return await TAURI_INVOKE("reset_adjustments_for_paths", { paths }); +}, +async applyAutoAdjustmentsToPaths(paths: string[]) : Promise { + return await TAURI_INVOKE("apply_auto_adjustments_to_paths", { paths }); +}, +async handleImportPresetsFromFile(filePath: string) : Promise { + return await TAURI_INVOKE("handle_import_presets_from_file", { filePath }); +}, +async handleImportLegacyPresetsFromFile(filePath: string) : Promise { + return await TAURI_INVOKE("handle_import_legacy_presets_from_file", { filePath }); +}, +async handleExportPresetsToFile(presetsToExport: PresetItem[], filePath: string) : Promise { + return await TAURI_INVOKE("handle_export_presets_to_file", { presetsToExport, filePath }); +}, +async saveCommunityPreset(name: string, adjustments: JsonValue) : Promise { + return await TAURI_INVOKE("save_community_preset", { name, adjustments }); +}, +async clearAllSidecars(rootPath: string) : Promise { + return await TAURI_INVOKE("clear_all_sidecars", { rootPath }); +}, +async clearThumbnailCache() : Promise { + return await TAURI_INVOKE("clear_thumbnail_cache"); +}, +async setColorLabelForPaths(paths: string[], color: string | null) : Promise { + return await TAURI_INVOKE("set_color_label_for_paths", { paths, color }); +}, +async importFiles(sourcePaths: string[], destinationFolder: string, settings: ImportSettings) : Promise { + return await TAURI_INVOKE("import_files", { sourcePaths, destinationFolder, settings }); +}, +async createVirtualCopy(sourceVirtualPath: string) : Promise { + return await TAURI_INVOKE("create_virtual_copy", { sourceVirtualPath }); +}, +async startBackgroundIndexing(folderPath: string) : Promise { + return await TAURI_INVOKE("start_background_indexing", { folderPath }); +}, +async clearAiTags(rootPath: string) : Promise { + return await TAURI_INVOKE("clear_ai_tags", { rootPath }); +}, +async clearAllTags(rootPath: string) : Promise { + return await TAURI_INVOKE("clear_all_tags", { rootPath }); +}, +async addTagForPaths(paths: string[], tag: string) : Promise { + return await TAURI_INVOKE("add_tag_for_paths", { paths, tag }); +}, +async removeTagForPaths(paths: string[], tag: string) : Promise { + return await TAURI_INVOKE("remove_tag_for_paths", { paths, tag }); +}, +async cullImages(paths: string[], settings: CullingSettings) : Promise { + return await TAURI_INVOKE("cull_images", { paths, settings }); +}, +async getLensfunMakers() : Promise { + return await TAURI_INVOKE("get_lensfun_makers"); +}, +async getLensfunLensesForMaker(maker: string) : Promise { + return await TAURI_INVOKE("get_lensfun_lenses_for_maker", { maker }); +}, +async autodetectLens(maker: string, model: string) : Promise<[string, string] | null> { + return await TAURI_INVOKE("autodetect_lens", { maker, model }); +}, +async getLensDistortionParams(maker: string, model: string, focalLength: number, aperture: number | null, distance: number | null) : Promise { + return await TAURI_INVOKE("get_lens_distortion_params", { maker, model, focalLength, aperture, distance }); +}, +async previewNegativeConversion(path: string, params: NegativeConversionParams) : Promise { + return await TAURI_INVOKE("preview_negative_conversion", { path, params }); +}, +async convertNegativeFull(path: string, params: NegativeConversionParams) : Promise { + return await TAURI_INVOKE("convert_negative_full", { path, params }); +}, +async saveConvertedNegative(originalPathStr: string) : Promise { + return await TAURI_INVOKE("save_converted_negative", { originalPathStr }); +} +} + +/** user-defined events **/ + + +export const events = __makeEvents__<{ +aiConnectorStatusUpdate: AiConnectorStatusUpdate, +aiModelDownloadFinish: AiModelDownloadFinish, +aiModelDownloadStart: AiModelDownloadStart, +batchExportProgress: BatchExportProgress, +cullingComplete: CullingComplete, +cullingProgress: CullingProgress, +cullingStart: CullingStart, +denoiseComplete: DenoiseComplete, +denoiseError: DenoiseError, +denoiseProgress: DenoiseProgress, +exportCancelled: ExportCancelled, +exportComplete: ExportComplete, +exportCompleteWithErrors: ExportCompleteWithErrors, +exportError: ExportError, +hdrComplete: HdrComplete, +hdrError: HdrError, +hdrProgress: HdrProgress, +histogramUpdate: HistogramUpdate, +importComplete: ImportComplete, +importError: ImportError, +importProgress: ImportProgress, +importStart: ImportStart, +indexingError: IndexingError, +indexingFinished: IndexingFinished, +indexingProgress: IndexingProgress, +indexingStarted: IndexingStarted, +openWithFile: OpenWithFile, +panoramaComplete: PanoramaComplete, +panoramaError: PanoramaError, +panoramaProgress: PanoramaProgress, +panoramaWarning: PanoramaWarning, +previewUpdateFinal: PreviewUpdateFinal, +previewUpdateUncropped: PreviewUpdateUncropped, +thumbnailGenerated: ThumbnailGenerated, +thumbnailGenerationComplete: ThumbnailGenerationComplete, +thumbnailGenerationError: ThumbnailGenerationError, +thumbnailProgress: ThumbnailProgress, +waveformUpdate: WaveformUpdate +}>({ +aiConnectorStatusUpdate: "ai-connector-status-update", +aiModelDownloadFinish: "ai-model-download-finish", +aiModelDownloadStart: "ai-model-download-start", +batchExportProgress: "batch-export-progress", +cullingComplete: "culling-complete", +cullingProgress: "culling-progress", +cullingStart: "culling-start", +denoiseComplete: "denoise-complete", +denoiseError: "denoise-error", +denoiseProgress: "denoise-progress", +exportCancelled: "export-cancelled", +exportComplete: "export-complete", +exportCompleteWithErrors: "export-complete-with-errors", +exportError: "export-error", +hdrComplete: "hdr-complete", +hdrError: "hdr-error", +hdrProgress: "hdr-progress", +histogramUpdate: "histogram-update", +importComplete: "import-complete", +importError: "import-error", +importProgress: "import-progress", +importStart: "import-start", +indexingError: "indexing-error", +indexingFinished: "indexing-finished", +indexingProgress: "indexing-progress", +indexingStarted: "indexing-started", +openWithFile: "open-with-file", +panoramaComplete: "panorama-complete", +panoramaError: "panorama-error", +panoramaProgress: "panorama-progress", +panoramaWarning: "panorama-warning", +previewUpdateFinal: "preview-update-final", +previewUpdateUncropped: "preview-update-uncropped", +thumbnailGenerated: "thumbnail-generated", +thumbnailGenerationComplete: "thumbnail-generation-complete", +thumbnailGenerationError: "thumbnail-generation-error", +thumbnailProgress: "thumbnail-progress", +waveformUpdate: "waveform-update" +}) + +/** user-defined constants **/ + + + +/** user-defined types **/ + +export type AiConnectorStatusUpdate = { connected: boolean } +export type AiForegroundMaskParameters = { maskDataBase64?: string | null; rotation?: number | null; flipHorizontal?: boolean | null; flipVertical?: boolean | null; orientationSteps?: number | null } +export type AiModelDownloadFinish = JsonValue +export type AiModelDownloadStart = JsonValue +export type AiPatchDefinition = { id: string; name: string; visible: boolean; invert: boolean; prompt: string; patchData?: PatchData | null; opacity?: number; subMasks: SubMask[] } +export type AiSkyMaskParameters = { maskDataBase64?: string | null; rotation?: number | null; flipHorizontal?: boolean | null; flipVertical?: boolean | null; orientationSteps?: number | null } +export type AiSubjectMaskParameters = { startX: number; startY: number; endX: number; endY: number; maskDataBase64?: string | null; rotation?: number | null; flipHorizontal?: boolean | null; flipVertical?: boolean | null; orientationSteps?: number | null } +export type AppSettings = { lastRootPath: string | null; pinnedFolders?: string[]; editorPreviewResolution: number | null; enableZoomHifi?: boolean | null; enableLivePreviews?: boolean | null; enableHighQualityLivePreviews?: boolean | null; sortCriteria: SortCriteria | null; filterCriteria: FilterCriteria | null; theme: string | null; transparent: boolean | null; decorations: boolean | null; aiConnectorAddress: string | null; lastFolderState: LastFolderState | null; adaptiveEditorTheme: boolean | null; uiVisibility: JsonValue | null; enableAiTagging: boolean | null; taggingThreadCount: number | null; taggingShortcuts?: string[] | null; customAiTags?: string[] | null; aiTagCount?: number | null; thumbnailSize: string | null; thumbnailAspectRatio: string | null; aiProvider: string | null; adjustmentVisibility?: Partial<{ [key in string]: boolean }>; enableExifReading: boolean | null; activeTreeSection?: string | null; copyPasteSettings?: CopyPasteSettings; rawHighlightCompression?: number | null; processingBackend?: string | null; linuxGpuOptimization?: boolean | null; libraryViewMode?: string | null; exportPresets?: ExportPreset[]; myLenses?: MyLens[] | null; enableFolderImageCounts?: boolean | null; linearRawMode?: string; enableXmpSync?: boolean | null; createXmpIfMissing?: boolean | null } +export type BatchExportProgress = JsonValue +export type CommunityPreset = { name: string; creator: string; adjustments: JsonValue } +export type CopyPasteSettings = { mode: PasteMode; includedAdjustments?: string[]; knownAdjustments?: string[] } +export type CullGroup = { representative: ImageAnalysisResult; duplicates: ImageAnalysisResult[] } +export type CullingComplete = JsonValue +export type CullingProgress = JsonValue +export type CullingSettings = { similarityThreshold: number; blurThreshold: number; groupSimilar: boolean; filterBlurry: boolean } +export type CullingStart = JsonValue +export type CullingSuggestions = { similarGroups: CullGroup[]; blurryImages: ImageAnalysisResult[]; failedPaths: string[] } +export type DenoiseComplete = JsonValue +export type DenoiseError = JsonValue +export type DenoiseProgress = JsonValue +export type ExportCancelled = JsonValue +export type ExportComplete = JsonValue +export type ExportCompleteWithErrors = JsonValue +export type ExportError = JsonValue +export type ExportPreset = { id: string; name: string; fileFormat: string; jpegQuality: number; enableResize: boolean; resizeMode: string; resizeValue: number; dontEnlarge: boolean; keepMetadata: boolean; stripGps: boolean; filenameTemplate: string; enableWatermark: boolean; watermarkPath: string | null; watermarkAnchor: string | null; watermarkScale: number; watermarkSpacing: number; watermarkOpacity: number; exportMasks?: boolean | null } +export type ExportSettings = { jpegQuality: number; resize: ResizeOptions | null; keepMetadata: boolean; stripGps: boolean; filenameTemplate: string | null; watermark: WatermarkSettings | null; exportMasks?: boolean } +export type FilterCriteria = { rating: number; rawStatus: string; colors?: string[] } +export type FolderNode = { name: string; path: string; children: FolderNode[]; isDir: boolean; imageCount: number } +export type GeometryParams = { distortion: number; vertical: number; horizontal: number; rotate: number; aspect: number; scale: number; x_offset: number; y_offset: number; lens_distortion_amount: number; lens_vignette_amount: number; lens_tca_amount: number; lens_distortion_enabled: boolean; lens_tca_enabled: boolean; lens_vignette_enabled: boolean; lens_auto_crop: boolean; lens_dist_k1: number; lens_dist_k2: number; lens_dist_k3: number; lens_model: number; tca_vr: number; tca_vb: number; vig_k1: number; vig_k2: number; vig_k3: number } +export type HdrComplete = JsonValue +export type HdrError = JsonValue +export type HdrProgress = JsonValue +export type HistogramData = { red: number[]; green: number[]; blue: number[]; luma: number[] } +export type HistogramUpdate = JsonValue +export type ImageAnalysisResult = { path: string; qualityScore: number; sharpnessMetric: number; centerFocusMetric: number; exposureMetric: number; width: number; height: number } +export type ImageDimensions = { width: number; height: number } +export type ImageFile = { path: string; modified: number; is_edited: boolean; tags: string[] | null; exif: Partial<{ [key in string]: string }> | null; is_virtual_copy: boolean } +export type ImageMetadata = { version: number; rating: number; adjustments: JsonValue; tags?: string[] | null } +export type ImportComplete = JsonValue +export type ImportError = JsonValue +export type ImportProgress = JsonValue +export type ImportSettings = { filenameTemplate: string; organizeByDate: boolean; dateFolderFormat: string; deleteAfterImport: boolean } +export type ImportStart = JsonValue +export type IndexingError = JsonValue +export type IndexingFinished = JsonValue +export type IndexingProgress = JsonValue +export type IndexingStarted = JsonValue +export type JsonValue = null | boolean | number | string | JsonValue[] | Partial<{ [key in string]: JsonValue }> +export type LastFolderState = { currentFolderPath: string; expandedFolders: string[] } +export type LensDistortionParams = { k1: number; k2: number; k3: number; model: number; tca_vr: number; tca_vb: number; vig_k1: number; vig_k2: number; vig_k3: number } +export type LoadImageResult = { width: number; height: number; metadata: ImageMetadata; exif: Partial<{ [key in string]: string }>; is_raw: boolean } +export type LutParseResult = { size: number } +export type MaskDefinition = { id: string; name: string; visible: boolean; invert: boolean; opacity?: number; adjustments: JsonValue; subMasks: SubMask[] } +export type MyLens = { maker: string; model: string } +export type NegativeConversionParams = { red_weight: number; green_weight: number; blue_weight: number; exposure: number; contrast: number } +export type OpenWithFile = JsonValue +export type PanoramaComplete = JsonValue +export type PanoramaError = JsonValue +export type PanoramaProgress = JsonValue +export type PanoramaWarning = JsonValue +export type PasteMode = "merge" | "replace" +export type PatchData = { color: string; mask: string } +export type Preset = { id: string; name: string; adjustments: JsonValue } +export type PresetFolder = { id: string; name: string; children: Preset[] } +export type PresetItem = { preset: Preset } | { folder: PresetFolder } +export type PreviewUpdateFinal = JsonValue +export type PreviewUpdateUncropped = JsonValue +export type ResizeMode = "longEdge" | "shortEdge" | "width" | "height" +export type ResizeOptions = { mode: ResizeMode; value: number; dontEnlarge: boolean } +export type SortCriteria = { key: string; order: string } +export type SubMask = { id: string; type: string; visible: boolean; invert?: boolean; opacity?: number; mode: SubMaskMode; parameters: JsonValue } +export type SubMaskMode = "additive" | "subtractive" +export type ThumbnailGenerated = JsonValue +export type ThumbnailGenerationComplete = JsonValue +export type ThumbnailGenerationError = JsonValue +export type ThumbnailProgress = JsonValue +export type WatermarkAnchor = "topLeft" | "topCenter" | "topRight" | "centerLeft" | "center" | "centerRight" | "bottomLeft" | "bottomCenter" | "bottomRight" +export type WatermarkSettings = { path: string; anchor: WatermarkAnchor; scale: number; spacing: number; opacity: number } +export type WaveformData = { red: number[]; green: number[]; blue: number[]; luma: number[]; width: number; height: number } +export type WaveformUpdate = JsonValue + +/** tauri-specta globals **/ + +import { + invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, +} from "@tauri-apps/api/core"; +import * as TAURI_API_EVENT from "@tauri-apps/api/event"; +import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; + +type __EventObj__ = { + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: null extends T + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; +}; + +export type Result = + | { status: "ok"; data: T } + | { status: "error"; error: E }; + +function __makeEvents__>( + mappings: Record, +) { + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case "listen": + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case "once": + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case "emit": + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + }, + ); +} diff --git a/src/components/modals/CollageModal.tsx b/src/components/modals/CollageModal.tsx index 7dedbb181..682c585c5 100644 --- a/src/components/modals/CollageModal.tsx +++ b/src/components/modals/CollageModal.tsx @@ -1,8 +1,8 @@ import { useState, useEffect, useCallback, useRef, useLayoutEffect } from 'react'; -import { invoke } from '@tauri-apps/api/core'; import { AnimatePresence, motion } from 'framer-motion'; import { CheckCircle, XCircle, Loader2, Save, Crop, Proportions, LayoutTemplate, Shuffle, RectangleHorizontal, RectangleVertical, Palette } from 'lucide-react'; -import { ImageFile, Invokes } from '../ui/AppProperties'; +import { commands } from '../../bindings'; +import { ImageFile } from '../ui/AppProperties'; import Button from '../ui/Button'; import Slider from '../ui/Slider'; import Switch from '../ui/Switch'; @@ -122,10 +122,10 @@ export default function CollageModal({ isOpen, onClose, onSave, sourceImages }: setError(null); try { const imagePromises = sourceImages.map(async (imageFile) => { - const metadata: any = await invoke(Invokes.LoadMetadata, { path: imageFile.path }); + const metadata: any = await commands.loadMetadata(imageFile.path); const adjustments = metadata.adjustments && !metadata.adjustments.is_null ? metadata.adjustments : {}; - const imageData: Uint8Array = await invoke(Invokes.GeneratePreviewForPath, { path: imageFile.path, jsAdjustments: adjustments }); + const imageData: Uint8Array = await commands.generatePreviewForPath(imageFile.path, adjustments); const blob = new Blob([imageData], { type: 'image/jpeg' }); const url = URL.createObjectURL(blob); diff --git a/src/components/modals/CullingModal.tsx b/src/components/modals/CullingModal.tsx index fd5d5b6a1..ed3d1e888 100644 --- a/src/components/modals/CullingModal.tsx +++ b/src/components/modals/CullingModal.tsx @@ -1,8 +1,8 @@ import { useState, useEffect, useCallback } from 'react'; -import { invoke } from '@tauri-apps/api/core'; import { CheckCircle, XCircle, Loader2, Users, Trash2, Star, Tag } from 'lucide-react'; import { AnimatePresence, motion } from 'framer-motion'; -import { CullingSettings, CullingSuggestions, Invokes, Progress } from '../ui/AppProperties'; +import { commands } from '../../bindings'; +import { CullingSettings, CullingSuggestions, Progress } from '../ui/AppProperties'; import Button from '../ui/Button'; import Switch from '../ui/Switch'; import Slider from '../ui/Slider'; @@ -124,7 +124,7 @@ export default function CullingModal({ const handleStartCulling = useCallback(async () => { try { - await invoke(Invokes.CullImages, { paths: imagePaths, settings }); + await commands.cullImages(imagePaths, settings); } catch (err) { console.error('Culling failed to start:', err); onError(String(err)); diff --git a/src/components/modals/LensCorrectionModal.tsx b/src/components/modals/LensCorrectionModal.tsx index c165e58cd..196a01ad6 100644 --- a/src/components/modals/LensCorrectionModal.tsx +++ b/src/components/modals/LensCorrectionModal.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; -import { invoke } from '@tauri-apps/api/core'; +import { commands } from '../../bindings'; import { RotateCcw, Search, @@ -207,6 +207,9 @@ export default function LensCorrectionModal({ const fetchDistortionParams = async (maker: string, model: string) => { try { +<<<<<<< refactor/tauri-specta + const distParams: any = await commands.getLensDistortionParams(maker, model, focalLength ?? 50, aperture, distance); +======= const distParams: any = await invoke('get_lens_distortion_params', { maker, model, @@ -214,6 +217,7 @@ export default function LensCorrectionModal({ aperture: aperture, distance: distance, }); +>>>>>>> main return distParams; } catch (error) { console.error('Failed to fetch lens params', error); @@ -253,11 +257,7 @@ export default function LensCorrectionModal({ vig_k3: currentParams.lensDistortionParams?.vig_k3 ?? 0, }; - const result: string = await invoke('preview_geometry_transform', { - params: fullParams, - jsAdjustments: currentAdjustments, - showLines: false, - }); + const result: string = await commands.previewGeometryTransform(fullParams, currentAdjustments, false); setPreviewUrl(result); } catch (e) { console.error('Lens correction preview failed', e); @@ -271,7 +271,7 @@ export default function LensCorrectionModal({ setIsMounted(true); const timer = setTimeout(() => setShow(true), 10); - invoke('load_settings').then((settings: any) => { + commands.loadSettings().then((settings: any) => { if (settings?.myLenses) { setMyLenses(settings.myLenses); } @@ -294,12 +294,12 @@ export default function LensCorrectionModal({ handleResetZoom(); updatePreview(initParams); - invoke('get_lensfun_makers') + commands.getLensfunMakers() .then((m: any) => setMakers(m)) .catch(console.error); if (initParams.lensMaker) { - invoke('get_lensfun_lenses_for_maker', { maker: initParams.lensMaker }) + commands.getLensfunLensesForMaker(initParams.lensMaker) .then((l: any) => setLenses(l)) .catch(console.error); } @@ -326,8 +326,13 @@ export default function LensCorrectionModal({ setParams(newParams); setLenses([]); setDetectionStatus('idle'); +<<<<<<< refactor/tauri-specta + + commands.getLensfunLensesForMaker(maker) +======= invoke('get_lensfun_lenses_for_maker', { maker }) +>>>>>>> main .then((l: any) => setLenses(l)) .catch(console.error); @@ -357,7 +362,7 @@ export default function LensCorrectionModal({ setParams(tempParams); setDetectionStatus('idle'); - invoke('get_lensfun_lenses_for_maker', { maker: selected.maker }) + commands.getLensfunLensesForMaker(selected.maker) .then((l: any) => setLenses(l)) .catch(console.error); @@ -395,13 +400,18 @@ export default function LensCorrectionModal({ setDetectionStatus('detecting'); try { +<<<<<<< refactor/tauri-specta + const result: [string, string] | null = await commands.autodetectLens(exifMaker, exifModel); + +======= const result: [string, string] | null = await invoke('autodetect_lens', { maker: exifMaker, model: exifModel }); +>>>>>>> main if (result) { const [detectedMaker, detectedModel] = result; if (detectedMaker !== params.lensMaker) { - await invoke('get_lensfun_lenses_for_maker', { maker: detectedMaker }).then((l: any) => setLenses(l)); + await commands.getLensfunLensesForMaker(detectedMaker).then((l: any) => setLenses(l)); } const distortionParams = await fetchDistortionParams(detectedMaker, detectedModel); @@ -480,11 +490,7 @@ export default function LensCorrectionModal({ vig_k3: currentAdjustments.lensDistortionParams?.vig_k3 ?? 0, }; - invoke('preview_geometry_transform', { - params: fullParams, - jsAdjustments: currentAdjustments, - showLines: false, - }).then((result: any) => setPreviewUrl(result)); + commands.previewGeometryTransform(fullParams, currentAdjustments, false).then((result: any) => setPreviewUrl(result)); } else { updatePreview(params); } diff --git a/src/components/modals/NegativeConversionModal.tsx b/src/components/modals/NegativeConversionModal.tsx index ccebe1205..d5b08548e 100644 --- a/src/components/modals/NegativeConversionModal.tsx +++ b/src/components/modals/NegativeConversionModal.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback, useRef } from 'react'; -import { invoke } from '@tauri-apps/api/core'; +import { commands } from '../../bindings'; import { RotateCcw, ZoomIn, @@ -109,10 +109,7 @@ export default function NegativeConversionModal({ throttle(async (currentParams: NegativeParams, isInitialLoad: boolean = false) => { if (!selectedImagePath) return; try { - const result: string = await invoke('preview_negative_conversion', { - path: selectedImagePath, - params: currentParams - }); + const result: string = await commands.previewNegativeConversion(selectedImagePath, currentParams); setPreviewUrl(result); if (isInitialLoad) { setIsLoading(false); @@ -133,10 +130,7 @@ export default function NegativeConversionModal({ setIsLoading(true); setTimeout(() => setShow(true), 10); updatePreview(DEFAULT_PARAMS, true); - invoke('generate_preview_for_path', { - path: selectedImagePath, - jsAdjustments: {} - }).then((res: any) => { + commands.generatePreviewForPath(selectedImagePath || '', {}).then((res: any) => { const blob = new Blob([new Uint8Array(res)], { type: 'image/jpeg' }); setOriginalUrl(URL.createObjectURL(blob)); }).catch(console.error); @@ -164,10 +158,8 @@ export default function NegativeConversionModal({ if (!selectedImagePath) return; setIsSaving(true); try { - await invoke('convert_negative_full', { path: selectedImagePath, params }); - const savedPath: string = await invoke('save_converted_negative', { - originalPathStr: selectedImagePath - }); + await commands.convertNegativeFull(selectedImagePath, params); + const savedPath: string = await commands.saveConvertedNegative(selectedImagePath); onSave(savedPath); onClose(); } catch (e) { diff --git a/src/components/modals/TransformModal.tsx b/src/components/modals/TransformModal.tsx index 23271a87e..da6d338e0 100644 --- a/src/components/modals/TransformModal.tsx +++ b/src/components/modals/TransformModal.tsx @@ -1,6 +1,22 @@ import { useState, useEffect, useCallback, useRef } from 'react'; +<<<<<<< refactor/tauri-specta +import { commands } from '../../bindings'; +import { + Check, + RotateCcw, + Grid3X3, + Eye, + EyeOff, + Info, + LineChart, + ZoomIn, + ZoomOut, + Maximize, +} from 'lucide-react'; +======= import { invoke } from '@tauri-apps/api/core'; import { Check, RotateCcw, Grid3X3, Eye, EyeOff, Info, LineChart, ZoomIn, ZoomOut, Maximize } from 'lucide-react'; +>>>>>>> main import { AnimatePresence, motion } from 'framer-motion'; import Button from '../ui/Button'; import Slider from '../ui/Slider'; @@ -218,11 +234,7 @@ export default function TransformModal({ isOpen, onClose, onApply, currentAdjust lens_vignette_enabled: currentAdjustments.lensVignetteEnabled ?? true, }; - const result: string = await invoke('preview_geometry_transform', { - params: fullParams, - jsAdjustments: currentAdjustments, - showLines: linesEnabled, - }); + const result: string = await commands.previewGeometryTransform(fullParams, currentAdjustments, linesEnabled); setPreviewUrl(result); } catch (e) { console.error('Preview transform failed', e); @@ -310,11 +322,7 @@ export default function TransformModal({ isOpen, onClose, onApply, currentAdjust lens_tca_enabled: currentAdjustments.lensTcaEnabled ?? true, lens_vignette_enabled: currentAdjustments.lensVignetteEnabled ?? true, }; - const result: string = await invoke('preview_geometry_transform', { - params: fullParams, - jsAdjustments: currentAdjustments, - showLines: false, - }); + const result: string = await commands.previewGeometryTransform(fullParams, currentAdjustments, false); setPreviewUrl(result); } else { updatePreview(params, showLines); diff --git a/src/components/panel/CommunityPage.tsx b/src/components/panel/CommunityPage.tsx index 515db565c..60f572067 100644 --- a/src/components/panel/CommunityPage.tsx +++ b/src/components/panel/CommunityPage.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; -import { invoke } from '@tauri-apps/api/core'; +import { commands } from '../../bindings'; import { ArrowLeft, CheckCircle2, @@ -12,7 +12,7 @@ import { import { motion, AnimatePresence } from 'framer-motion'; import Button from '../ui/Button'; import Input from '../ui/Input'; -import { Invokes, SupportedTypes, ImageFile } from '../ui/AppProperties'; +import { SupportedTypes, ImageFile } from '../ui/AppProperties'; import { INITIAL_ADJUSTMENTS } from '../../utils/adjustments'; const DEFAULT_PREVIEW_IMAGE_URL = 'https://raw.githubusercontent.com/CyberTimon/RapidRAW-Presets/main/sample-image.jpg'; @@ -75,7 +75,7 @@ const CommunityPage = ({ onBackToLibrary, imageList, currentFolderPath }: Commun try { const response = await fetch(DEFAULT_PREVIEW_IMAGE_URL); const blob = await response.blob(); - const tempPath: string = await invoke(Invokes.SaveTempFile, { bytes: Array.from(new Uint8Array(await blob.arrayBuffer())) }); + const tempPath: string = await commands.saveTempFile(Array.from(new Uint8Array(await blob.arrayBuffer()))); return tempPath; } catch (error) { console.error("Failed to fetch default preview image:", error); @@ -87,7 +87,7 @@ const CommunityPage = ({ onBackToLibrary, imageList, currentFolderPath }: Commun const fetchPresets = async () => { setIsLoading(true); try { - const communityPresets: CommunityPreset[] = await invoke(Invokes.FetchCommunityPresets); + const communityPresets: CommunityPreset[] = await commands.fetchCommunityPresets(); setPresets(communityPresets); } catch (error) { console.error("Failed to fetch community presets:", error); @@ -141,13 +141,13 @@ const CommunityPage = ({ onBackToLibrary, imageList, currentFolderPath }: Commun const generateAllPreviews = async () => { setAllPreviewsLoaded(false); try { - const previewDataMap: Record = await invoke(Invokes.GenerateAllCommunityPreviews, { - imagePaths: previewImagePaths, - presets: presets.map(p => ({ + const previewDataMap: Record = await commands.generateAllCommunityPreviews( + previewImagePaths, + presets.map((p) => ({ ...p, adjustments: { ...INITIAL_ADJUSTMENTS, ...p.adjustments } })), - }); + ); const newPreviews: Record = {}; for (const [presetName, imageData] of Object.entries(previewDataMap)) { @@ -178,10 +178,7 @@ const CommunityPage = ({ onBackToLibrary, imageList, currentFolderPath }: Commun throw new Error("Preset adjustments are missing."); } - await invoke(Invokes.SaveCommunityPreset, { - name: preset.name, - adjustments: preset.adjustments, - }); + await commands.saveCommunityPreset(preset.name, preset.adjustments); setDownloadStatus(prev => ({ ...prev, [preset.name]: 'success' })); } catch (error) { console.error(`Failed to download preset ${preset.name}:`, error); @@ -331,4 +328,4 @@ const CommunityPage = ({ onBackToLibrary, imageList, currentFolderPath }: Commun ); }; -export default CommunityPage; \ No newline at end of file +export default CommunityPage; diff --git a/src/components/panel/Editor.tsx b/src/components/panel/Editor.tsx index 0ade93995..de88cd0ef 100644 --- a/src/components/panel/Editor.tsx +++ b/src/components/panel/Editor.tsx @@ -3,7 +3,7 @@ import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch'; import { Crop, PercentCrop } from 'react-image-crop'; import { Loader2 } from 'lucide-react'; import clsx from 'clsx'; -import { invoke } from '@tauri-apps/api/core'; +import { commands } from '../../bindings'; import debounce from 'lodash.debounce'; import { AnimatePresence } from 'framer-motion'; import { ImageDimensions, useImageRenderSize } from '../../hooks/useImageRenderSize'; @@ -12,7 +12,7 @@ import EditorToolbar from './editor/EditorToolbar'; import ImageCanvas from './editor/ImageCanvas'; import Waveform from './editor/Waveform'; import { Mask, SubMask } from './right/Masks'; -import { BrushSettings, Invokes, Panel, SelectedImage, TransformState, WaveformData } from '../ui/AppProperties'; +import { BrushSettings, Panel, SelectedImage, TransformState, WaveformData } from '../ui/AppProperties'; import type { OverlayMode } from './right/CropPanel'; interface EditorProps { @@ -311,13 +311,13 @@ export default function Editor({ } try { const cropOffset = [adjustments.crop?.x || 0, adjustments.crop?.y || 0]; - const dataUrl: string = await invoke(Invokes.GenerateMaskOverlay, { - cropOffset, - height: Math.round(renderSize.height), + const dataUrl: string = await commands.generateMaskOverlay( maskDef, - scale: renderSize.scale, - width: Math.round(renderSize.width), - }); + Math.round(renderSize.width), + Math.round(renderSize.height), + renderSize.scale, + cropOffset, + ); if (dataUrl) { setMaskOverlayUrl(dataUrl); } else { diff --git a/src/components/panel/MainLibrary.tsx b/src/components/panel/MainLibrary.tsx index 273344457..2b1722660 100644 --- a/src/components/panel/MainLibrary.tsx +++ b/src/components/panel/MainLibrary.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef, forwardRef, useMemo, useCallback } from 'react'; import { getVersion } from '@tauri-apps/api/app'; -import { invoke } from '@tauri-apps/api/core'; +import { commands } from '../../bindings'; import { open } from '@tauri-apps/plugin-shell'; import { AlertTriangle, @@ -29,7 +29,6 @@ import { AppSettings, FilterCriteria, ImageFile, - Invokes, LibraryViewMode, Progress, RawStatus, @@ -1348,7 +1347,7 @@ export default function MainLibrary({ }, []); useEffect(() => { - invoke(Invokes.GetSupportedFileTypes) + commands.getSupportedFileTypes() .then((types: any) => setSupportedTypes(types)) .catch((err) => console.error('Failed to load supported file types:', err)); }, []); diff --git a/src/components/panel/SettingsPanel.tsx b/src/components/panel/SettingsPanel.tsx index 45d957c09..13df77416 100644 --- a/src/components/panel/SettingsPanel.tsx +++ b/src/components/panel/SettingsPanel.tsx @@ -15,7 +15,7 @@ import { Keyboard, Bookmark, } from 'lucide-react'; -import { invoke } from '@tauri-apps/api/core'; +import { commands } from '../../bindings'; import { relaunch } from '@tauri-apps/plugin-process'; import { motion, AnimatePresence } from 'framer-motion'; import clsx from 'clsx'; @@ -27,7 +27,7 @@ import Switch from '../ui/Switch'; import Input from '../ui/Input'; import Slider from '../ui/Slider'; import { ThemeProps, THEMES, DEFAULT_THEME_ID } from '../../utils/themes'; -import { Invokes } from '../ui/AppProperties'; + interface ConfirmModalState { confirmText: string; @@ -279,7 +279,7 @@ export default function SettingsPanel({ useEffect(() => { const fetchLogPath = async () => { try { - const path: string = await invoke(Invokes.GetLogFilePath); + const path: string = await commands.getLogFilePath(); setLogPath(path); } catch (error) { console.error('Failed to get log file path:', error); @@ -288,7 +288,7 @@ export default function SettingsPanel({ }; fetchLogPath(); - invoke('get_lensfun_makers') + commands.getLensfunMakers() .then((m: any) => setLensMakers(m)) .catch(console.error); }, []); @@ -321,7 +321,7 @@ export default function SettingsPanel({ setTempLensModel(''); setLensModels([]); if (maker) { - invoke('get_lensfun_lenses_for_maker', { maker }) + commands.getLensfunLensesForMaker(maker) .then((l: any) => setLensModels(l)) .catch(console.error); } @@ -363,7 +363,7 @@ export default function SettingsPanel({ setIsClearing(true); setClearMessage('Deleting sidecar files, please wait...'); try { - const count: number = await invoke(Invokes.ClearAllSidecars, { rootPath: effectiveRootPath }); + const count: number = await commands.clearAllSidecars(effectiveRootPath); setClearMessage(`${count} sidecar files deleted successfully.`); onLibraryRefresh(); } catch (err: any) { @@ -393,7 +393,7 @@ export default function SettingsPanel({ setIsClearingAiTags(true); setAiTagsClearMessage('Clearing AI tags from all sidecar files...'); try { - const count: number = await invoke(Invokes.ClearAiTags, { rootPath: effectiveRootPath }); + const count: number = await commands.clearAiTags(effectiveRootPath); setAiTagsClearMessage(`${count} files updated. AI tags removed.`); onLibraryRefresh(); } catch (err: any) { @@ -423,7 +423,7 @@ export default function SettingsPanel({ setIsClearingTags(true); setTagsClearMessage('Clearing all tags from sidecar files...'); try { - const count: number = await invoke(Invokes.ClearAllTags, { rootPath: effectiveRootPath }); + const count: number = await commands.clearAllTags(effectiveRootPath); setTagsClearMessage(`${count} files updated. All non-color tags removed.`); onLibraryRefresh(); } catch (err: any) { @@ -476,7 +476,7 @@ export default function SettingsPanel({ setIsClearingCache(true); setCacheClearMessage('Clearing thumbnail cache...'); try { - await invoke(Invokes.ClearThumbnailCache); + await commands.clearThumbnailCache(); setCacheClearMessage('Thumbnail cache cleared successfully.'); onLibraryRefresh(); } catch (err: any) { @@ -508,7 +508,7 @@ export default function SettingsPanel({ } setTestStatus({ testing: true, message: 'Testing...', success: null }); try { - await invoke(Invokes.TestAIConnectorConnection, { address: aiConnectorAddress }); + await commands.testAiConnectorConnection(aiConnectorAddress); setTestStatus({ testing: false, message: 'Connection successful!', success: true }); } catch (err) { setTestStatus({ testing: false, message: `Connection failed.`, success: false }); @@ -1352,7 +1352,7 @@ export default function SettingsPanel({ { if (logPath && !logPath.startsWith('Could not')) { - await invoke(Invokes.ShowInFinder, { path: logPath }); + await commands.showInFinder(logPath); } }} buttonText="Open Log File" diff --git a/src/components/panel/right/ExportPanel.tsx b/src/components/panel/right/ExportPanel.tsx index 1075a9c5e..f1f094f90 100644 --- a/src/components/panel/right/ExportPanel.tsx +++ b/src/components/panel/right/ExportPanel.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import { save, open } from '@tauri-apps/plugin-dialog'; -import { invoke } from '@tauri-apps/api/core'; +import { commands } from '../../../bindings'; import { Save, CheckCircle, XCircle, Loader, Ban } from 'lucide-react'; import debounce from 'lodash.debounce'; import Switch from '../../ui/Switch'; @@ -19,7 +19,7 @@ import { FileFormats, WatermarkAnchor, } from '../../ui/ExportImportProperties'; -import { Invokes, SelectedImage, AppSettings } from '../../ui/AppProperties'; +import { SelectedImage, AppSettings } from '../../ui/AppProperties'; import ExportPresetsList from '../../ui/ExportPresetsList'; import { useExportSettings } from '../../../hooks/useExportSettings'; @@ -262,9 +262,7 @@ export default function ExportPanel({ const fetchWatermarkDimensions = async () => { if (watermarkPath) { try { - const dimensions: { width: number; height: number } = await invoke('get_image_dimensions', { - path: watermarkPath, - }); + const dimensions: { width: number; height: number } = await commands.getImageDimensions(watermarkPath); if (dimensions.height > 0) { setWatermarkImageAspectRatio(dimensions.width / dimensions.height); } else { @@ -302,11 +300,7 @@ export default function ExportPanel({ } setIsEstimating(true); try { - const size: number = await invoke(Invokes.EstimateExportSize, { - jsAdjustments: currentAdjustments, - exportSettings, - outputFormat: format, - }); + const size: number = await commands.estimateExportSize(currentAdjustments, exportSettings, format); setEstimatedSize(size); } catch (err) { console.error('Failed to estimate export size:', err); @@ -422,6 +416,16 @@ export default function ExportPanel({ defaultPath: lastExportPath ?? undefined, }); if (outputFolder) { +<<<<<<< refactor/tauri-specta + await commands.batchExportImages( + outputFolder, + pathsToExport, + exportSettings, + FILE_FORMATS.find((f: FileFormat) => f.id === fileFormat)?.extensions[0] || 'jpeg', + ); + } else { + setExportState((prev: ExportState) => ({ ...prev, status: Status.Idle })); +======= saveLastUsedPreset(outputFolder as string); setExportState({ status: Status.Exporting, progress: { current: 0, total: numImages }, errorMessage: '' }); await invoke(Invokes.BatchExportImages, { @@ -430,6 +434,7 @@ export default function ExportPanel({ outputFormat: FILE_FORMATS.find((f: FileFormat) => f.id === fileFormat)?.extensions[0], paths: pathsToExport, }); +>>>>>>> main } } else { const selectedFormat: any = FILE_FORMATS.find((f) => f.id === fileFormat); @@ -449,6 +454,11 @@ export default function ExportPanel({ ], }); if (filePath) { +<<<<<<< refactor/tauri-specta + await commands.exportImage(selectedImage.path, filePath, adjustments, exportSettings); + } else { + setExportState((prev: ExportState) => ({ ...prev, status: Status.Idle })); +======= const dir = filePath.substring(0, Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'))); if (dir) saveLastUsedPreset(dir); setExportState({ status: Status.Exporting, progress: { current: 0, total: numImages }, errorMessage: '' }); @@ -458,6 +468,7 @@ export default function ExportPanel({ originalPath: selectedImage.path, outputPath: filePath, }); +>>>>>>> main } } } catch (error) { @@ -472,7 +483,7 @@ export default function ExportPanel({ const handleCancel = async () => { try { - await invoke(Invokes.CancelExport); + await commands.cancelExport(); } catch (error) { console.error('Failed to send cancel request:', error); } diff --git a/src/components/panel/right/LibraryExportPanel.tsx b/src/components/panel/right/LibraryExportPanel.tsx index 7234fc421..3d745fc1a 100644 --- a/src/components/panel/right/LibraryExportPanel.tsx +++ b/src/components/panel/right/LibraryExportPanel.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import { open } from '@tauri-apps/plugin-dialog'; -import { invoke } from '@tauri-apps/api/core'; +import { commands } from '../../../bindings'; import { Save, CheckCircle, XCircle, Loader, X, Ban } from 'lucide-react'; import debounce from 'lodash.debounce'; import Switch from '../../ui/Switch'; @@ -18,7 +18,7 @@ import { FileFormats, WatermarkAnchor, } from '../../ui/ExportImportProperties'; -import { Invokes, ImageFile, AppSettings } from '../../ui/AppProperties'; +import { ImageFile, AppSettings } from '../../ui/AppProperties'; import ExportPresetsList from '../../ui/ExportPresetsList'; import { useExportSettings } from '../../../hooks/useExportSettings'; @@ -259,9 +259,7 @@ export default function LibraryExportPanel({ if (multiSelectedPaths.length > 0) { try { const firstPath = multiSelectedPaths[0]; - const dimensions: { width: number; height: number } = await invoke('get_image_dimensions', { - path: firstPath, - }); + const dimensions: { width: number; height: number } = await commands.getImageDimensions(firstPath); if (dimensions.width > 0 && dimensions.height > 0) { setImageAspectRatio(dimensions.width / dimensions.height); } else { @@ -285,9 +283,7 @@ export default function LibraryExportPanel({ const fetchWatermarkDimensions = async () => { if (watermarkPath) { try { - const dimensions: { width: number; height: number } = await invoke('get_image_dimensions', { - path: watermarkPath, - }); + const dimensions: { width: number; height: number } = await commands.getImageDimensions(watermarkPath); if (dimensions.height > 0) { setWatermarkImageAspectRatio(dimensions.width / dimensions.height); } else { @@ -321,11 +317,7 @@ export default function LibraryExportPanel({ debounce(async (paths, exportSettings, format) => { setIsEstimating(true); try { - const size: number = await invoke(Invokes.EstimateBatchExportSize, { - paths, - exportSettings, - outputFormat: format, - }); + const size: number = await commands.estimateBatchExportSize(paths, exportSettings, format); setEstimatedSize(size); } catch (err) { console.error('Failed to estimate batch export size:', err); @@ -452,6 +444,16 @@ export default function LibraryExportPanel({ }); if (outputFolder) { +<<<<<<< refactor/tauri-specta + await commands.batchExportImages( + outputFolder, + multiSelectedPaths, + exportSettings, + FILE_FORMATS.find((f: FileFormat) => f.id === fileFormat)?.extensions[0] || 'jpeg', + ); + } else { + setExportState((prev: ExportState) => ({ ...prev, status: Status.Idle })); +======= saveLastUsedPreset(outputFolder as string); setExportState({ status: Status.Exporting, progress: { current: 0, total: numImages }, errorMessage: '' }); await invoke(Invokes.BatchExportImages, { @@ -460,6 +462,7 @@ export default function LibraryExportPanel({ outputFormat: FILE_FORMATS.find((f: FileFormat) => f.id === fileFormat)?.extensions[0], paths: multiSelectedPaths, }); +>>>>>>> main } } catch (error) { console.error('Error exporting images:', error); @@ -473,7 +476,7 @@ export default function LibraryExportPanel({ const handleCancel = async () => { try { - await invoke(Invokes.CancelExport); + await commands.cancelExport(); } catch (error) { console.error('Failed to send cancel request:', error); } diff --git a/src/components/panel/right/MetadataPanel.tsx b/src/components/panel/right/MetadataPanel.tsx index 09b68fff4..f4ce4a091 100644 --- a/src/components/panel/right/MetadataPanel.tsx +++ b/src/components/panel/right/MetadataPanel.tsx @@ -1,9 +1,9 @@ import { useState, useMemo } from 'react'; -import { invoke } from '@tauri-apps/api/core'; import { Check, ChevronDown, ChevronRight, Plus, Star, Tag, X } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import clsx from 'clsx'; -import { SelectedImage, AppSettings, Invokes } from '../../ui/AppProperties'; +import { commands } from '../../../bindings'; +import { SelectedImage, AppSettings } from '../../ui/AppProperties'; import { COLOR_LABELS, Color } from '../../../utils/adjustments'; interface CameraSetting { @@ -176,7 +176,7 @@ export default function MetadataPanel({ if (newTagValue && !currentTags.some((t) => t.tag === newTagValue)) { try { const prefixedTag = `${USER_TAG_PREFIX}${newTagValue}`; - await invoke(Invokes.AddTagForPaths, { paths: [selectedImage.path], tag: prefixedTag }); + await commands.addTagForPaths([selectedImage.path], prefixedTag); const newTags = [...currentTags, { tag: newTagValue, isUser: true }]; onTagsChanged([selectedImage.path], newTags); @@ -190,7 +190,7 @@ export default function MetadataPanel({ const handleRemoveTag = async (tagToRemove: { tag: string; isUser: boolean }) => { try { const prefixedTag = tagToRemove.isUser ? `${USER_TAG_PREFIX}${tagToRemove.tag}` : tagToRemove.tag; - await invoke(Invokes.RemoveTagForPaths, { paths: [selectedImage.path], tag: prefixedTag }); + await commands.removeTagForPaths([selectedImage.path], prefixedTag); const newTags = currentTags.filter((t) => t.tag !== tagToRemove.tag); onTagsChanged([selectedImage.path], newTags); diff --git a/src/components/panel/right/PresetsPanel.tsx b/src/components/panel/right/PresetsPanel.tsx index b1421fdf6..28fa066a4 100644 --- a/src/components/panel/right/PresetsPanel.tsx +++ b/src/components/panel/right/PresetsPanel.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; -import { invoke } from '@tauri-apps/api/core'; +import { commands } from '../../../bindings'; import { open as openDialog, save as saveDialog } from '@tauri-apps/plugin-dialog'; import { DndContext, @@ -34,7 +34,7 @@ import CreateFolderModal from '../../modals/CreateFolderModal'; import RenameFolderModal from '../../modals/RenameFolderModal'; import Button from '../../ui/Button'; import { Adjustments, INITIAL_ADJUSTMENTS } from '../../../utils/adjustments'; -import { Invokes, OPTION_SEPARATOR, Panel, Preset, SelectedImage } from '../../ui/AppProperties'; +import { OPTION_SEPARATOR, Panel, Preset, SelectedImage } from '../../ui/AppProperties'; interface DroppableFolderItemProps { children: any; @@ -393,9 +393,7 @@ export default function PresetsPanel({ try { const fullPresetAdjustments = { ...INITIAL_ADJUSTMENTS, ...preset.adjustments }; - const imageData: Uint8Array = await invoke(Invokes.GeneratePresetPreview, { - jsAdjustments: fullPresetAdjustments, - }); + const imageData: Uint8Array = await commands.generatePresetPreview(fullPresetAdjustments); const blob = new Blob([imageData], { type: 'image/jpeg' }); const url = URL.createObjectURL(blob); setPreviews((prev: Record) => { @@ -453,9 +451,7 @@ export default function PresetsPanel({ setIsGeneratingPreviews(true); try { const fullPresetAdjustments: any = { ...INITIAL_ADJUSTMENTS, ...preset.adjustments }; - const imageData: Uint8Array = await invoke(Invokes.GeneratePresetPreview, { - jsAdjustments: fullPresetAdjustments, - }); + const imageData: Uint8Array = await commands.generatePresetPreview(fullPresetAdjustments); const blob = new Blob([imageData], { type: 'image/jpeg' }); const url = URL.createObjectURL(blob); diff --git a/src/components/ui/AppProperties.tsx b/src/components/ui/AppProperties.tsx index 9e9c171bc..cade6133a 100644 --- a/src/components/ui/AppProperties.tsx +++ b/src/components/ui/AppProperties.tsx @@ -5,83 +5,6 @@ import { ToolType } from '../panel/right/Masks'; export const GLOBAL_KEYS = [' ', 'ArrowUp', 'ArrowDown', 'f', 'b', 'w']; export const OPTION_SEPARATOR = 'separator'; -export enum Invokes { - AddTagForPaths = 'add_tag_for_paths', - ApplyAdjustments = 'apply_adjustments', - ApplyAdjustmentsToPaths = 'apply_adjustments_to_paths', - ApplyAutoAdjustmentsToPaths = 'apply_auto_adjustments_to_paths', - ApplyDenoising = 'apply_denoising', - BatchExportImages = 'batch_export_images', - CalculateAutoAdjustments = 'calculate_auto_adjustments', - CancelExport = 'cancel_export', - CheckAIConnectorStatus = 'check_ai_connector_status', - ClearAllSidecars = 'clear_all_sidecars', - ClearAiTags = 'clear_ai_tags', - ClearAllTags = 'clear_all_tags', - ClearThumbnailCache = 'clear_thumbnail_cache', - CopyFiles = 'copy_files', - CreateFolder = 'create_folder', - CreateVirtualCopy = 'create_virtual_copy', - CullImages = 'cull_images', - DeleteFolder = 'delete_folder', - DuplicateFile = 'duplicate_file', - EstimateBatchExportSize = 'estimate_batch_export_size', - EstimateExportSize = 'estimate_export_size', - ExportImage = 'export_image', - FrontendLog = 'frontend_log', - GenerateAiForegroundMask = 'generate_ai_foreground_mask', - GenerateAiSkyMask = 'generate_ai_sky_mask', - GenerateAiSubjectMask = 'generate_ai_subject_mask', - GenerateFullscreenPreview = 'generate_fullscreen_preview', - GeneratePreviewForPath = 'generate_preview_for_path', - GenerateHistogram = 'generate_histogram', - GenerateMaskOverlay = 'generate_mask_overlay', - GeneratePresetPreview = 'generate_preset_preview', - GenerateThumbnailsProgressive = 'generate_thumbnails_progressive', - GenerateUncroppedPreview = 'generate_uncropped_preview', - GenerateWaveform = 'image_processing::generate_waveform', - GetFolderTree = 'get_folder_tree', - GetLogFilePath = 'get_log_file_path', - GetPinnedFolderTrees = 'get_pinned_folder_trees', - GetSupportedFileTypes = 'get_supported_file_types', - HandleExportPresetsToFile = 'handle_export_presets_to_file', - HandleImportPresetsFromFile = 'handle_import_presets_from_file', - HandleImportLegacyPresetsFromFile = 'handle_import_legacy_presets_from_file', - ImportFiles = 'import_files', - InvokeGenerativeReplace = 'invoke_generative_replace', - InvokeGenerativeReplaseWithMaskDef = 'invoke_generative_replace_with_mask_def', - ListImagesInDir = 'list_images_in_dir', - ListImagesRecursive = 'list_images_recursive', - LoadImage = 'load_image', - LoadMetadata = 'load_metadata', - LoadPresets = 'load_presets', - LoadSettings = 'load_settings', - MoveFiles = 'move_files', - ReadExifForPaths = 'read_exif_for_paths', - RemoveTagForPaths = 'remove_tag_for_paths', - RenameFiles = 'rename_files', - RenameFolder = 'rename_folder', - ResetAdjustmentsForPaths = 'reset_adjustments_for_paths', - SaveMetadataAndUpdateThumbnail = 'save_metadata_and_update_thumbnail', - SaveCollage = 'save_collage', - SaveDenoisedImage = 'save_denoised_image', - SavePanorama = 'save_panorama', - SaveHdr = 'save_hdr', - SavePresets = 'save_presets', - SaveSettings = 'save_settings', - SetColorLabelForPaths = 'set_color_label_for_paths', - ShowInFinder = 'show_in_finder', - StartBackgroundIndexing = 'start_background_indexing', - StitchPanorama = 'stitch_panorama', - MergeHdr = 'merge_hdr', - TestAIConnectorConnection = 'test_ai_connector_connection', - UpdateWindowEffect = 'update_window_effect', - FetchCommunityPresets = 'fetch_community_presets', - GenerateAllCommunityPreviews = 'generate_all_community_previews', - SaveCommunityPreset = 'save_community_preset', - SaveTempFile = 'save_temp_file', -} - export enum Panel { Adjustments = 'adjustments', Ai = 'ai', diff --git a/src/context/TaggingSubMenu.tsx b/src/context/TaggingSubMenu.tsx index 3af7c3741..869d2f273 100644 --- a/src/context/TaggingSubMenu.tsx +++ b/src/context/TaggingSubMenu.tsx @@ -1,8 +1,7 @@ import { useState, useEffect, useRef } from 'react'; -import { invoke } from '@tauri-apps/api/core'; import { X, Plus } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; -import { Invokes } from '../components/ui/AppProperties'; +import { commands } from '../bindings'; interface TaggingSubMenuProps { paths: string[]; @@ -43,7 +42,7 @@ export default function TaggingSubMenu({ if (newTagValue && !tags.some((t) => t.tag === newTagValue)) { try { const prefixedTag = `${USER_TAG_PREFIX}${newTagValue}`; - await invoke(Invokes.AddTagForPaths, { paths, tag: prefixedTag }); + await commands.addTagForPaths(paths, prefixedTag); const newTags = [...tags, { tag: newTagValue, isUser: true }].sort((a, b) => a.tag.localeCompare(b.tag)); setTags(newTags); onTagsChanged(paths, newTags); @@ -57,7 +56,7 @@ export default function TaggingSubMenu({ const handleRemoveTag = async (tagToRemove: { tag: string; isUser: boolean }) => { try { const prefixedTag = tagToRemove.isUser ? `${USER_TAG_PREFIX}${tagToRemove.tag}` : tagToRemove.tag; - await invoke(Invokes.RemoveTagForPaths, { paths, tag: prefixedTag }); + await commands.removeTagForPaths(paths, prefixedTag); const newTags = tags.filter((t) => t.tag !== tagToRemove.tag); setTags(newTags); onTagsChanged(paths, newTags); diff --git a/src/hooks/usePresets.ts b/src/hooks/usePresets.ts index 1b334eecc..17531e968 100644 --- a/src/hooks/usePresets.ts +++ b/src/hooks/usePresets.ts @@ -1,8 +1,8 @@ import { useState, useEffect, useCallback } from 'react'; -import { invoke } from '@tauri-apps/api/core'; import debounce from 'lodash.debounce'; +import { commands } from '../bindings'; import { Adjustments, COPYABLE_ADJUSTMENT_KEYS } from '../utils/adjustments'; -import { Folder, Invokes, Preset } from '../components/ui/AppProperties'; +import { Folder, Preset } from '../components/ui/AppProperties'; export enum PresetListType { Folder = 'folder', @@ -30,7 +30,7 @@ export function usePresets(currentAdjustments: Adjustments) { const loadPresets = useCallback(async () => { setIsLoading(true); try { - const loadedPresets: Array = await invoke(Invokes.LoadPresets); + const loadedPresets: Array = await commands.loadPresets(); console.log(loadedPresets); setPresets(loadedPresets); } catch (error) { @@ -44,7 +44,7 @@ export function usePresets(currentAdjustments: Adjustments) { const savePresetsToBackend = useCallback( debounce((presetsToSave: Array) => { console.log(presetsToSave); - invoke(Invokes.SavePresets, { presets: presetsToSave }).catch((err) => + commands.savePresets(presetsToSave).catch((err) => console.error('Failed to save presets:', err), ); }, 500), @@ -392,7 +392,7 @@ export function usePresets(currentAdjustments: Adjustments) { async (filePath: string) => { setIsLoading(true); try { - const updatedPresetList: Array = await invoke(Invokes.HandleImportPresetsFromFile, { filePath }); + const updatedPresetList: Array = await commands.handleImportPresetsFromFile(filePath); setPresets(updatedPresetList); } catch (error) { console.error('Failed to import presets from file:', error); @@ -408,9 +408,7 @@ export function usePresets(currentAdjustments: Adjustments) { async (filePath: string) => { setIsLoading(true); try { - const updatedPresetList: Array = await invoke(Invokes.HandleImportLegacyPresetsFromFile, { - filePath, - }); + const updatedPresetList: Array = await commands.handleImportLegacyPresetsFromFile(filePath); setPresets(updatedPresetList); } catch (error) { console.error('Failed to import legacy presets from file:', error); @@ -424,7 +422,7 @@ export function usePresets(currentAdjustments: Adjustments) { const exportPresetsToFile = useCallback(async (presetsToExport: Array, filePath: string) => { try { - await invoke(Invokes.HandleExportPresetsToFile, { presetsToExport, filePath }); + await commands.handleExportPresetsToFile(presetsToExport, filePath); } catch (error) { console.error('Failed to export presets to file:', error); throw error; diff --git a/src/hooks/useThumbnails.tsx b/src/hooks/useThumbnails.tsx index 37674b6d4..343543ba9 100644 --- a/src/hooks/useThumbnails.tsx +++ b/src/hooks/useThumbnails.tsx @@ -1,7 +1,6 @@ import { useState, useEffect, useRef } from 'react'; -import { invoke } from '@tauri-apps/api/core'; -import { listen } from '@tauri-apps/api/event'; -import { ImageFile, Invokes, Progress } from '../components/ui/AppProperties'; +import { commands, events } from '../bindings'; +import { ImageFile, Progress } from '../components/ui/AppProperties'; export function useThumbnails(imageList: Array, setThumbnails: any) { const [loading, setLoading] = useState(false); @@ -51,17 +50,17 @@ export function useThumbnails(imageList: Array, setThumbnails: any) { setLoading(true); setProgress({ completed: 0, total: imagePaths.length }); - unlistenProgress = await listen('thumbnail-progress', (event: any) => { + unlistenProgress = await events.thumbnailProgress.listen((event: any) => { const { completed, total } = event.payload; setProgress({ completed, total }); }); - unlistenComplete = await listen('thumbnail-generation-complete', () => { + unlistenComplete = await events.thumbnailGenerationComplete.listen(() => { setLoading(false); }); try { - await invoke(Invokes.GenerateThumbnailsProgressive, { paths: imagePaths }); + await commands.generateThumbnailsProgressive(imagePaths); } catch (error) { console.error('Failed to invoke thumbnail generation:', error); setLoading(false); @@ -81,4 +80,4 @@ export function useThumbnails(imageList: Array, setThumbnails: any) { }, [imageList, setThumbnails]); return { loading, progress }; -} \ No newline at end of file +} diff --git a/src/utils/frontendLogBridge.ts b/src/utils/frontendLogBridge.ts index 03d067851..36735f7ea 100644 --- a/src/utils/frontendLogBridge.ts +++ b/src/utils/frontendLogBridge.ts @@ -1,5 +1,4 @@ -import { invoke } from '@tauri-apps/api/core'; -import { Invokes } from '../components/ui/AppProperties'; +import { commands } from '../bindings'; type FrontendLogLevel = 'debug' | 'info' | 'warn' | 'error'; @@ -242,10 +241,10 @@ function sendToBackend(level: FrontendLogLevel, args: unknown[]): void { return; } - void invoke(Invokes.FrontendLog, { + void commands.frontendLog( level, message, - }).catch(() => { + ).catch(() => { // Prevent recursion if backend logging channel is unavailable. }); }