# Conflicts:
#	crates/ui-egui/locales/es.json
This commit is contained in:
bflatastic committed 2026-10-13 11:05:45 +07:00
commit 97f8b8c87c
11 files changed
+256 -68

No files matched your search

+10 -3
View File
@@ -652,9 +652,16 @@ pub(crate) fn decode_raw_preview(bytes: &[u8], opts: lightcraft_codecs::DecodeOp
/// Oriented size of the embedded preview of a raw file.
fn embedded_preview_size(bytes: &[u8]) -> Option<(u32, u32)> {
let jpeg = stand_in_image(bytes)?;
let d = lightcraft_codecs::decode(&jpeg, lightcraft_codecs::DecodeOptions::fit(64, 64)).ok()?;
let (mut w, mut h) = (d.source_width, d.source_height);
if preview_orientation(bytes, d.orientation).swaps_axes() {
// headers only, as for other images (issue #367): decoding a full-size preview just for its
// size took about a second per file in an import; a decode remains the fallback
let (mut w, mut h, orientation) = match lightcraft_codecs::read_header(&jpeg) {
Ok(header) => (header.width, header.height, header.orientation),
Err(_) => {
let d = lightcraft_codecs::decode(&jpeg, lightcraft_codecs::DecodeOptions::fit(64, 64)).ok()?;
(d.source_width, d.source_height, d.orientation)
}
};
if preview_orientation(bytes, orientation).swaps_axes() {
std::mem::swap(&mut w, &mut h);
}
Some((w, h))
+72 -9
View File
@@ -466,10 +466,18 @@ pub fn launch_path(arg: &str, cwd: Option<&Path>, home: Option<&Path>, dcim: imp
/// Probe files (headers + content hash) as an import would.
pub(crate) fn probe_paths(s: &Session, paths: &[String]) -> Vec<Result<ProbeInfo, String>> {
probe_all(s.media.file_probe.as_ref(), paths, &ScanProgress::default())
let progress = ScanProgress::default();
probe_all(s.media.file_probe.as_ref(), paths, &progress.done, &progress.cancel)
}
fn probe_all(probe: Option<&crate::media::FileProbe>, paths: &[String], progress: &ScanProgress) -> Vec<Result<ProbeInfo, String>> {
/// Probe `paths` (several at once), counting each in `done`. Stops before the next file once
/// `cancel` is set: the files not reached come back as errors.
fn probe_all(
probe: Option<&crate::media::FileProbe>,
paths: &[String],
done: &std::sync::atomic::AtomicUsize,
cancel: &std::sync::atomic::AtomicBool,
) -> Vec<Result<ProbeInfo, String>> {
use std::sync::atomic::Ordering::Relaxed;
let Some(probe) = probe.cloned() else {
return paths
@@ -497,11 +505,11 @@ fn probe_all(probe: Option<&crate::media::FileProbe>, paths: &[String], progress
if i >= paths.len() {
break;
}
if progress.cancel.load(Relaxed) {
if cancel.load(Relaxed) {
break;
}
let r = probe(&paths[i]);
progress.done.fetch_add(1, Relaxed);
done.fetch_add(1, Relaxed);
out.lock().unwrap_or_else(|e| e.into_inner())[i] = Some(r);
}
});
@@ -513,11 +521,11 @@ fn probe_all(probe: Option<&crate::media::FileProbe>, paths: &[String], progress
paths
.iter()
.map(|p| {
if progress.cancel.load(Relaxed) {
if cancel.load(Relaxed) {
return Err("cancelled".into());
}
let r = probe(p);
progress.done.fetch_add(1, Relaxed);
done.fetch_add(1, Relaxed);
r
})
.collect()
@@ -620,7 +628,7 @@ pub fn scan_with(mut input: ScanInput, paths: &[String], progress: &ScanProgress
.collect();
let missing: Vec<String> = todo.iter().zip(&cached).filter(|(_, c)| c.is_none()).map(|(f, _)| f.clone()).collect();
progress.done.store(todo.len() - missing.len(), Relaxed);
let mut fresh = probe_all(input.probe.as_ref(), &missing, progress).into_iter();
let mut fresh = probe_all(input.probe.as_ref(), &missing, &progress.done, &progress.cancel).into_iter();
let probed: Vec<Result<ProbeInfo, String>> =
cached.into_iter().map(|c| c.map(Ok).unwrap_or_else(|| fresh.next().unwrap_or_else(|| Err("not probed".into())))).collect();
crate::memory::release();
@@ -705,6 +713,10 @@ pub struct ImportJob {
/// Files found so far (folders expanded) and handled so far, for progress.
pub total: std::sync::Arc<std::sync::atomic::AtomicUsize>,
pub done: std::sync::Arc<std::sync::atomic::AtomicUsize>,
/// Of those, the files that have to be read first (not probed by a preceding scan) and the
/// ones read so far: the step before they are handled.
pub read_total: std::sync::Arc<std::sync::atomic::AtomicUsize>,
pub read_done: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
/// The outcome of [`ImportJob::prepare`] for one batch, in file order.
@@ -857,6 +869,8 @@ impl ImportJob {
now: (s.clock)(),
total: Default::default(),
done: Default::default(),
read_total: Default::default(),
read_done: Default::default(),
})
}
@@ -917,8 +931,10 @@ impl ImportJob {
})
.collect();
let missing: Vec<String> = todo.iter().zip(&cached).filter(|(_, c)| c.is_none()).map(|(f, _)| f.clone()).collect();
let progress = ScanProgress::default();
let mut fresh = probe_all(self.probe.as_ref(), &missing, &progress).into_iter();
// Reading the files is the slow part of adding them where they are: it is counted on its
// own, and `cancel` stops it too (a large library takes long enough to want both).
self.read_total.fetch_add(missing.len(), Relaxed);
let mut fresh = probe_all(self.probe.as_ref(), &missing, &self.read_done, cancel).into_iter();
let probed: Vec<Result<ProbeInfo, String>> =
cached.into_iter().map(|c| c.map(Ok).unwrap_or_else(|| fresh.next().unwrap_or_else(|| Err("not probed".into())))).collect();
crate::memory::release();
@@ -1498,4 +1514,51 @@ mod prepared_tests {
assert!(matches!(prepared.items.first(), Some(PreparedItem::Duplicate { reason: "path", .. })));
assert!(matches!(prepared.items.get(1), Some(PreparedItem::Ready(_))));
}
fn unread_paths(n: usize) -> Vec<String> {
(0..n).map(|i| std::env::temp_dir().join(format!("lightcraft-probe-progress-{i}.jpg")).to_string_lossy().into()).collect()
}
/// Reading the files of a large import (a Lightroom catalog's originals) reported nothing:
/// the progress stood still at "14 of 24554" for as long as it took.
#[test]
fn prepare_counts_the_files_it_reads() {
use std::sync::atomic::Ordering::Relaxed;
let mut s = Session::new();
let seen = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let (seen_in_probe, done_in_probe) = (seen.clone(), std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)));
let mut job = ImportJob::new(&mut s, ImportOptions { mode: ImportMode::Add, ..ImportOptions::default() }).unwrap();
let (done, last) = (job.read_done.clone(), done_in_probe.clone());
job.probe = Some(std::sync::Arc::new(move |_: &str| {
seen_in_probe.fetch_add(1, Relaxed);
last.fetch_max(done.load(Relaxed), Relaxed);
Ok(ProbeInfo { format: "JPEG".into(), ..ProbeInfo::default() })
}));
let prepared = job.prepare(&unread_paths(40), &std::sync::atomic::AtomicBool::new(false));
assert_eq!(prepared.items.len(), 40);
assert_eq!(seen.load(Relaxed), 40);
assert!(done_in_probe.load(Relaxed) > 0, "progress moves while files are read");
assert_eq!((job.read_done.load(Relaxed), job.read_total.load(Relaxed)), (40, 40), "every file read is counted");
assert_eq!((job.done.load(Relaxed), job.total.load(Relaxed)), (40, 40), "handling them is counted on its own");
}
/// Cancel had no effect until every file had been read.
#[test]
fn cancel_stops_reading_files() {
use std::sync::atomic::Ordering::Relaxed;
let mut s = Session::new();
let cancel = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let seen = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let mut job = ImportJob::new(&mut s, ImportOptions { mode: ImportMode::Add, ..ImportOptions::default() }).unwrap();
let (seen_in_probe, cancel_in_probe) = (seen.clone(), cancel.clone());
job.probe = Some(std::sync::Arc::new(move |_: &str| {
seen_in_probe.fetch_add(1, Relaxed);
cancel_in_probe.store(true, Relaxed);
Ok(ProbeInfo { format: "JPEG".into(), ..ProbeInfo::default() })
}));
let prepared = job.prepare(&unread_paths(400), &cancel);
// at most the files the (up to 8) readers had already started
assert!(seen.load(Relaxed) <= 8, "read {} files after cancel", seen.load(Relaxed));
assert!(prepared.items.is_empty());
}
}
+118 -23
View File
@@ -129,6 +129,65 @@ pub struct LightroomCompletion {
/// Alias used by hosts that call the result a commit.
pub type LightroomCommit = LightroomCompletion;
/// The step of preparing a Lightroom import that is under way ([`LightroomProgress::step`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LightroomStep {
/// Reading the catalog's tables.
Catalog,
/// Reading the photos the catalog names (headers, content hash): nearly all of the time.
ReadPhotos,
/// Readying the photos that were read for the catalog.
AddPhotos,
}
/// Progress of [`LightroomJob::prepare`] step by step, shared with the thread that runs it.
#[derive(Clone, Default)]
pub struct LightroomProgress {
/// Catalog tables to read and read so far ([`lightroom_catalog::read_with_progress`]).
pub catalog_total: Arc<AtomicUsize>,
pub catalog_done: Arc<AtomicUsize>,
read_total: Arc<AtomicUsize>,
read_done: Arc<AtomicUsize>,
add_total: Arc<AtomicUsize>,
add_done: Arc<AtomicUsize>,
/// The catalog is read: the photos are next.
photos: Arc<AtomicBool>,
}
impl LightroomProgress {
/// Reading the photos is nearly all of an import's time; the catalog and readying the photos
/// take the rest of [`LightroomProgress::fraction`].
const CATALOG_SHARE: f32 = 0.05;
const READ_SHARE: f32 = 0.85;
/// The step under way, with how much of it is done and its total (0 while not yet known).
pub fn step(&self) -> (LightroomStep, usize, usize) {
let load = |n: &AtomicUsize| n.load(Ordering::Relaxed);
if !self.photos.load(Ordering::Relaxed) {
return (LightroomStep::Catalog, load(&self.catalog_done), load(&self.catalog_total));
}
let (read_done, read_total) = (load(&self.read_done), load(&self.read_total));
let (add_done, add_total) = (load(&self.add_done), load(&self.add_total));
// nothing to read (every photo is already in the library) goes straight to adding
if read_done < read_total || (read_total == 0 && add_done == 0) {
(LightroomStep::ReadPhotos, read_done, read_total)
} else {
(LightroomStep::AddPhotos, add_done, add_total)
}
}
/// The whole preparation, 0..=1.
pub fn fraction(&self) -> f32 {
let (step, done, total) = self.step();
let part = if total == 0 { 0.0 } else { (done as f32 / total as f32).clamp(0.0, 1.0) };
match step {
LightroomStep::Catalog => Self::CATALOG_SHARE * part,
LightroomStep::ReadPhotos => Self::CATALOG_SHARE + Self::READ_SHARE * part,
LightroomStep::AddPhotos => Self::CATALOG_SHARE + Self::READ_SHARE + (1.0 - Self::CATALOG_SHARE - Self::READ_SHARE) * part,
}
}
}
/// A Lightroom import detached from the owner thread.
pub struct LightroomJob {
path: PathBuf,
@@ -136,6 +195,7 @@ pub struct LightroomJob {
import: ImportJob,
token: LightroomLibraryToken,
archive_dir: Option<PathBuf>,
progress: LightroomProgress,
}
impl LightroomJob {
@@ -145,33 +205,25 @@ impl LightroomJob {
let opts = ImportOptions { mode: ImportMode::Add, on_deleted: crate::import::OnDeleted::Skip, ..ImportOptions::default() };
let import = ImportJob::new(s, opts)?;
let archive_dir = s.library.as_ref().filter(|library| library.on_disk).map(|library| library.dir.join("Interop"));
Ok(Self { path, update_existing, import, token: LightroomLibraryToken::capture(s), archive_dir })
let progress = LightroomProgress {
read_total: Arc::clone(&import.read_total),
read_done: Arc::clone(&import.read_done),
add_total: Arc::clone(&import.total),
add_done: Arc::clone(&import.done),
..LightroomProgress::default()
};
Ok(Self { path, update_existing, import, token: LightroomLibraryToken::capture(s), archive_dir, progress })
}
pub fn total(&self) -> usize {
self.import.total.load(Ordering::Relaxed)
}
pub fn done(&self) -> usize {
self.import.done.load(Ordering::Relaxed)
}
pub fn progress(&self) -> (usize, usize) {
(self.done(), self.total())
/// The job's progress, to watch from another thread while [`LightroomJob::prepare`] runs.
pub fn progress(&self) -> LightroomProgress {
self.progress.clone()
}
pub fn token(&self) -> &LightroomLibraryToken {
&self.token
}
pub fn total_atomic(&self) -> std::sync::Arc<AtomicUsize> {
std::sync::Arc::clone(&self.import.total)
}
pub fn done_atomic(&self) -> std::sync::Arc<AtomicUsize> {
std::sync::Arc::clone(&self.import.done)
}
/// Finish an owner commit's deferred archive-index write on a worker.
pub fn finish_archive(finalization: LightroomArchiveFinalization) -> crate::Result<()> {
finalization.finish()
@@ -182,14 +234,12 @@ impl LightroomJob {
if cancel.load(Ordering::Relaxed) {
return Err(error("Lightroom import cancelled"));
}
self.import.total.store(1, Ordering::Relaxed);
self.import.done.store(0, Ordering::Relaxed);
let mut data = lightroom_catalog::read_with_progress(&self.path, cancel, &self.import.total, &self.import.done).map_err(error)?;
let mut data =
lightroom_catalog::read_with_progress(&self.path, cancel, &self.progress.catalog_total, &self.progress.catalog_done).map_err(error)?;
lightroom_catalog::validate_for_job(&data).map_err(error)?;
if cancel.load(Ordering::Relaxed) {
return Err(error("Lightroom import cancelled"));
}
self.import.done.fetch_add(1, Ordering::Relaxed);
let paths: Vec<String> = data
.photos
.iter()
@@ -203,6 +253,9 @@ impl LightroomJob {
.filter(|photo| !Path::new(&photo.path).is_file())
.map(|photo| normalize(&photo.path))
.collect();
// the catalog step's last unit (validation, and looking for each original), then the photos
self.progress.catalog_done.fetch_add(1, Ordering::Relaxed);
self.progress.photos.store(true, Ordering::Relaxed);
let normal = self.import.prepare(&paths, cancel);
if cancel.load(Ordering::Relaxed) {
normal.rollback();
@@ -335,6 +388,48 @@ mod tests {
}
}
/// One count for the whole import read "14 of 24554" while the photos were read, then twice
/// the number of photos: each step has its own count, and the whole only moves forward.
#[test]
fn progress_names_each_step_with_its_own_count() {
let p = LightroomProgress::default();
let set = |n: &AtomicUsize, v: usize| n.store(v, Ordering::Relaxed);
assert_eq!(p.step(), (LightroomStep::Catalog, 0, 0));
assert_eq!(p.fraction(), 0.0);
set(&p.catalog_total, 14);
set(&p.catalog_done, 7);
assert_eq!(p.step(), (LightroomStep::Catalog, 7, 14));
let mut last = p.fraction();
assert!(last > 0.0);
let mut forward = |p: &LightroomProgress| {
let f = p.fraction();
assert!(f >= last && f <= 1.0, "{f} after {last}");
last = f;
};
set(&p.catalog_done, 14);
p.photos.store(true, Ordering::Relaxed);
assert_eq!(p.step(), (LightroomStep::ReadPhotos, 0, 0), "the photos are not counted yet");
forward(&p);
set(&p.add_total, 100);
set(&p.read_total, 100);
set(&p.read_done, 40);
assert_eq!(p.step(), (LightroomStep::ReadPhotos, 40, 100));
forward(&p);
set(&p.read_done, 100);
set(&p.add_done, 25);
assert_eq!(p.step(), (LightroomStep::AddPhotos, 25, 100));
forward(&p);
set(&p.add_done, 100);
forward(&p);
assert!(p.fraction() > 0.999);
// every photo already in the library: nothing to read
let known = LightroomProgress::default();
known.photos.store(true, Ordering::Relaxed);
set(&known.add_total, 5);
set(&known.add_done, 5);
assert_eq!(known.step(), (LightroomStep::AddPhotos, 5, 5));
}
#[test]
fn cancellation_before_commit_leaves_owner_catalog_unchanged() {
let mut session = crate::Session::new();
+2
View File
@@ -1815,6 +1815,8 @@
"Install the SAM 3 Model": "SAM-3-Modell installieren",
"This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "Diese Version kann das Modell noch nicht herunterladen. Zum Installieren von Hand lege model.safetensors, vocab.json und merges.txt aus Metas facebook/sam3 in den obigen Ordner: Objekt und Beschreiben funktionieren, sobald sie dort liegen.",
"Open Log Folder": "Protokollordner öffnen",
"Reading photos…": "Fotos werden gelesen…",
"Adding photos…": "Fotos werden hinzugefügt…",
"Back to this person": "Back to this person",
"AI Denoise": "AI Denoise",
"AI Denoise Model": "AI Denoise Model",
+2
View File
@@ -1749,6 +1749,8 @@
"cached picture": "imagen en caché",
"cached pictures": "imágenes en caché",
"commercial use allowed": "uso comercial permitido",
"Reading photos…": "Leyendo fotos…",
"Adding photos…": "Añadiendo fotos…",
"denoiseModel.accept": "denoiseModel.accept",
"in use": "en uso",
"installed": "instalado",
+2
View File
@@ -1659,6 +1659,8 @@
"Install the SAM 3 Model": "SAM 3モデルのインストール",
"This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "このビルドではまだモデルをダウンロードできません。手動でインストールするには、Meta の facebook/sam3 の model.safetensors、vocab.json、merges.txt を上のフォルダに置いてください。置いた時点で「オブジェクト」と「説明で選択」が使えるようになります。",
"Open Log Folder": "ログフォルダーを開く",
"Reading photos…": "写真を確認中…",
"Adding photos…": "写真を追加中…",
"Back to this person": "Back to this person",
"AI Denoise": "AI Denoise",
"AI Denoise Model": "AI Denoise Model",
+2
View File
@@ -950,6 +950,8 @@
"Open Library": "Abrir biblioteca",
"Open Library…": "Abrir biblioteca…",
"Open Log Folder": "Abrir pasta de logs",
"Reading photos…": "Lendo fotos…",
"Adding photos…": "Adicionando fotos…",
"Open in": "Abrir em",
"Open in Detail": "Abrir em Detalhe",
"Open the place in OpenStreetMap": "Abrir o local no OpenStreetMap",
+2
View File
@@ -785,6 +785,8 @@
"Open Library": "Открыть библиотеку",
"Open Library…": "Открыть библиотеку…",
"Open Log Folder": "Открыть папку журналов",
"Reading photos…": "Чтение фото…",
"Adding photos…": "Добавление фото…",
"Open in": "Открыть в",
"Open in Detail": "Открыть в режиме «Детализация»",
"Open the place in OpenStreetMap": "Открыть место в OpenStreetMap",
+2
View File
@@ -1659,6 +1659,8 @@
"Install the SAM 3 Model": "安装 SAM 3 模型",
"This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "此版本尚无法下载模型。如需手动安装,请将 Meta 的 facebook/sam3 中的 model.safetensors、vocab.json 和 merges.txt 放入上方的文件夹:放入后即可使用“对象”和“描述”。",
"Open Log Folder": "打开日志文件夹",
"Reading photos…": "正在读取照片…",
"Adding photos…": "正在添加照片…",
"Back to this person": "返回此人",
"AI Denoise": "AI 降噪",
"AI Denoise Model": "AI 降噪模型",
+2
View File
@@ -1659,6 +1659,8 @@
"Install the SAM 3 Model": "安裝 SAM 3 模型",
"This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "此版本尚無法下載模型。如需手動安裝,請將 Meta 的 facebook/sam3 中的 model.safetensors、vocab.json 和 merges.txt 放入上方的資料夾:放入後即可使用「物件」與「描述」。",
"Open Log Folder": "開啟記錄資料夾",
"Reading photos…": "正在讀取照片…",
"Adding photos…": "正在新增照片…",
"Back to this person": "Back to this person",
"AI Denoise": "AI Denoise",
"AI Denoise Model": "AI Denoise Model",
+42 -33
View File
@@ -5,10 +5,11 @@
//! data only on the owner thread, then finishes the archive index on a worker.
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, mpsc};
use lightcraft_engine::activity::{Cancel, TaskGuard};
use lightcraft_engine::lightroom_job::{LightroomProgress, LightroomStep};
use serde_json::{Value, json};
use crate::LightcraftApp;
@@ -37,8 +38,7 @@ pub struct LightroomTask {
kind: Kind,
path: PathBuf,
cancel: Arc<AtomicBool>,
total: Arc<AtomicUsize>,
done: Arc<AtomicUsize>,
progress: LightroomProgress,
rx: mpsc::Receiver<Message>,
phase: Phase,
cancelled: bool,
@@ -52,8 +52,7 @@ impl LightroomTask {
kind: Kind,
path: PathBuf,
cancel: Arc<AtomicBool>,
total: Arc<AtomicUsize>,
done: Arc<AtomicUsize>,
progress: LightroomProgress,
rx: mpsc::Receiver<Message>,
) -> Self {
let label = match kind {
@@ -61,7 +60,7 @@ impl LightroomTask {
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 };
let task = Self { kind, path, cancel, progress, rx, phase: Phase::Reading, cancelled: false, guard };
task.refresh();
task
}
@@ -73,16 +72,21 @@ impl LightroomTask {
self.refresh();
}
/// Bring the activity row up to date: the count while reading, then what is being done.
/// Bring the activity row up to date: while reading, the step under way and its own count
/// (reading the photos takes nearly all of an import, so it is counted, not waited out);
/// 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…"),
let (step, done, total) = self.progress.step();
let detail = match (self.phase, self.kind, step) {
(Phase::Reading, Kind::Inspect, _) => "",
(Phase::Reading, Kind::Import, LightroomStep::Catalog) => crate::i18n::tr("Reading Lightroom catalog…"),
(Phase::Reading, Kind::Import, LightroomStep::ReadPhotos) => crate::i18n::tr("Reading photos…"),
(Phase::Reading, Kind::Import, LightroomStep::AddPhotos) => crate::i18n::tr("Adding photos…"),
(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);
self.guard.progress(done as u64, total as u64);
} else {
self.guard.progress(0, 0);
}
@@ -100,13 +104,21 @@ impl LightroomTask {
Phase::Committing => "committing",
Phase::Finalizing => "finalizing",
};
let (step, done, total) = self.progress.step();
let step = match step {
LightroomStep::Catalog => "catalog",
LightroomStep::ReadPhotos => "readPhotos",
LightroomStep::AddPhotos => "addPhotos",
};
json!({
"running": true,
"kind": kind,
"path": self.path,
"phase": phase,
"done": self.done.load(Ordering::Relaxed),
"total": self.total.load(Ordering::Relaxed),
"step": step,
"done": done,
"total": total,
"fraction": self.progress.fraction(),
"cancelled": self.cancelled || self.cancel.load(Ordering::Relaxed),
})
}
@@ -148,8 +160,7 @@ fn busy(app: &LightcraftApp) -> Result<(), String> {
fn spawn_inspect(
path: PathBuf,
cancel: Arc<AtomicBool>,
total: Arc<AtomicUsize>,
done: Arc<AtomicUsize>,
progress: LightroomProgress,
tx: mpsc::Sender<Message>,
ctx: egui::Context,
) -> Result<(), String> {
@@ -157,7 +168,7 @@ fn spawn_inspect(
.name("lc-lightroom-inspect".into())
.spawn(move || {
let result = lightcraft_engine::guard::catch("Lightroom inspect", || {
lightcraft_engine::lightroom_catalog::read_with_progress(&path, &cancel, &total, &done)
lightcraft_engine::lightroom_catalog::read_with_progress(&path, &cancel, &progress.catalog_total, &progress.catalog_done)
})
.and_then(|r| r.map(inspect_report));
let _ = tx.send(Message::Inspected(result));
@@ -278,12 +289,11 @@ fn terminal(app: &mut LightcraftApp, ctx: &egui::Context, task: &LightroomTask,
fn start_inspect(app: &mut LightcraftApp, path: PathBuf, ctx: &egui::Context) -> Result<Value, String> {
busy(app)?;
let cancel = Arc::new(AtomicBool::new(false));
let total = Arc::new(AtomicUsize::new(0));
let done = Arc::new(AtomicUsize::new(0));
let progress = LightroomProgress::default();
let (tx, rx) = mpsc::channel();
spawn_inspect(path.clone(), cancel.clone(), total.clone(), done.clone(), tx, ctx.clone())?;
spawn_inspect(path.clone(), cancel.clone(), progress.clone(), tx, ctx.clone())?;
app.lightroom_last = None;
app.lightroom = Some(LightroomTask::new(&app.session, Kind::Inspect, path.clone(), cancel, total, done, rx));
app.lightroom = Some(LightroomTask::new(&app.session, Kind::Inspect, path.clone(), cancel, progress, rx));
Ok(json!({"running": true, "kind": "inspect", "path": path}))
}
@@ -292,12 +302,11 @@ fn start_import(app: &mut LightcraftApp, path: PathBuf, update_existing: bool, c
busy(app)?;
let job = lightcraft_engine::lightroom_job::LightroomJob::new(&mut app.session, path.clone(), update_existing).map_err(|e| e.to_string())?;
let cancel = Arc::new(AtomicBool::new(false));
let total = job.total_atomic();
let done = job.done_atomic();
let progress = job.progress();
let (tx, rx) = mpsc::channel();
spawn_prepare(job, cancel.clone(), tx, ctx.clone())?;
app.lightroom_last = None;
app.lightroom = Some(LightroomTask::new(&app.session, Kind::Import, path.clone(), cancel, total, done, rx));
app.lightroom = Some(LightroomTask::new(&app.session, Kind::Import, path.clone(), cancel, progress, rx));
Ok(json!({"running": true, "kind": "import", "path": path}))
}
@@ -463,11 +472,11 @@ mod tests {
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>) {
fn fake_import(h: &crate::headless::Headless) -> (LightroomTask, mpsc::Sender<Message>, Arc<AtomicBool>, LightroomProgress) {
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)
let (cancel, progress) = (Arc::new(AtomicBool::new(false)), LightroomProgress::default());
let task = LightroomTask::new(&h.app.session, Kind::Import, "fixture.lrcat".into(), cancel.clone(), progress.clone(), rx);
(task, tx, cancel, progress)
}
fn demo() -> crate::headless::Headless {
@@ -481,10 +490,10 @@ mod tests {
#[test]
fn lightroom_row_is_not_cancellable_while_committing() {
let mut h = demo();
let (task, tx, cancel, total, done) = fake_import(&h);
let (task, tx, cancel, progress) = fake_import(&h);
h.app.lightroom = Some(task);
total.store(10, Ordering::Relaxed);
done.store(3, Ordering::Relaxed);
progress.catalog_total.store(10, Ordering::Relaxed);
progress.catalog_done.store(3, Ordering::Relaxed);
h.step();
let rows = h.app.session.activity.list();
assert_eq!(rows.len(), 1);
@@ -505,7 +514,7 @@ mod tests {
#[test]
fn lightroom_cancel_from_the_stack_stops_the_read() {
let mut h = demo();
let (task, tx, cancel, _, _) = fake_import(&h);
let (task, tx, cancel, _) = fake_import(&h);
h.app.lightroom = Some(task);
h.step();
let id = h.app.session.activity.list()[0].id;