Skip to content

Commit 0cd162a

Browse files
committed
fix: move auto adjust selected images in new thread to unblock UI
This also includes a small progress view.
1 parent 3c077b2 commit 0cd162a

2 files changed

Lines changed: 151 additions & 103 deletions

File tree

src-tauri/src/file_management.rs

Lines changed: 86 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -2030,127 +2030,119 @@ pub fn apply_auto_adjustments_to_paths(
20302030
paths: Vec<String>,
20312031
app_handle: AppHandle,
20322032
) -> Result<(), String> {
2033-
let settings = load_settings(app_handle.clone()).unwrap_or_default();
2034-
let highlight_compression = settings.raw_highlight_compression.unwrap_or(2.5);
2035-
let linear_mode = settings.linear_raw_mode;
2036-
let enable_xmp_sync = settings.enable_xmp_sync.unwrap_or(false);
2037-
let create_xmp_if_missing = settings.create_xmp_if_missing.unwrap_or(false);
2033+
thread::spawn(move || {
2034+
let settings = load_settings(app_handle.clone()).unwrap_or_default();
2035+
let highlight_compression = settings.raw_highlight_compression.unwrap_or(2.5);
2036+
let linear_mode = settings.linear_raw_mode;
2037+
let enable_xmp_sync = settings.enable_xmp_sync.unwrap_or(false);
2038+
let create_xmp_if_missing = settings.create_xmp_if_missing.unwrap_or(false);
20382039

2039-
paths.par_iter().for_each(|path| {
2040-
let result: Result<(), String> = (|| {
2041-
let (source_path, sidecar_path) = parse_virtual_path(path);
2042-
let source_path_str = source_path.to_string_lossy().to_string();
2040+
let state = app_handle.state::<AppState>();
2041+
let gpu_context = gpu_processing::get_or_init_gpu_context(&state).ok();
2042+
let thumb_cache_dir = resolve_thumbnail_cache_dir(&app_handle).ok();
20432043

2044-
let file_bytes = fs::read(&source_path).map_err(|e| e.to_string())?;
2045-
let image = image_loader::load_base_image_from_bytes(
2046-
&file_bytes,
2047-
&source_path_str,
2048-
false,
2049-
highlight_compression,
2050-
linear_mode.clone(),
2051-
None,
2052-
)
2053-
.map_err(|e| e.to_string())?;
2044+
let total_count = paths.len();
2045+
let completed_count = Arc::new(AtomicUsize::new(0));
20542046

2055-
let auto_results = perform_auto_analysis(&image);
2056-
let auto_adjustments_json = auto_results_to_json(&auto_results);
2047+
paths.par_iter().for_each(|path| {
2048+
let result: Result<(), String> = (|| {
2049+
let (source_path, sidecar_path) = parse_virtual_path(path);
2050+
let source_path_str = source_path.to_string_lossy().to_string();
20572051

2058-
let mut existing_metadata: ImageMetadata = if sidecar_path.exists() {
2059-
fs::read_to_string(&sidecar_path)
2060-
.ok()
2061-
.and_then(|content| serde_json::from_str(&content).ok())
2062-
.unwrap_or_default()
2063-
} else {
2064-
ImageMetadata::default()
2065-
};
2052+
let file_bytes = fs::read(&source_path).map_err(|e| e.to_string())?;
2053+
let image = image_loader::load_base_image_from_bytes(
2054+
&file_bytes,
2055+
&source_path_str,
2056+
false,
2057+
highlight_compression,
2058+
linear_mode.clone(),
2059+
None,
2060+
)
2061+
.map_err(|e| e.to_string())?;
20662062

2067-
if existing_metadata.adjustments.is_null() {
2068-
existing_metadata.adjustments = serde_json::json!({});
2069-
}
2063+
let auto_results = perform_auto_analysis(&image);
2064+
let auto_adjustments_json = auto_results_to_json(&auto_results);
2065+
2066+
let mut existing_metadata: ImageMetadata = if sidecar_path.exists() {
2067+
fs::read_to_string(&sidecar_path)
2068+
.ok()
2069+
.and_then(|content| serde_json::from_str(&content).ok())
2070+
.unwrap_or_default()
2071+
} else {
2072+
ImageMetadata::default()
2073+
};
2074+
2075+
if existing_metadata.adjustments.is_null() {
2076+
existing_metadata.adjustments = serde_json::json!({});
2077+
}
20702078

2071-
if let (Some(existing_map), Some(auto_map)) = (
2072-
existing_metadata.adjustments.as_object_mut(),
2073-
auto_adjustments_json.as_object(),
2074-
) {
2075-
for (k, v) in auto_map {
2076-
if k == "sectionVisibility" {
2077-
if let Some(existing_vis_val) = existing_map.get_mut(k) {
2078-
if let (Some(existing_vis), Some(auto_vis)) =
2079-
(existing_vis_val.as_object_mut(), v.as_object())
2080-
{
2081-
for (vis_k, vis_v) in auto_vis {
2082-
existing_vis.insert(vis_k.clone(), vis_v.clone());
2079+
if let (Some(existing_map), Some(auto_map)) = (
2080+
existing_metadata.adjustments.as_object_mut(),
2081+
auto_adjustments_json.as_object(),
2082+
) {
2083+
for (k, v) in auto_map {
2084+
if k == "sectionVisibility" {
2085+
if let Some(existing_vis_val) = existing_map.get_mut(k) {
2086+
if let (Some(existing_vis), Some(auto_vis)) =
2087+
(existing_vis_val.as_object_mut(), v.as_object())
2088+
{
2089+
for (vis_k, vis_v) in auto_vis {
2090+
existing_vis.insert(vis_k.clone(), vis_v.clone());
2091+
}
20832092
}
2093+
} else {
2094+
existing_map.insert(k.clone(), v.clone());
20842095
}
20852096
} else {
20862097
existing_map.insert(k.clone(), v.clone());
20872098
}
2088-
} else {
2089-
existing_map.insert(k.clone(), v.clone());
20902099
}
20912100
}
2092-
}
20932101

2094-
existing_metadata.rating = existing_metadata.adjustments["rating"]
2095-
.as_u64()
2096-
.unwrap_or(0) as u8;
2102+
existing_metadata.rating = existing_metadata.adjustments["rating"]
2103+
.as_u64()
2104+
.unwrap_or(0) as u8;
20972105

2098-
if let Ok(json_string) = serde_json::to_string_pretty(&existing_metadata) {
2099-
let _ = std::fs::write(&sidecar_path, json_string);
2100-
}
2101-
2102-
if enable_xmp_sync {
2103-
sync_metadata_to_xmp(&source_path, &existing_metadata, create_xmp_if_missing);
2104-
}
2105-
Ok(())
2106-
})();
2107-
if let Err(e) = result {
2108-
eprintln!("Failed to apply auto adjustments to {}: {}", path, e);
2109-
}
2110-
});
2111-
2112-
thread::spawn(move || {
2113-
let state = app_handle.state::<AppState>();
2114-
let thumb_cache_dir = match resolve_thumbnail_cache_dir(&app_handle) {
2115-
Ok(dir) => dir,
2116-
Err(e) => {
2117-
log::warn!("Unable to initialize thumbnail cache directory: {}", e);
2118-
for path in &paths {
2119-
emit_thumbnail_cache_setup_error(&app_handle, path, &e);
2106+
if let Ok(json_string) = serde_json::to_string_pretty(&existing_metadata) {
2107+
let _ = std::fs::write(&sidecar_path, json_string);
21202108
}
2121-
let _ = app_handle.emit("thumbnail-generation-complete", true);
2122-
return;
2123-
}
2124-
};
21252109

2126-
let gpu_context = gpu_processing::get_or_init_gpu_context(&state).ok();
2127-
let total_count = paths.len();
2128-
let completed_count = Arc::new(AtomicUsize::new(0));
2110+
if enable_xmp_sync {
2111+
sync_metadata_to_xmp(&source_path, &existing_metadata, create_xmp_if_missing);
2112+
}
21292113

2130-
paths.par_iter().for_each(|path_str| {
2131-
let result = generate_single_thumbnail_and_cache(
2132-
path_str,
2133-
&thumb_cache_dir,
2134-
gpu_context.as_ref(),
2135-
None,
2136-
true,
2137-
&app_handle,
2138-
);
2114+
// Regenerate thumbnail immediately using the already-decoded image,
2115+
// so the UI updates without waiting for the full batch to finish.
2116+
if let Some(cache_dir) = &thumb_cache_dir {
2117+
if let Some((thumbnail_data, rating)) = generate_single_thumbnail_and_cache(
2118+
path,
2119+
cache_dir,
2120+
gpu_context.as_ref(),
2121+
Some(&image),
2122+
true,
2123+
&app_handle,
2124+
) {
2125+
let _ = app_handle.emit(
2126+
"thumbnail-generated",
2127+
serde_json::json!({ "path": path, "data": thumbnail_data, "rating": rating }),
2128+
);
2129+
}
2130+
}
21392131

2140-
if let Some((thumbnail_data, rating)) = result {
2141-
let _ = app_handle.emit(
2142-
"thumbnail-generated",
2143-
serde_json::json!({ "path": path_str, "data": thumbnail_data, "rating": rating }),
2144-
);
2132+
Ok(())
2133+
})();
2134+
if let Err(e) = result {
2135+
eprintln!("Failed to apply auto adjustments to {}: {}", path, e);
21452136
}
21462137

21472138
let completed = completed_count.fetch_add(1, Ordering::Relaxed) + 1;
21482139
let _ = app_handle.emit(
2149-
"thumbnail-progress",
2140+
"auto-adjust-progress",
21502141
serde_json::json!({ "completed": completed, "total": total_count }),
21512142
);
21522143
});
21532144

2145+
let _ = app_handle.emit("auto-adjust-complete", true);
21542146
let _ = app_handle.emit("thumbnail-generation-complete", true);
21552147
});
21562148

src/App.tsx

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,8 @@ function App() {
497497
status: Status.Idle,
498498
});
499499

500+
const [autoAdjustProgress, setAutoAdjustProgress] = useState<{ completed: number; total: number } | null>(null);
501+
500502
useEffect(() => {
501503
currentFolderPathRef.current = currentFolderPath;
502504
}, [currentFolderPath]);
@@ -3235,6 +3237,16 @@ function App() {
32353237
}));
32363238
}
32373239
}),
3240+
listen('auto-adjust-progress', (event: any) => {
3241+
if (isEffectActive) {
3242+
setAutoAdjustProgress({ completed: event.payload.completed, total: event.payload.total });
3243+
}
3244+
}),
3245+
listen('auto-adjust-complete', () => {
3246+
if (isEffectActive) {
3247+
setAutoAdjustProgress(null);
3248+
}
3249+
}),
32383250
];
32393251
return () => {
32403252
isEffectActive = false;
@@ -4276,9 +4288,13 @@ function App() {
42764288
if (finalSelection.length === 0) return;
42774289
finalSelection.forEach((p) => imageCacheRef.current.delete(p));
42784290

4279-
invoke(Invokes.ApplyAutoAdjustmentsToPaths, { paths: finalSelection })
4280-
.then(async () => {
4281-
if (selectedImage && finalSelection.includes(selectedImage.path)) {
4291+
setAutoAdjustProgress({ completed: 0, total: finalSelection.length });
4292+
4293+
const selectionSnapshot = [...finalSelection];
4294+
const unlistenComplete = listen('auto-adjust-complete', async () => {
4295+
unlistenComplete.then((ul) => ul());
4296+
if (selectedImage && selectionSnapshot.includes(selectedImage.path)) {
4297+
try {
42824298
const metadata: Metadata = await invoke(Invokes.LoadMetadata, {
42834299
path: selectedImage.path,
42844300
});
@@ -4287,21 +4303,31 @@ function App() {
42874303
setLiveAdjustments(normalized);
42884304
resetAdjustmentsHistory(normalized);
42894305
}
4306+
} catch (err) {
4307+
console.error('Failed to reload adjustments after auto-adjust:', err);
42904308
}
4291-
if (libraryActivePath && finalSelection.includes(libraryActivePath)) {
4309+
}
4310+
if (libraryActivePath && selectionSnapshot.includes(libraryActivePath)) {
4311+
try {
42924312
const metadata: Metadata = await invoke(Invokes.LoadMetadata, {
42934313
path: libraryActivePath,
42944314
});
42954315
if (metadata.adjustments && !metadata.adjustments.is_null) {
42964316
const normalized = normalizeLoadedAdjustments(metadata.adjustments);
42974317
setLibraryActiveAdjustments(normalized);
42984318
}
4319+
} catch (err) {
4320+
console.error('Failed to reload library adjustments after auto-adjust:', err);
42994321
}
4300-
})
4301-
.catch((err) => {
4302-
console.error('Failed to apply auto adjustments to paths:', err);
4303-
setError(`Failed to apply auto adjustments: ${err}`);
4304-
});
4322+
}
4323+
});
4324+
4325+
invoke(Invokes.ApplyAutoAdjustmentsToPaths, { paths: finalSelection }).catch((err) => {
4326+
unlistenComplete.then((ul) => ul());
4327+
setAutoAdjustProgress(null);
4328+
console.error('Failed to apply auto adjustments to paths:', err);
4329+
setError(`Failed to apply auto adjustments: ${err}`);
4330+
});
43054331
};
43064332

43074333
const onExportClick = () => {
@@ -5423,6 +5449,36 @@ function App() {
54235449
sourceImages={collageModalState.sourceImages}
54245450
thumbnails={thumbnails}
54255451
/>
5452+
<AnimatePresence>
5453+
{autoAdjustProgress !== null && (
5454+
<motion.div
5455+
animate={{ opacity: 1, y: 0 }}
5456+
className="fixed bottom-6 right-6 z-[9999] flex flex-col gap-2 rounded-xl border border-border-color bg-surface px-5 py-4 shadow-2xl"
5457+
exit={{ opacity: 0, y: 16 }}
5458+
initial={{ opacity: 0, y: 16 }}
5459+
transition={{ duration: 0.2 }}
5460+
>
5461+
<div className="flex items-center gap-3">
5462+
<Gauge className="h-4 w-4 shrink-0 text-text-secondary" />
5463+
<span className="text-sm font-medium text-text-primary">Applying Auto Adjustments</span>
5464+
</div>
5465+
<div className="flex items-center gap-3">
5466+
<div className="h-1.5 w-48 overflow-hidden rounded-full bg-surface-secondary">
5467+
<motion.div
5468+
animate={{
5469+
width: `${autoAdjustProgress.total > 0 ? (autoAdjustProgress.completed / autoAdjustProgress.total) * 100 : 0}%`,
5470+
}}
5471+
className="h-full rounded-full bg-accent"
5472+
transition={{ duration: 0.15 }}
5473+
/>
5474+
</div>
5475+
<span className="text-xs text-text-secondary">
5476+
{autoAdjustProgress.completed} / {autoAdjustProgress.total}
5477+
</span>
5478+
</div>
5479+
</motion.div>
5480+
)}
5481+
</AnimatePresence>
54265482
<ToastContainer
54275483
position="bottom-right"
54285484
autoClose={5000}

0 commit comments

Comments
 (0)