commit
97f8b8c87c
11 files changed
+256
-68
No files matched your search
@@ -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))
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 降噪模型",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user