diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index 9b5f1920..598ace82 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -2024,5 +2024,7 @@ "Scanning folder": "Ordner wird durchsucht", "Building previews": "Vorschauen werden erstellt", "Building smart previews": "Smart-Vorschauen werden erstellt", - "Discarding smart previews": "Smart-Vorschauen werden verworfen" + "Discarding smart previews": "Smart-Vorschauen werden verworfen", + "Importing Lightroom catalog": "Lightroom-Katalog wird importiert", + "Reading Lightroom catalog": "Lightroom-Katalog wird gelesen" } diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index 104e7596..f0740bd5 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -1869,5 +1869,7 @@ "Scanning folder": "Explorando carpeta", "Building previews": "Generando vistas previas", "Building smart previews": "Generando vistas previas inteligentes", - "Discarding smart previews": "Descartando vistas previas inteligentes" + "Discarding smart previews": "Descartando vistas previas inteligentes", + "Importing Lightroom catalog": "Importando catálogo de Lightroom", + "Reading Lightroom catalog": "Leyendo catálogo de Lightroom" } diff --git a/crates/ui-egui/locales/fr.json b/crates/ui-egui/locales/fr.json index cdbf6ee5..51dab1b2 100644 --- a/crates/ui-egui/locales/fr.json +++ b/crates/ui-egui/locales/fr.json @@ -1866,5 +1866,10 @@ "Quit while tasks are running?": "Quitter pendant que des tâches sont en cours ?", "Exporting contact sheet": "Exportation de la planche-contact", "Reading folder": "Lecture du dossier", - "Scanning folder": "Analyse du dossier" + "Scanning folder": "Analyse du dossier", + "Building previews": "Génération des aperçus", + "Building smart previews": "Génération des aperçus dynamiques", + "Discarding smart previews": "Suppression des aperçus dynamiques", + "Importing Lightroom catalog": "Importation du catalogue Lightroom", + "Reading Lightroom catalog": "Lecture du catalogue Lightroom" } diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index 02dbbc81..a10a08e1 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -1871,5 +1871,7 @@ "Scanning folder": "フォルダーをスキャン中", "Building previews": "プレビューを作成中", "Building smart previews": "スマートプレビューを作成中", - "Discarding smart previews": "スマートプレビューを破棄中" + "Discarding smart previews": "スマートプレビューを破棄中", + "Importing Lightroom catalog": "Lightroom カタログを読み込み中", + "Reading Lightroom catalog": "Lightroom カタログを読み取り中" } diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index fa1bcac4..d9d07b5f 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -1869,5 +1869,7 @@ "Scanning folder": "Verificando pasta", "Building previews": "Gerando prévias", "Building smart previews": "Gerando prévias inteligentes", - "Discarding smart previews": "Descartando prévias inteligentes" + "Discarding smart previews": "Descartando prévias inteligentes", + "Importing Lightroom catalog": "Importando catálogo do Lightroom", + "Reading Lightroom catalog": "Lendo catálogo do Lightroom" } diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index 453b1e21..3a442515 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -1582,5 +1582,7 @@ "Scanning folder": "Сканирование папки", "Building previews": "Создание просмотров", "Building smart previews": "Создание смарт-просмотров", - "Discarding smart previews": "Удаление смарт-просмотров" + "Discarding smart previews": "Удаление смарт-просмотров", + "Importing Lightroom catalog": "Импорт каталога Lightroom", + "Reading Lightroom catalog": "Чтение каталога Lightroom" } diff --git a/crates/ui-egui/locales/uk.json b/crates/ui-egui/locales/uk.json index 2c43493f..a18094f4 100644 --- a/crates/ui-egui/locales/uk.json +++ b/crates/ui-egui/locales/uk.json @@ -2060,5 +2060,10 @@ "Quit while tasks are running?": "Вийти, поки виконуються завдання?", "Exporting contact sheet": "Експорт аркуша мініатюр", "Reading folder": "Читання папки", - "Scanning folder": "Сканування папки" + "Scanning folder": "Сканування папки", + "Building previews": "Створення попередніх переглядів", + "Building smart previews": "Створення смарт-переглядів", + "Discarding smart previews": "Видалення смарт-переглядів", + "Importing Lightroom catalog": "Імпорт каталогу Lightroom", + "Reading Lightroom catalog": "Читання каталогу Lightroom" } diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index adcc16f8..c2c29b47 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -1869,5 +1869,7 @@ "Scanning folder": "正在扫描文件夹", "Building previews": "正在构建预览", "Building smart previews": "正在构建智能预览", - "Discarding smart previews": "正在丢弃智能预览" + "Discarding smart previews": "正在丢弃智能预览", + "Importing Lightroom catalog": "正在导入 Lightroom 目录", + "Reading Lightroom catalog": "正在读取 Lightroom 目录" } diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index 2c4eca30..e808c312 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -1869,5 +1869,7 @@ "Scanning folder": "正在掃描檔案夾", "Building previews": "正在建立預覽", "Building smart previews": "正在建立智慧型預覽", - "Discarding smart previews": "正在捨棄智慧型預覽" + "Discarding smart previews": "正在捨棄智慧型預覽", + "Importing Lightroom catalog": "正在匯入 Lightroom 目錄", + "Reading Lightroom catalog": "正在讀取 Lightroom 目錄" } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 03a23ef5..af75d195 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1066,7 +1066,6 @@ impl LightcraftApp { panels::dialogs::show(self, &ctx); panels::library_problem::show(self, &ctx); sync::progress_window(self, &ctx); - lightroom_import::progress(self, &ctx); export_task::poll(self, &ctx); panels::activity::show(self, &ctx); pick::poll(self, &ctx); diff --git a/crates/ui-egui/src/lightroom_import.rs b/crates/ui-egui/src/lightroom_import.rs index 8cee63e3..b05b381c 100644 --- a/crates/ui-egui/src/lightroom_import.rs +++ b/crates/ui-egui/src/lightroom_import.rs @@ -8,12 +8,10 @@ use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, mpsc}; -use egui::Align2; +use lightcraft_engine::activity::{Cancel, TaskGuard}; use serde_json::{Value, json}; use crate::LightcraftApp; -use crate::theme::Tokens; -use crate::widgets::register; #[derive(Clone, Copy)] enum Kind { @@ -34,7 +32,7 @@ enum Phase { Finalizing, } -/// A Lightroom task in flight. The receiver is polled from the egui owner thread. +/// A Lightroom task in flight. The receiver is polled from the egui owner thread; the activity stack shows it. pub struct LightroomTask { kind: Kind, path: PathBuf, @@ -44,9 +42,53 @@ pub struct LightroomTask { rx: mpsc::Receiver, phase: Phase, cancelled: bool, + guard: TaskGuard, } impl LightroomTask { + #[cfg(not(target_arch = "wasm32"))] + fn new( + s: &lightcraft_engine::Session, + kind: Kind, + path: PathBuf, + cancel: Arc, + total: Arc, + done: Arc, + rx: mpsc::Receiver, + ) -> Self { + let label = match kind { + Kind::Inspect => "Reading Lightroom catalog", + Kind::Import => "Importing Lightroom catalog", + }; + let guard = s.activity.start("lightroom", label, Cancel::Flag(cancel.clone())); + let task = Self { kind, path, cancel, total, done, rx, phase: Phase::Reading, cancelled: false, guard }; + task.refresh(); + task + } + + /// Move to `phase`; only reading the catalog can stop, adding the photos runs to the end. + fn enter(&mut self, phase: Phase) { + self.phase = phase; + self.guard.set_cancellable(phase == Phase::Reading); + self.refresh(); + } + + /// Bring the activity row up to date: the count while reading, then what is being done. + fn refresh(&self) { + let detail = match (self.phase, self.kind) { + (Phase::Reading, Kind::Inspect) => "", + (Phase::Reading, Kind::Import) => crate::i18n::tr("Reading Lightroom catalog…"), + (Phase::Committing, _) => crate::i18n::tr("Adding Lightroom photos…"), + (Phase::Finalizing, _) => crate::i18n::tr("Saving Lightroom import index…"), + }; + if self.phase == Phase::Reading { + self.guard.progress(self.done.load(Ordering::Relaxed) as u64, self.total.load(Ordering::Relaxed) as u64); + } else { + self.guard.progress(0, 0); + } + self.guard.detail(detail); + } + #[cfg(not(target_arch = "wasm32"))] fn status(&self) -> Value { let kind = match self.kind { @@ -236,7 +278,7 @@ fn start_inspect(app: &mut LightcraftApp, path: PathBuf, ctx: &egui::Context) -> let (tx, rx) = mpsc::channel(); spawn_inspect(path.clone(), cancel.clone(), total.clone(), done.clone(), tx, ctx.clone())?; app.lightroom_last = None; - app.lightroom = Some(LightroomTask { kind: Kind::Inspect, path: path.clone(), cancel, total, done, rx, phase: Phase::Reading, cancelled: false }); + app.lightroom = Some(LightroomTask::new(&app.session, Kind::Inspect, path.clone(), cancel, total, done, rx)); Ok(json!({"running": true, "kind": "inspect", "path": path})) } @@ -250,7 +292,7 @@ fn start_import(app: &mut LightcraftApp, path: PathBuf, update_existing: bool, c let (tx, rx) = mpsc::channel(); spawn_prepare(job, cancel.clone(), tx, ctx.clone())?; app.lightroom_last = None; - app.lightroom = Some(LightroomTask { kind: Kind::Import, path: path.clone(), cancel, total, done, rx, phase: Phase::Reading, cancelled: false }); + app.lightroom = Some(LightroomTask::new(&app.session, Kind::Import, path.clone(), cancel, total, done, rx)); Ok(json!({"running": true, "kind": "import", "path": path})) } @@ -296,6 +338,7 @@ pub fn tick(app: &mut LightcraftApp, ctx: &egui::Context) { let message = match task.rx.try_recv() { Ok(m) => m, Err(mpsc::TryRecvError::Empty) => { + task.refresh(); app.lightroom = Some(task); return; } @@ -337,7 +380,7 @@ pub fn tick(app: &mut LightcraftApp, ctx: &egui::Context) { ); return; } - task.phase = Phase::Committing; + task.enter(Phase::Committing); let mut finalization = None; let mut report = None; let committed = app.session.execute_fn("library.importLightroom", |s| { @@ -347,7 +390,7 @@ pub fn tick(app: &mut LightcraftApp, ctx: &egui::Context) { Ok(completion.report) }); let report = report.unwrap_or(Value::Null); - task.phase = Phase::Finalizing; + task.enter(Phase::Finalizing); let (tx, rx) = mpsc::channel(); let commit_error = committed.as_ref().err().map(ToString::to_string); let Some(finalization) = finalization else { @@ -409,50 +452,66 @@ pub fn wait_for(_app: &mut LightcraftApp, _ctx: &egui::Context, _timeout: std::t unsupported_wasm() } -/// The progress window for inspect/import, with cancellation. -pub fn progress(app: &mut LightcraftApp, ctx: &egui::Context) { - let Some(task) = app.lightroom.as_ref() else { return }; - let total = task.total.load(Ordering::Relaxed); - let done = task.done.load(Ordering::Relaxed); - let title = match task.kind { - Kind::Inspect => crate::i18n::tr("Inspecting Lightroom Catalog"), - Kind::Import => crate::i18n::tr("Importing Lightroom Catalog"), - }; - let text = match task.phase { - Phase::Reading if total == 0 => crate::i18n::tr("Reading Lightroom catalog…").into(), - Phase::Reading => crate::i18n::tr_format!("Reading Lightroom catalog… {done} of {total}", done = done, total = total), - Phase::Committing => crate::i18n::tr("Adding Lightroom photos…").into(), - Phase::Finalizing => crate::i18n::tr("Saving Lightroom import index…").into(), - }; - let mut cancel = false; - let t = Tokens::get(ctx); - egui::Window::new(title).title_bar(false).resizable(false).anchor(Align2::CENTER_BOTTOM, [0.0, -80.0]).fixed_size([360.0, 92.0]).show( - ctx, - |ui| { - ui.label(egui::RichText::new(text).color(t.text)); - ui.add(egui::ProgressBar::new(done as f32 / total.max(1) as f32).desired_width(340.0)); - let r = - ui.add_enabled(task.phase == Phase::Reading && !task.cancel.load(Ordering::Relaxed), egui::Button::new(crate::i18n::tr("Cancel"))); - register(ui.ctx(), "button:lightroomCancel", r.rect); - cancel = r.clicked(); - }, - ); - if cancel { - if let Some(task) = app.lightroom.as_mut() { - task.cancel(); - } - ctx.request_repaint(); - } - if app.lightroom.is_some() { - ctx.request_repaint_after(std::time::Duration::from_millis(100)); - } -} - #[cfg(test)] mod tests { - use super::inspect_report; + use super::*; use lightcraft_engine::lightroom_catalog::CatalogImport; + /// A task as `start_import` makes it, fed by a channel the test holds instead of a worker. + fn fake_import(h: &crate::headless::Headless) -> (LightroomTask, mpsc::Sender, Arc, Arc, Arc) { + let (tx, rx) = mpsc::channel(); + let (cancel, total, done) = (Arc::new(AtomicBool::new(false)), Arc::new(AtomicUsize::new(0)), Arc::new(AtomicUsize::new(0))); + let task = LightroomTask::new(&h.app.session, Kind::Import, "fixture.lrcat".into(), cancel.clone(), total.clone(), done.clone(), rx); + (task, tx, cancel, total, done) + } + + fn demo() -> crate::headless::Headless { + let app = crate::LightcraftApp::new(lightcraft_engine::Session::with_demo(), crate::Services { png: None, ..Default::default() }); + let mut h = crate::headless::Headless::new(app, [1200.0, 800.0], 1.0); + h.step(); + h + } + + #[test] + fn lightroom_row_is_not_cancellable_while_committing() { + let mut h = demo(); + let (task, tx, cancel, total, done) = fake_import(&h); + h.app.lightroom = Some(task); + total.store(10, Ordering::Relaxed); + done.store(3, Ordering::Relaxed); + h.step(); + let rows = h.app.session.activity.list(); + assert_eq!(rows.len(), 1); + assert_eq!((rows[0].kind, rows[0].label.as_str()), ("lightroom", "Importing Lightroom catalog")); + assert_eq!((rows[0].done, rows[0].total), (3, 10)); + assert!(rows[0].cancellable, "reading the catalog can stop"); + h.app.lightroom.as_mut().unwrap().enter(Phase::Committing); + let row = h.app.session.activity.list().remove(0); + assert!(!row.cancellable, "once photos are being added it runs to the end"); + assert!(h.app.session.activity.cancel(row.id).is_err()); + assert!(!cancel.load(Ordering::Relaxed)); + tx.send(Message::Finalized(Ok(()))).unwrap(); + h.step(); + assert!(h.app.lightroom.is_none()); + assert!(h.app.session.activity.list().is_empty(), "the row goes with the task"); + } + + #[test] + fn lightroom_cancel_from_the_stack_stops_the_read() { + let mut h = demo(); + let (task, tx, cancel, _, _) = fake_import(&h); + h.app.lightroom = Some(task); + h.step(); + let id = h.app.session.activity.list()[0].id; + h.app.session.activity.cancel(id).unwrap(); + assert!(cancel.load(Ordering::Relaxed), "the worker sees the stack's ✕"); + tx.send(Message::Prepared(Err("cancelled".into()))).unwrap(); + h.step(); + assert!(h.app.lightroom.is_none()); + assert_eq!(h.app.lightroom_last, Some(serde_json::json!({"cancelled": true}))); + assert!(h.app.session.activity.list().is_empty()); + } + #[test] fn inspect_report_keeps_empty_catalog_shape() { let report = inspect_report(CatalogImport {