mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
Issue #345: Lightroom catalog import in the activity stack
Inspecting and importing a Lightroom catalog show a row instead of the progress window; reading the catalog stops from its cross, adding the photos and saving the index run to the end. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9c8a721389
commit
c3bb8e9916
11 files changed
+140
-58
No files matched your search
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 カタログを読み取り中"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 目录"
|
||||
}
|
||||
@@ -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 目錄"
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Message>,
|
||||
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<AtomicBool>,
|
||||
total: Arc<AtomicUsize>,
|
||||
done: Arc<AtomicUsize>,
|
||||
rx: mpsc::Receiver<Message>,
|
||||
) -> 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<Message>, Arc<AtomicBool>, Arc<AtomicUsize>, Arc<AtomicUsize>) {
|
||||
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 {
|
||||
|
||||
Reference in new issue
Block a user