mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 10:09:01 -06:00
Merge PR #2049: LRC-LIB-LIBFOLDERS: Synchronize Folder says it is listing the folder instead of sitting at "0 of N"
This commit is contained in:
@@ -691,6 +691,11 @@ pub(crate) fn scan_files(mut input: ScanInput, files: Vec<String>, progress: &Sc
|
||||
.iter()
|
||||
.map(|f| {
|
||||
let info = input.cache.remove(f)?;
|
||||
// (✕ set while the files' cached probes are checked: no more round trips to the disk; what is
|
||||
// not reached counts as not probed, like the files after it)
|
||||
if progress.cancel.load(Relaxed) {
|
||||
return None;
|
||||
}
|
||||
let size = std::fs::metadata(f).map(|m| m.len()).ok();
|
||||
(size.is_none() || size == Some(info.file_size)).then_some(info)
|
||||
})
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
//! - a sidecar is found as Read Metadata from File finds it, including the other naming
|
||||
//! scheme's file (`IMG_1.xmp` for `IMG_1.JPG` beside `IMG_1.CR3`).
|
||||
//!
|
||||
//! The walk for new files stops at the import's limits on depth and entries (logged), and is
|
||||
//! not cancelled midway.
|
||||
//! The walk for new files stops at the import's limits on depth and entries (logged), or when the
|
||||
//! scan is cancelled.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -52,13 +52,19 @@ use crate::{EngineError, Result, Session};
|
||||
/// The largest XMP sidecar read when looking for metadata updates; a bigger one is skipped.
|
||||
const MAX_SIDECAR_BYTES: u64 = 16 * 1024 * 1024;
|
||||
|
||||
/// How far a scan is: the folder's files probed ([`ScanProgress`], whose `cancel` stops the
|
||||
/// whole scan) and the library's photos checked.
|
||||
/// How far a scan is: the folders and files listed so far (the walk, which comes first and on a
|
||||
/// network share can take minutes), the folder's files probed ([`ScanProgress`], whose `cancel`
|
||||
/// stops the whole scan, walk included) and the library's photos checked.
|
||||
#[derive(Default)]
|
||||
pub struct SyncProgress {
|
||||
pub files: ScanProgress,
|
||||
pub checked: std::sync::atomic::AtomicUsize,
|
||||
pub to_check: std::sync::atomic::AtomicUsize,
|
||||
/// Folders and files (photos or not) the walk has listed.
|
||||
pub folders_listed: std::sync::atomic::AtomicUsize,
|
||||
pub files_listed: std::sync::atomic::AtomicUsize,
|
||||
/// The walk is over (or there was nothing to walk).
|
||||
pub listed: std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl SyncProgress {
|
||||
@@ -68,6 +74,15 @@ impl SyncProgress {
|
||||
let done = self.files.done.load(Relaxed).saturating_add(self.checked.load(Relaxed));
|
||||
(done, self.files.total.load(Relaxed).saturating_add(self.to_check.load(Relaxed)))
|
||||
}
|
||||
|
||||
/// While a scan lists the folder (before any file can be checked): the folders and files
|
||||
/// listed so far. `None` once the listing is over.
|
||||
pub fn listing(&self) -> Option<(usize, usize)> {
|
||||
use std::sync::atomic::Ordering::{Acquire, Relaxed};
|
||||
// (Acquire pairs with the Release store of `listed`: once the listing is seen to be
|
||||
// over, so is what was stored before it, and the counts that follow are the final ones)
|
||||
(!self.listed.load(Acquire)).then(|| (self.folders_listed.load(Relaxed), self.files_listed.load(Relaxed)))
|
||||
}
|
||||
}
|
||||
|
||||
/// What a scan of a folder needs, taken from the session so it can run on a worker thread.
|
||||
@@ -196,6 +211,7 @@ pub fn scan_with(input: SyncInput, progress: &SyncProgress) -> FolderChanges {
|
||||
let SyncInput { folder, scan, ids, naming, labels } = input;
|
||||
let seen: Vec<Arc<Photo>> = ids.iter().filter_map(|id| labels.photo(*id)).map(Arc::clone).collect();
|
||||
if !cfg!(target_arch = "wasm32") && !Path::new(&folder).is_dir() {
|
||||
progress.listed.store(true, std::sync::atomic::Ordering::Release);
|
||||
return FolderChanges { path: folder, seen, offline: true, ..Default::default() };
|
||||
}
|
||||
// (how each photo's sidecar is named: a pass over the library, made here and not while the
|
||||
@@ -207,15 +223,30 @@ pub fn scan_with(input: SyncInput, progress: &SyncProgress) -> FolderChanges {
|
||||
// network share that is one request for a folder's names where it was several for each
|
||||
// photo. Then both halves wait on the disk at once: the new files are probed (on their own
|
||||
// threads) beside the checks of the photos, which ask the disk only what the listing can't say.
|
||||
// (how many photos there are to check is known before the walk: the count shows while it runs)
|
||||
// (how many photos there are to check is known before the walk; while the walk runs, the
|
||||
// folders and files it listed tell how far it is)
|
||||
progress.to_check.store(photos.len(), std::sync::atomic::Ordering::Relaxed);
|
||||
let mut listing = Listing::default();
|
||||
let (files, _) = crate::import::expand_noting(
|
||||
std::slice::from_ref(&folder),
|
||||
scan.skip(),
|
||||
crate::walk::Limits::default(),
|
||||
Some(&mut |dir, names, complete| listing.note(dir, names, complete)),
|
||||
Some(&mut |dir, names, complete| {
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
progress.folders_listed.fetch_add(1, Relaxed);
|
||||
let files = names.iter().filter(|(_, kind)| *kind == crate::walk::EntryKind::File).count();
|
||||
progress.files_listed.fetch_add(files, Relaxed);
|
||||
listing.note(dir, names, complete);
|
||||
// ✕ stops the walk too (a cancelled scan's result is not used)
|
||||
!progress.files.cancel.load(Relaxed)
|
||||
}),
|
||||
);
|
||||
progress.listed.store(true, std::sync::atomic::Ordering::Release);
|
||||
// ✕ during the listing: its result is not used, so nothing more is asked of the disk (the files' cached probes
|
||||
// would each be checked against the file, a round trip per file on a network share)
|
||||
if progress.files.cancel.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
return FolderChanges { path: folder, seen, ..Default::default() };
|
||||
}
|
||||
let listing = &listing;
|
||||
// (no threads on the web: one after the other there)
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
@@ -146,8 +146,10 @@ fn a_check_from_the_listing_is_the_check_from_the_disk() {
|
||||
.collect();
|
||||
// (`dir.jpg` is gone, though a sidecar of its name is there)
|
||||
let mut l = Listing::default();
|
||||
let walked =
|
||||
crate::walk::files_in_noting(&dir, None, crate::walk::Limits::default(), |_| true, &mut |d, names, complete| l.note(d, names, complete));
|
||||
let walked = crate::walk::files_in_noting(&dir, None, crate::walk::Limits::default(), |_| true, &mut |d, names, complete| {
|
||||
l.note(d, names, complete);
|
||||
true
|
||||
});
|
||||
assert_eq!(walked.files.len(), 12);
|
||||
let nothing = Listing::default();
|
||||
for naming in [SidecarNaming::Stem, SidecarNaming::Full] {
|
||||
@@ -167,7 +169,10 @@ fn a_check_from_the_listing_is_the_check_from_the_disk() {
|
||||
l.asked.load(std::sync::atomic::Ordering::Relaxed)
|
||||
};
|
||||
let mut with = Listing::default();
|
||||
crate::walk::files_in_noting(&dir, None, crate::walk::Limits::default(), |_| true, &mut |d, names, complete| with.note(d, names, complete));
|
||||
crate::walk::files_in_noting(&dir, None, crate::walk::Limits::default(), |_| true, &mut |d, names, complete| {
|
||||
with.note(d, names, complete);
|
||||
true
|
||||
});
|
||||
let (with, without) = (asked(&with), asked(&Listing::default()));
|
||||
// (without: each photo's file, and the names of its sidecar up to the one that is there: 20 questions where
|
||||
// names are found in any case, 21 where they are not)
|
||||
@@ -181,7 +186,10 @@ fn a_check_from_the_listing_is_the_check_from_the_disk() {
|
||||
// a folder of plain names only: the disk is asked nothing at all
|
||||
let sub = dir.join("sub");
|
||||
let mut only_plain = Listing::default();
|
||||
crate::walk::files_in_noting(&sub, None, crate::walk::Limits::default(), |_| true, &mut |d, names, complete| only_plain.note(d, names, complete));
|
||||
crate::walk::files_in_noting(&sub, None, crate::walk::Limits::default(), |_| true, &mut |d, names, complete| {
|
||||
only_plain.note(d, names, complete);
|
||||
true
|
||||
});
|
||||
let mut c2 = Catalog::new();
|
||||
for (name, expected) in [("deep.jpg", Check::News), ("gone.jpg", Check::Missing), ("gone.jpg.xmp", Check::Missing)] {
|
||||
let p = photo(&mut c2, &sub.join(name));
|
||||
|
||||
@@ -314,6 +314,41 @@ fn a_cancelled_scan_stops_checking_files() {
|
||||
assert!(c.missing.is_empty() && c.metadata.is_empty(), "{c:?}");
|
||||
}
|
||||
|
||||
/// While the folder is listed, before a single photo can be checked, the scan says how far the
|
||||
/// listing is: a folder tree on a network share takes minutes to list, and "0 of 64,085" all
|
||||
/// that while looks like a scan that has stopped.
|
||||
#[test]
|
||||
fn a_scan_says_how_many_folders_and_files_it_has_listed() {
|
||||
let dir = Scratch::new("listed");
|
||||
let mut s = library(&dir);
|
||||
write_png(&dir.path("trip/day2/c.png"), 3);
|
||||
std::fs::write(dir.path("trip/day2/notes.txt"), "not a photo").unwrap();
|
||||
let input = crate::sync::SyncInput::new(&mut s, &dir.path("trip"), false).unwrap();
|
||||
let progress = crate::sync::SyncProgress::default();
|
||||
assert_eq!(progress.listing(), Some((0, 0)), "listing is the first thing a scan does");
|
||||
crate::sync::scan_with(input, &progress);
|
||||
assert_eq!(progress.listing(), None, "the listing is done once the scan is");
|
||||
let (folders, files) =
|
||||
(progress.folders_listed.load(std::sync::atomic::Ordering::Relaxed), progress.files_listed.load(std::sync::atomic::Ordering::Relaxed));
|
||||
// trip, trip/day1, trip/day2; a.png, b.png, c.png and the note (every file is listed, photo or not)
|
||||
assert_eq!((folders, files), (3, 4));
|
||||
}
|
||||
|
||||
/// ✕ on a scan's row stops the listing too, not only the checks after it.
|
||||
#[test]
|
||||
fn a_cancelled_scan_stops_listing_the_folders() {
|
||||
let dir = Scratch::new("cancel-listing");
|
||||
let mut s = library(&dir);
|
||||
for i in 0..20u8 {
|
||||
write_png(&dir.path(&format!("trip/more/{i}/x.png")), i);
|
||||
}
|
||||
let input = crate::sync::SyncInput::new(&mut s, &dir.path("trip"), false).unwrap();
|
||||
let progress = crate::sync::SyncProgress::default();
|
||||
progress.files.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
crate::sync::scan_with(input, &progress);
|
||||
assert_eq!(progress.folders_listed.load(std::sync::atomic::Ordering::Relaxed), 1, "the walk stopped after the folder it was in");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_photos_are_checked_in_parallel_with_the_same_answer() {
|
||||
let dir = Scratch::new("many");
|
||||
@@ -685,3 +720,22 @@ fn sidecar_namings_for_many_photos_agree_with_one_at_a_time() {
|
||||
assert_eq!(s.sidecar_namings(&ids), one_by_one);
|
||||
assert!(one_by_one.contains(&crate::sidecar::SidecarNaming::Full), "the case that matters is there");
|
||||
}
|
||||
|
||||
/// ✕ set after the listing, while the scan goes through the files: the probes a preceding scan
|
||||
/// kept are not checked against the disk one by one (a round trip per file on a network share);
|
||||
/// the files not reached come back as errors.
|
||||
#[test]
|
||||
fn a_cancelled_scan_does_not_check_the_cached_probes_against_the_disk() {
|
||||
let dir = Scratch::new("cancel-cached");
|
||||
let mut s = library(&dir);
|
||||
write_png(&dir.path("trip/new.png"), 9);
|
||||
let found = crate::import::scan(&mut s, &[dir.path("trip")]);
|
||||
assert!(found.iter().any(|c| c.name == "new.png" && c.error.is_none()), "{found:?}");
|
||||
assert!(!s.import_probes.is_empty(), "the scan kept the new file's probe");
|
||||
let (input, paths) = crate::import::ScanInput::new(&mut s, &[dir.path("trip")]);
|
||||
let progress = crate::import::ScanProgress::default();
|
||||
progress.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let out = crate::import::scan_with(input, &paths, &progress);
|
||||
let new = out.candidates.iter().find(|c| c.name == "new.png").expect("new.png is a candidate");
|
||||
assert!(new.error.is_some(), "the cached probe was used after ✕: {new:?}");
|
||||
}
|
||||
|
||||
@@ -58,8 +58,9 @@ pub enum EntryKind {
|
||||
|
||||
/// What [`files_in_noting`] hands over for each folder it lists: the folder, the name and kind
|
||||
/// of every entry in it (hidden ones too), and whether that is all of them (not when the walk
|
||||
/// stopped inside the folder, or an entry could not be read).
|
||||
pub type NoteFolder<'a> = &'a mut dyn FnMut(&Path, Vec<(std::ffi::OsString, EntryKind)>, bool);
|
||||
/// stopped inside the folder, or an entry could not be read). It answers whether to go on: `false`
|
||||
/// ends the walk there (a scan cancelled; [`Walked::truncated`] is left alone: who stopped it knows).
|
||||
pub type NoteFolder<'a> = &'a mut dyn FnMut(&Path, Vec<(std::ffi::OsString, EntryKind)>, bool) -> bool;
|
||||
|
||||
/// The files below folder `root` that `keep` accepts. Hidden entries (names starting with `.`)
|
||||
/// are left out, the folder `skip` (e.g. the library's own) is never entered, and the walk stops
|
||||
@@ -154,8 +155,10 @@ fn walk(root: &Path, skip: Option<&Path>, limits: Limits, mut keep: impl FnMut(&
|
||||
};
|
||||
children.push((p, dir));
|
||||
}
|
||||
if let (Some(note), Some(listed)) = (note.as_mut(), listed) {
|
||||
note(&path, listed, complete);
|
||||
if let (Some(note), Some(listed)) = (note.as_mut(), listed)
|
||||
&& !note(&path, listed, complete)
|
||||
{
|
||||
break;
|
||||
}
|
||||
children.sort();
|
||||
let below = depth.saturating_add(1);
|
||||
@@ -193,6 +196,7 @@ mod tests {
|
||||
let mut entries: Vec<(String, EntryKind)> = entries.into_iter().map(|(n, k)| (n.to_string_lossy().to_string(), k)).collect();
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
noted.push((dir.to_path_buf(), entries, complete));
|
||||
true
|
||||
});
|
||||
assert_eq!(walked, files_in(&root, None, Limits::default(), |_| true), "the same walk, noted or not");
|
||||
let named = |n: &str, k: EntryKind| (n.to_string(), k);
|
||||
@@ -205,7 +209,8 @@ mod tests {
|
||||
// stopped inside the first folder: its listing is not all of it
|
||||
let mut cut = Vec::new();
|
||||
let walked = files_in_noting(&root, None, Limits { max_depth: 8, max_entries: 2 }, |_| true, &mut |dir, entries, complete| {
|
||||
cut.push((dir.to_path_buf(), entries.len(), complete))
|
||||
cut.push((dir.to_path_buf(), entries.len(), complete));
|
||||
true
|
||||
});
|
||||
assert!(walked.truncated);
|
||||
assert_eq!(cut.first().map(|(dir, n, complete)| (dir.clone(), *n, *complete)), Some((root.clone(), 2, false)));
|
||||
|
||||
@@ -296,5 +296,6 @@
|
||||
"Already in “{}”": "Již v „{}“",
|
||||
"Move {n} photo{} to “{}”": "Přesunout do „{1}“ fotografie: {n}{0:.0}",
|
||||
"Move {n} photo{} to “{}”?": "Přesunout do „{1}“ fotografie: {n}{0:.0}?",
|
||||
"Listing folders — folders: {folders}, files: {files}": "Procházení složek – složky: {folders}, soubory: {files}",
|
||||
"Moved {n} photo{} to “{}”. Edit → Undo moves them back.": "Do „{1}“ přesunuto fotografií: {n}{0:.0}. Úpravy → Zpět je vrátí."
|
||||
}
|
||||
|
||||
@@ -296,5 +296,6 @@
|
||||
"Already in “{}”": "Bereits in „{}“",
|
||||
"Move {n} photo{} to “{}”": "{n} Foto{0} nach „{1}“ verschieben",
|
||||
"Move {n} photo{} to “{}”?": "{n} Foto{0} nach „{1}“ verschieben?",
|
||||
"Listing folders — folders: {folders}, files: {files}": "Ordner werden aufgelistet – Ordner: {folders}, Dateien: {files}",
|
||||
"Moved {n} photo{} to “{}”. Edit → Undo moves them back.": "{n} Foto{0} nach „{1}“ verschoben. Bearbeiten → Rückgängig holt sie zurück."
|
||||
}
|
||||
|
||||
@@ -296,5 +296,6 @@
|
||||
"Already in “{}”": "Ya está en “{}”",
|
||||
"Move {n} photo{} to “{}”": "Mover {n} foto{0} a “{1}”",
|
||||
"Move {n} photo{} to “{}”?": "¿Mover {n} foto{0} a “{1}”?",
|
||||
"Listing folders — folders: {folders}, files: {files}": "Listando carpetas — carpetas: {folders}, archivos: {files}",
|
||||
"Moved {n} photo{} to “{}”. Edit → Undo moves them back.": "{n} foto{0} movida{0} a “{1}”. Editar → Deshacer las devuelve."
|
||||
}
|
||||
|
||||
@@ -296,5 +296,6 @@
|
||||
"Already in “{}”": "Déjà dans « {} »",
|
||||
"Move {n} photo{} to “{}”": "Déplacer {n} photo{} vers « {} »",
|
||||
"Move {n} photo{} to “{}”?": "Déplacer {n} photo{} vers « {} » ?",
|
||||
"Listing folders — folders: {folders}, files: {files}": "Liste des dossiers — dossiers : {folders}, fichiers : {files}",
|
||||
"Moved {n} photo{} to “{}”. Edit → Undo moves them back.": "{n} photo{} déplacée(s) vers « {} ». Modifier → Annuler les remet en place."
|
||||
}
|
||||
|
||||
@@ -296,5 +296,6 @@
|
||||
"Already in “{}”": "Già in “{}”",
|
||||
"Move {n} photo{} to “{}”": "Sposta foto in “{1}” ({n}){0:.0}",
|
||||
"Move {n} photo{} to “{}”?": "Spostare le foto in “{1}” ({n})?{0:.0}",
|
||||
"Moved {n} photo{} to “{}”. Edit → Undo moves them back.": "Foto spostate in “{1}”: {n}{0:.0}. Modifica → Annulla le riporta indietro."
|
||||
"Moved {n} photo{} to “{}”. Edit → Undo moves them back.": "Foto spostate in “{1}”: {n}{0:.0}. Modifica → Annulla le riporta indietro.",
|
||||
"Listing folders — folders: {folders}, files: {files}": "Elenco delle cartelle — cartelle: {folders}, file: {files}"
|
||||
}
|
||||
|
||||
@@ -296,5 +296,6 @@
|
||||
"Renames {n} face{} across the library. The files are not changed.": "ライブラリ全体で{n}件の顔の名前を変更します。ファイルは変更されません。{:.0}",
|
||||
"“{to}” is already a person. Combining them makes one person; Undo separates them again.": "「{to}」はすでに別の人物です。まとめると1人になり、元に戻すと再び分かれます。",
|
||||
"{what} merge added at {mp}: its full size, {full}, needs more memory than this computer has": "{what}合成を{mp}で追加しました:フルサイズ({full})にはこのコンピューターのメモリが足りません",
|
||||
"Listing folders — folders: {folders}, files: {files}": "フォルダーを一覧中 — フォルダー:{folders}、ファイル:{files}",
|
||||
"{what} merge added at {mp}, the largest a panorama can be (its full size is {full})": "{what}合成を{mp}で追加しました。パノラマの最大サイズです(フルサイズ:{full})"
|
||||
}
|
||||
|
||||
@@ -296,5 +296,6 @@
|
||||
"Already in “{}”": "Already in “{}”",
|
||||
"Move {n} photo{} to “{}”": "Move {n} photo{} to “{}”",
|
||||
"Move {n} photo{} to “{}”?": "Move {n} photo{} to “{}”?",
|
||||
"Listing folders — folders: {folders}, files: {files}": "საქაღალდეების ჩამოთვლა — საქაღალდეები: {folders}, ფაილები: {files}",
|
||||
"Moved {n} photo{} to “{}”. Edit → Undo moves them back.": "Moved {n} photo{} to “{}”. Edit → Undo moves them back."
|
||||
}
|
||||
|
||||
@@ -296,5 +296,6 @@
|
||||
"Already in “{}”": "Już w „{}”",
|
||||
"Move {n} photo{} to “{}”": "Przenieś do „{1}” — zdjęcia: {n}{0:.0}",
|
||||
"Move {n} photo{} to “{}”?": "Przenieść do „{1}” — zdjęcia: {n}{0:.0}?",
|
||||
"Listing folders — folders: {folders}, files: {files}": "Przeglądanie folderów — foldery: {folders}, pliki: {files}",
|
||||
"Moved {n} photo{} to “{}”. Edit → Undo moves them back.": "Przeniesiono do „{1}” — zdjęcia: {n}{0:.0}. Edytuj → Cofnij przywróci je."
|
||||
}
|
||||
|
||||
@@ -296,5 +296,6 @@
|
||||
"Renames {n} face{} across the library. The files are not changed.": "Renomeia {n} rosto{} na biblioteca. Os arquivos não são alterados.",
|
||||
"“{to}” is already a person. Combining them makes one person; Undo separates them again.": "“{to}” já é uma pessoa. Combiná-las deixa uma só; Desfazer as separa de novo.",
|
||||
"{what} merge added at {mp}: its full size, {full}, needs more memory than this computer has": "{what} mesclagem adicionada em {mp}: o tamanho completo, {full}, precisa de mais memória do que este computador tem",
|
||||
"Listing folders — folders: {folders}, files: {files}": "Listando pastas — pastas: {folders}, arquivos: {files}",
|
||||
"{what} merge added at {mp}, the largest a panorama can be (its full size is {full})": "{what} mesclagem adicionada em {mp}, o maior tamanho possível para um panorama (tamanho completo: {full})"
|
||||
}
|
||||
|
||||
@@ -296,5 +296,6 @@
|
||||
"Already in “{}”": "Уже в «{}»",
|
||||
"Move {n} photo{} to “{}”": "Переместить в «{1}» фото: {n}{0:.0}",
|
||||
"Move {n} photo{} to “{}”?": "Переместить в «{1}» фото: {n}{0:.0}?",
|
||||
"Listing folders — folders: {folders}, files: {files}": "Просмотр папок — папок: {folders}, файлов: {files}",
|
||||
"Moved {n} photo{} to “{}”. Edit → Undo moves them back.": "В «{1}» перемещено фото: {n}{0:.0}. Правка → Отменить вернёт их."
|
||||
}
|
||||
|
||||
@@ -296,5 +296,6 @@
|
||||
"Already in “{}”": "Уже в «{}»",
|
||||
"Move {n} photo{} to “{}”": "Перемістити до «{1}» фото: {n}{0:.0}",
|
||||
"Move {n} photo{} to “{}”?": "Перемістити до «{1}» фото: {n}{0:.0}?",
|
||||
"Listing folders — folders: {folders}, files: {files}": "Перегляд тек — тек: {folders}, файлів: {files}",
|
||||
"Moved {n} photo{} to “{}”. Edit → Undo moves them back.": "До «{1}» переміщено фото: {n}{0:.0}. Редагування → Скасувати поверне їх."
|
||||
}
|
||||
|
||||
@@ -296,5 +296,6 @@
|
||||
"Renames {n} face{} across the library. The files are not changed.": "在整个图库中重命名 {n} 张面孔。不会改动文件。{:.0}",
|
||||
"“{to}” is already a person. Combining them makes one person; Undo separates them again.": "“{to}”已经是另一个人。合并后成为一个人;撤销会再把他们分开。",
|
||||
"{what} merge added at {mp}: its full size, {full}, needs more memory than this computer has": "已添加 {what} 合并({mp}):完整尺寸({full})所需的内存超出了这台电脑的内存",
|
||||
"Listing folders — folders: {folders}, files: {files}": "正在列出文件夹 — 文件夹:{folders},文件:{files}",
|
||||
"{what} merge added at {mp}, the largest a panorama can be (its full size is {full})": "已添加 {what} 合并({mp}),这是全景图的最大尺寸(完整尺寸:{full})"
|
||||
}
|
||||
|
||||
@@ -296,5 +296,6 @@
|
||||
"Renames {n} face{} across the library. The files are not changed.": "在整個照片庫中重新命名 {n} 張面孔。不會改動檔案。{:.0}",
|
||||
"“{to}” is already a person. Combining them makes one person; Undo separates them again.": "「{to}」已經是另一個人。合併後成為一個人;還原會再把他們分開。",
|
||||
"{what} merge added at {mp}: its full size, {full}, needs more memory than this computer has": "已新增{what}合併照片({mp}):完整尺寸({full})所需的記憶體超過這台電腦的記憶體",
|
||||
"Listing folders — folders: {folders}, files: {files}": "正在列出資料夾 — 資料夾:{folders},檔案:{files}",
|
||||
"{what} merge added at {mp}, the largest a panorama can be (its full size is {full})": "已新增{what}合併照片({mp}),這是全景圖的最大尺寸(完整尺寸:{full})"
|
||||
}
|
||||
|
||||
@@ -18,12 +18,32 @@ use crate::theme::Tokens;
|
||||
/// A running scan of the dialog's folder.
|
||||
pub struct SyncTask {
|
||||
path: String,
|
||||
/// What the dialog calls the folder (its row's detail).
|
||||
name: String,
|
||||
progress: Arc<SyncProgress>,
|
||||
rx: std::sync::mpsc::Receiver<FolderChanges>,
|
||||
/// Its row in the activity stack; ✕ cancels the scan and closes the dialog.
|
||||
guard: TaskGuard,
|
||||
}
|
||||
|
||||
/// What a scan's row shows: (done, of how many, detail). While the folder is listed, which comes
|
||||
/// first and on a network share can take minutes, a bar that moves on its own and how many
|
||||
/// folders and files were listed (a count stuck at "0 of 64085" looks like a scan that stopped);
|
||||
/// then the files and photos checked.
|
||||
pub(crate) fn scan_row(progress: &SyncProgress, name: &str) -> (u64, u64, String) {
|
||||
match progress.listing() {
|
||||
Some((folders, files)) => (0, 0, format!("{} · {name}", listing_text(folders, files))),
|
||||
None => {
|
||||
let (done, total) = progress.counts();
|
||||
(done as u64, total as u64, name.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn listing_text(folders: usize, files: usize) -> String {
|
||||
crate::i18n::tr_format!("Listing folders — folders: {folders}, files: {files}", folders = folders, files = files)
|
||||
}
|
||||
|
||||
/// Open the dialog for folder `path` (called `name` in it) and start scanning.
|
||||
pub fn open(app: &mut LightcraftApp, path: &str, name: &str, disk: bool) -> Result<(), String> {
|
||||
cancel(app);
|
||||
@@ -40,7 +60,7 @@ pub fn open(app: &mut LightcraftApp, path: &str, name: &str, disk: bool) -> Resu
|
||||
job();
|
||||
let guard = app.session.activity.start("sync", "Looking for changes", Cancel::Flag(progress.files.cancel.clone()));
|
||||
guard.detail(name);
|
||||
app.sync = Some(SyncTask { path: path.to_string(), progress, rx, guard });
|
||||
app.sync = Some(SyncTask { path: path.to_string(), name: name.to_string(), progress, rx, guard });
|
||||
let d = SyncChoice::default();
|
||||
app.ui.dialog = Some(Dialog::SynchronizeFolder {
|
||||
path: path.to_string(),
|
||||
@@ -90,8 +110,9 @@ pub fn poll(app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||
app.ui.dialog = None;
|
||||
return;
|
||||
}
|
||||
let (done, total) = task.progress.counts();
|
||||
task.guard.progress(done as u64, total as u64);
|
||||
let (done, total, detail) = scan_row(&task.progress, &task.name);
|
||||
task.guard.progress(done, total);
|
||||
task.guard.detail(&detail);
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(100));
|
||||
let changes = match task.rx.try_recv() {
|
||||
Ok(c) => c,
|
||||
@@ -120,9 +141,21 @@ pub fn poll(app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/// How far the scan is (files probed, of how many).
|
||||
fn progress(app: &LightcraftApp) -> Option<(usize, usize)> {
|
||||
app.sync.as_ref().map(|t| t.progress.counts())
|
||||
/// How far the scan is: the folders and files listed while it lists the folder, else the files
|
||||
/// and photos checked (done, of how many).
|
||||
enum ScanStage {
|
||||
Listing(usize, usize),
|
||||
Checking(usize, usize),
|
||||
}
|
||||
|
||||
fn progress(app: &LightcraftApp) -> Option<ScanStage> {
|
||||
app.sync.as_ref().map(|t| match t.progress.listing() {
|
||||
Some((folders, files)) => ScanStage::Listing(folders, files),
|
||||
None => {
|
||||
let (done, total) = t.progress.counts();
|
||||
ScanStage::Checking(done, total)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The dialog's body.
|
||||
@@ -138,7 +171,8 @@ pub fn body(app: &LightcraftApp, ui: &mut egui::Ui, dlg: &mut Dialog) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spinner();
|
||||
match progress(app) {
|
||||
Some((done, total)) if total > 0 => ui.label(format!("{} {done} / {total}", crate::i18n::tr("Scanning…"))),
|
||||
Some(ScanStage::Listing(folders, files)) => ui.label(listing_text(folders, files)),
|
||||
Some(ScanStage::Checking(done, total)) if total > 0 => ui.label(format!("{} {done} / {total}", crate::i18n::tr("Scanning…"))),
|
||||
_ => ui.label(crate::i18n::tr("Scanning…")),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
//! * Synchronize hands the work to the background at once: the dialog closes, the app keeps
|
||||
//! answering, and the photos arrive over the next frames under a row in the activity stack, as one undo
|
||||
//! step.
|
||||
//! * While the folder is listed (minutes, on a network share), the scan's row says how many
|
||||
//! folders and files it has listed, with a bar that moves on its own, not "0 of 64085"; then it
|
||||
//! counts the files and photos checked.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -343,3 +346,19 @@ fn a_local_folder_with_library_photos_offers_synchronize() {
|
||||
assert!(matches!(h.app.ui.dialog, Some(Dialog::SynchronizeFolder { .. })), "the dialog opens at once");
|
||||
assert!(h.step_until(T, |h| matches!(&h.app.ui.dialog, Some(Dialog::SynchronizeFolder { counts: Some(_), .. }))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn while_the_folder_is_listed_its_row_says_what_was_listed_and_its_bar_moves_on_its_own() {
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
let p = lightcraft_engine::sync::SyncProgress::default();
|
||||
p.to_check.store(64085, Relaxed);
|
||||
p.folders_listed.store(312, Relaxed);
|
||||
p.files_listed.store(18402, Relaxed);
|
||||
let (done, total, detail) = crate::sync::scan_row(&p, "photos/Various");
|
||||
assert_eq!((done, total), (0, 0), "a bar that moves on its own, not 0 of 64085");
|
||||
assert_eq!(detail, "Listing folders — folders: 312, files: 18402 · photos/Various");
|
||||
// the listing is over: the files and photos checked, of how many
|
||||
p.listed.store(true, Relaxed);
|
||||
p.checked.store(10, Relaxed);
|
||||
assert_eq!(crate::sync::scan_row(&p, "photos/Various"), (10, 64085, "photos/Various".to_string()));
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,6 +24,15 @@
|
||||
another ratio (4 × 5, 2 × 3…) keeps it portrait, as in Lightroom, and a Custom ratio taller than wide gives a
|
||||
portrait crop.
|
||||
|
||||
## 2026-10-16
|
||||
|
||||
### Synchronize Folder shows that it is listing the folder
|
||||
- A folder with tens of thousands of photos, above all on a network share, can take minutes to list before
|
||||
Synchronize Folder checks a single photo. Its row in the activity stack used to sit at "0 of 64085" with an empty
|
||||
bar all that time, as if it had stopped. It now says how many folders and files it has listed so far, with a bar
|
||||
that moves on its own, and then counts the photos it checks. The dialog says the same.
|
||||
- ✕ on the row stops the listing too, not only the checks after it.
|
||||
|
||||
## 2026-10-15
|
||||
|
||||
### Constrain Crop tightens the crop you see
|
||||
|
||||
Reference in New Issue
Block a user