diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index 64c70e031..eb237a2e8 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -1914,5 +1914,7 @@ "Failed": "Fehlgeschlagen", "Stopped": "Angehalten", "Stopped by an error (see the log)": "Durch einen Fehler angehalten (siehe Protokoll)", - "Not saved": "Nicht gespeichert" + "Not saved": "Nicht gespeichert", + "A folder is being synchronized": "Ein Ordner wird gerade synchronisiert", + "Wait for the import to finish": "Warten Sie, bis der Import abgeschlossen ist" } diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index d023cf6c4..c40f6372c 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -1759,5 +1759,7 @@ "Failed": "Con errores", "Stopped": "Detenido", "Stopped by an error (see the log)": "Detenido por un error (ver el registro)", - "Not saved": "Sin guardar" + "Not saved": "Sin guardar", + "A folder is being synchronized": "Se está sincronizando una carpeta", + "Wait for the import to finish": "Espera a que termine la importación" } diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index 9c5072025..bd55af76c 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -1759,5 +1759,7 @@ "Failed": "失敗", "Stopped": "停止しました", "Stopped by an error (see the log)": "エラーで停止しました(ログを参照)", - "Not saved": "保存されていません" + "Not saved": "保存されていません", + "A folder is being synchronized": "フォルダを同期中です", + "Wait for the import to finish": "読み込みが終わるまでお待ちください" } diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index 21a3d0c84..7e9511241 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -1759,5 +1759,7 @@ "Failed": "Com falha", "Stopped": "Interrompido", "Stopped by an error (see the log)": "Interrompido por um erro (veja o log)", - "Not saved": "Não salvo" + "Not saved": "Não salvo", + "A folder is being synchronized": "Uma pasta está sendo sincronizada", + "Wait for the import to finish": "Aguarde a importação terminar" } diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index 952ef5c92..4f3689f7d 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -1433,5 +1433,7 @@ "Failed": "Ошибки", "Stopped": "Остановлено", "Stopped by an error (see the log)": "Остановлено из-за ошибки (см. журнал)", - "Not saved": "Не сохранено" + "Not saved": "Не сохранено", + "A folder is being synchronized": "Идёт синхронизация папки", + "Wait for the import to finish": "Дождитесь окончания импорта" } diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index a67df15a8..f12273d05 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -1759,5 +1759,7 @@ "Failed": "失败", "Stopped": "已停止", "Stopped by an error (see the log)": "因错误而停止(见日志)", - "Not saved": "未保存" + "Not saved": "未保存", + "A folder is being synchronized": "正在同步文件夹", + "Wait for the import to finish": "请等待导入完成" } diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index 3d179de54..f7e7c35e7 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -1759,5 +1759,7 @@ "Failed": "失敗", "Stopped": "已停止", "Stopped by an error (see the log)": "因錯誤而停止(請見記錄)", - "Not saved": "未儲存" + "Not saved": "未儲存", + "A folder is being synchronized": "正在同步資料夾", + "Wait for the import to finish": "請等待匯入完成" } diff --git a/crates/ui-egui/src/import.rs b/crates/ui-egui/src/import.rs index 49e3a2664..4b1e40770 100644 --- a/crates/ui-egui/src/import.rs +++ b/crates/ui-egui/src/import.rs @@ -393,6 +393,12 @@ pub struct ScanTask { sources: Vec, } +/// An import and a Synchronize Folder never run at once: each readies its files against the +/// library as it was when it started, so both could add the same file. +pub(crate) fn busy_synchronizing(app: &LightcraftApp) -> Result<(), String> { + if app.sync_run.is_some() { Err(crate::i18n::tr("A folder is being synchronized").to_string()) } else { Ok(()) } +} + /// Scan `paths` in the background, then open the review dialog (see [`poll_scan`]). pub fn open(app: &mut LightcraftApp, paths: Vec) -> Result { if app.scan.is_some() { @@ -565,6 +571,7 @@ impl ScanTask { /// Start importing the dialog's checked files (the dialog's OK / `ui.dialog.confirm`). pub fn start(app: &mut LightcraftApp, d: &ImportDialog) -> Result { + busy_synchronizing(app)?; let queue = d.selected_paths(); if queue.is_empty() { return Err("no photos selected".into()); @@ -616,6 +623,7 @@ pub fn start(app: &mut LightcraftApp, d: &ImportDialog) -> Result /// Start importing `paths` (files or folders) in the background, e.g. dropped on the window. pub fn start_paths(app: &mut LightcraftApp, paths: Vec) -> Result { + busy_synchronizing(app)?; if app.import.is_some() || app.scan.as_ref().is_some_and(|t| !t.browse) { return Err("an import is running".into()); } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 503c50b28..05591366a 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -772,7 +772,12 @@ impl LightcraftApp { if let Some(folder) = self.session.import_defaults.auto_folder.clone() { const LABEL: &str = "Auto Import"; let now = ctx.input(|i| i.time); - if now - self.ui.auto_import_at >= 3.0 && self.import.is_none() && self.lightroom.is_none() && !self.tasks.is_running(LABEL) { + if now - self.ui.auto_import_at >= 3.0 + && self.import.is_none() + && self.sync_run.is_none() + && self.lightroom.is_none() + && !self.tasks.is_running(LABEL) + { self.ui.auto_import_at = now; let work = move || lightcraft_engine::cmd::library::list_auto_import_folder(&folder); let done = |app: &mut LightcraftApp, _ctx: &egui::Context, listing: Result, String>| { diff --git a/crates/ui-egui/src/sync.rs b/crates/ui-egui/src/sync.rs index b60243ac1..244f4308c 100644 --- a/crates/ui-egui/src/sync.rs +++ b/crates/ui-egui/src/sync.rs @@ -172,6 +172,10 @@ pub fn confirm(app: &mut LightcraftApp, dlg: &Dialog) -> Result { if app.sync_run.is_some() { return Err(crate::i18n::tr("A folder is already being synchronized").to_string()); } + // (an import readies its files against the library as it was: both could add the same file) + if app.import.is_some() { + return Err(crate::i18n::tr("Wait for the import to finish").to_string()); + } let choice = SyncChoice { import_new: *import_new, relink_moved: *relink_moved, remove_missing: *remove_missing, read_metadata: *read_metadata }; app.sync_owns_changes = false; let started = match app.session.take_folder_changes(path) { diff --git a/crates/ui-egui/src/tests_sync.rs b/crates/ui-egui/src/tests_sync.rs index dff960bc9..59fd041f5 100644 --- a/crates/ui-egui/src/tests_sync.rs +++ b/crates/ui-egui/src/tests_sync.rs @@ -217,3 +217,32 @@ fn a_stopped_synchronize_says_it_was_stopped_and_stays_at_most_one_step() { assert!(toast.contains("Stopped"), "never 'up to date': {toast:?}"); assert!(h.app.session.undo.len() <= steps + 1, "what was done by then is at most one step"); } + +#[test] +fn a_synchronize_and_an_import_never_run_at_once() { + let dir = Scratch::new("exclusive"); + let mut h = changed_folder(&dir); + open_and_scan(&mut h, &dir.path("trip")); + let dlg = h.app.ui.dialog.take().expect("the dialog is open"); + assert!(crate::panels::dialogs::confirm_dialog(&mut h.app, &dlg).is_ok()); + // dropping the same folder on the window while the run works would add its files twice + let r = crate::import::start_paths(&mut h.app, vec![dir.path("trip")]); + assert!(r.is_err(), "no import while synchronizing: {r:?}"); + assert!(h.step_until(T, |h| h.app.sync_run.is_none())); + // and the other way round: an import is running (no frame has run since it started) + let r = crate::import::start_paths(&mut h.app, vec![dir.path("trip/a.png")]); + assert!(r.is_ok(), "{r:?}"); + assert!(h.app.import.is_some()); + let d = Dialog::SynchronizeFolder { + path: dir.path("trip"), + name: "trip".into(), + disk: false, + counts: Some(Default::default()), + import_new: true, + relink_moved: true, + remove_missing: false, + read_metadata: false, + }; + let r = crate::panels::dialogs::confirm_dialog(&mut h.app, &d); + assert!(r.is_err() && h.app.sync_run.is_none(), "no synchronize while importing: {r:?}"); +}