Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion src-tauri/src/export_processing.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::{BTreeSet, HashMap};
use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -1447,6 +1447,15 @@ fn export_adjustments_as_lut(
Ok(cube_lut)
}

fn record_export_folder(path: &Path, folders: &Mutex<BTreeSet<String>>) {
if let Some(parent) = path.parent() {
folders
.lock()
.unwrap()
.insert(parent.to_string_lossy().into_owned());
}
}

#[allow(clippy::too_many_arguments)]
pub(crate) async fn export_images_impl(
paths: Vec<String>,
Expand Down Expand Up @@ -1546,6 +1555,7 @@ pub(crate) async fn export_images_impl(
}

let used_paths = Arc::new(Mutex::new(std::collections::HashSet::new()));
let output_folders = Arc::new(Mutex::new(BTreeSet::new()));
let semaphore = Arc::new(tokio::sync::Semaphore::new(num_threads));
let mut join_handles = Vec::new();

Expand All @@ -1570,6 +1580,7 @@ pub(crate) async fn export_images_impl(
let cancellation_token_clone = Arc::clone(&cancellation_token);
let adjustments_mode = adjustments_mode.clone();
let used_paths_clone = Arc::clone(&used_paths);
let output_folders_clone = Arc::clone(&output_folders);

let handle = tokio::task::spawn_blocking(move || {
ensure_export_not_cancelled(&cancellation_token_clone)?;
Expand Down Expand Up @@ -1722,6 +1733,7 @@ pub(crate) async fn export_images_impl(
}
#[cfg(not(target_os = "android"))]
fs::write(&output_path, cube_bytes).map_err(|e| e.to_string())?;
record_export_folder(&output_path, &output_folders_clone);
ensure_export_not_cancelled(&cancellation_token_clone)?;
return Ok(());
}
Expand Down Expand Up @@ -1801,6 +1813,7 @@ pub(crate) async fn export_images_impl(
&source_path_str,
&export_settings,
)?;
record_export_folder(&output_path, &output_folders_clone);
ensure_export_not_cancelled(&cancellation_token_clone)?;

if export_settings.preserve_timestamps {
Expand Down Expand Up @@ -1865,6 +1878,10 @@ pub(crate) async fn export_images_impl(
&export_state.export_task_token,
&cancellation_token,
|cancelled| {
let folders: Vec<_> = output_folders.lock().unwrap().iter().cloned().collect();
if !folders.is_empty() {
let _ = app_handle.emit("export-outputs", &folders);
}
if cancelled {
log::info!("Batch export cancelled and worker cleanup completed");
let _ = app_handle.emit("export-cancelled", ());
Expand Down Expand Up @@ -2352,3 +2369,28 @@ pub async fn estimate_export_sizes(

Ok(single_image_extrapolated_size * paths.len())
}

#[cfg(test)]
mod export_folder_tests {
use super::*;

#[test]
fn deduplicates_written_output_folders() {
let folders = Mutex::new(BTreeSet::new());
for path in [
"exports/scene-a/first.tiff",
"exports/scene-a/second.tiff",
"photos/edited.cube",
] {
record_export_folder(Path::new(path), &folders);
}
assert_eq!(
folders
.into_inner()
.unwrap()
.into_iter()
.collect::<Vec<_>>(),
vec!["exports/scene-a", "photos"]
);
}
}
24 changes: 22 additions & 2 deletions src/hooks/useTauriListeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import { useEditorStore } from '../store/useEditorStore';
import { useUIStore } from '../store/useUIStore';
import { useLibraryStore } from '../store/useLibraryStore';

function normalizeFolderPath(path: string): string {
const isWindowsPath = /^[a-z]:[\\/]/i.test(path) || path.startsWith('\\\\') || path.startsWith('//');
const normalized = isWindowsPath ? path.replace(/\\/g, '/').toLowerCase() : path;
return normalized.replace(/\/+$/, '') || '/';
}

interface TauriListenerProps {
refreshAllFolderTrees: () => void;
handleSelectSubfolder: (path: string, isNewRoot?: boolean, preloadedImages?: any[], expandParents?: boolean) => void;
Expand Down Expand Up @@ -160,21 +166,35 @@ export function useTauriListeners({
listen('batch-export-progress', (event: any) => {
if (isEffectActive) useProcessStore.getState().setExportState({ progress: event.payload });
}),
listen<string[]>('export-outputs', (event) => {
if (!isEffectActive) return;
const currentPath = useLibraryStore.getState().currentFolderPath;
if (
currentPath &&
event.payload.some((path) => normalizeFolderPath(path) === normalizeFolderPath(currentPath))
) {
refs.current.refreshAllFolderTrees();
refs.current.refreshImageList();
}
}),
listen('export-complete', () => {
if (isEffectActive) useProcessStore.getState().setExportState({ status: Status.Success });
}),
listen('export-error', (event: any) => {
if (isEffectActive)
if (isEffectActive) {
useProcessStore.getState().setExportState({
status: Status.Error,
errorMessage: typeof event.payload === 'string' ? event.payload : 'Unknown error',
});
}
}),
listen('export-cancelling', () => {
if (isEffectActive) useProcessStore.getState().setExportState({ status: Status.Cancelling });
}),
listen('export-cancelled', () => {
if (isEffectActive) useProcessStore.getState().setExportState({ status: Status.Cancelled });
if (isEffectActive) {
useProcessStore.getState().setExportState({ status: Status.Cancelled });
}
}),
listen('import-start', (event: any) => {
if (isEffectActive)
Expand Down
Loading