mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 05:57:09 -06:00
LR-IMP-ADD-DIALOG: an import looks up the sidecars of a batch's photos several at a time
Where the folder listing can't vouch for a name (non-ASCII names, a listing that is gone), each photo's sidecar names are asked of the disk, nine per photo, one photo after another. The batch's lookups now run 16 at a time; the loop that plans the photos takes each one's answer in order. They use the same `at_once` as the probe checks, so the latency stand-in measured with the previous commit applies to them as well (16 at a time against one at a time); the lookups themselves were not timed on a share for this version. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
6c19862876
commit
d402fcc4b6
@@ -245,7 +245,10 @@ pub(crate) enum Seen {
|
||||
Unchecked,
|
||||
/// Not there, or not readable.
|
||||
Gone,
|
||||
At { is_file: bool, len: u64 },
|
||||
At {
|
||||
is_file: bool,
|
||||
len: u64,
|
||||
},
|
||||
}
|
||||
|
||||
/// [`Seen`] of `path` from the file system.
|
||||
@@ -256,57 +259,74 @@ fn stat_file(path: &str) -> Seen {
|
||||
}
|
||||
}
|
||||
|
||||
/// Names asked about at once: a stat on a network share is a round trip (50 ms and more under load)
|
||||
/// that waits for nothing but the server, so a few more than the readers' threads.
|
||||
/// Files asked about at once: a question to a network share (a stat, a name looked up) is a round
|
||||
/// trip (50 ms and more under load) that waits for nothing but the server, so a few more than the
|
||||
/// readers' threads.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const STAT_THREADS: usize = 16;
|
||||
const AT_ONCE: usize = 16;
|
||||
|
||||
/// Ask `stat` about each of `paths`, several at once (one at a time, a folder of thousands of photos
|
||||
/// on a network share waited minutes for its stats alone). The answers come in the order of `paths`;
|
||||
/// the ones not reached once `cancel` is set are [`Seen::Unchecked`].
|
||||
pub(crate) fn stat_all(paths: &[&str], cancel: &std::sync::atomic::AtomicBool, stat: impl Fn(&str) -> Seen + Sync) -> Vec<Seen> {
|
||||
/// Run `ask` on each of `items`, several at once (one at a time, a folder of thousands of photos on
|
||||
/// a network share waited minutes for its questions alone). The answers come in the order of
|
||||
/// `items`; `None` for an item not reached once `cancel` is set (or whose asker died).
|
||||
fn at_once<T: Sync, R: Send>(items: &[T], cancel: &std::sync::atomic::AtomicBool, ask: impl Fn(&T) -> R + Sync) -> Vec<Option<R>> {
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
let mut seen = vec![Seen::Unchecked; paths.len()];
|
||||
let mut answers: Vec<Option<R>> = items.iter().map(|_| None).collect();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let n = STAT_THREADS.min(paths.len());
|
||||
let n = AT_ONCE.min(items.len());
|
||||
if n > 1 {
|
||||
let next = std::sync::atomic::AtomicUsize::new(0);
|
||||
let answers: Vec<Vec<(usize, Seen)>> = std::thread::scope(|sc| {
|
||||
let readers: Vec<_> = (0..n)
|
||||
let mine: Vec<Vec<(usize, R)>> = std::thread::scope(|sc| {
|
||||
let askers: Vec<_> = (0..n)
|
||||
.map(|_| {
|
||||
sc.spawn(|| {
|
||||
let mut mine = Vec::new();
|
||||
loop {
|
||||
let i = next.fetch_add(1, Relaxed);
|
||||
let Some(path) = paths.get(i) else { break };
|
||||
let Some(item) = items.get(i) else { break };
|
||||
if cancel.load(Relaxed) {
|
||||
break;
|
||||
}
|
||||
mine.push((i, stat(path)));
|
||||
mine.push((i, ask(item)));
|
||||
}
|
||||
mine
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
// (a reader that died leaves its files unchecked)
|
||||
readers.into_iter().map(|r| r.join().unwrap_or_default()).collect()
|
||||
askers.into_iter().map(|a| a.join().unwrap_or_default()).collect()
|
||||
});
|
||||
for (i, s) in answers.into_iter().flatten() {
|
||||
if let Some(slot) = seen.get_mut(i) {
|
||||
*slot = s;
|
||||
for (i, r) in mine.into_iter().flatten() {
|
||||
if let Some(slot) = answers.get_mut(i) {
|
||||
*slot = Some(r);
|
||||
}
|
||||
}
|
||||
return seen;
|
||||
return answers;
|
||||
}
|
||||
}
|
||||
for (slot, path) in seen.iter_mut().zip(paths) {
|
||||
for (slot, item) in answers.iter_mut().zip(items) {
|
||||
if cancel.load(Relaxed) {
|
||||
break;
|
||||
}
|
||||
*slot = stat(path);
|
||||
*slot = Some(ask(item));
|
||||
}
|
||||
seen
|
||||
answers
|
||||
}
|
||||
|
||||
/// Ask `stat` about each of `paths`, several at once ([`at_once`]); the ones not reached once
|
||||
/// `cancel` is set are [`Seen::Unchecked`].
|
||||
pub(crate) fn stat_all(paths: &[&str], cancel: &std::sync::atomic::AtomicBool, stat: impl Fn(&str) -> Seen + Sync) -> Vec<Seen> {
|
||||
at_once(paths, cancel, |p| stat(p)).into_iter().map(|s| s.unwrap_or(Seen::Unchecked)).collect()
|
||||
}
|
||||
|
||||
/// The sidecar of each of `files` (`find` says which: a few names looked up on the disk for each,
|
||||
/// a round trip apiece on a network share), several files at once ([`at_once`]); `None` for a
|
||||
/// file with no sidecar or not reached once `cancel` is set.
|
||||
pub(super) fn sidecars_of(
|
||||
files: &[&str],
|
||||
cancel: &std::sync::atomic::AtomicBool,
|
||||
find: impl Fn(&str) -> Option<PathBuf> + Sync,
|
||||
) -> Vec<Option<PathBuf>> {
|
||||
at_once(files, cancel, |f| find(f)).into_iter().map(Option::flatten).collect()
|
||||
}
|
||||
|
||||
/// The probes in `cache` of `files` that are still true: the file is looked at ([`stat_all`]) and
|
||||
@@ -539,4 +559,62 @@ mod prepared_tests {
|
||||
assert_eq!(prepared.items.len(), 4);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Given photos on a network share, when an import looks for their sidecars, then it asks about
|
||||
/// several photos at once: one photo after another, nine names each, they were 77 % of the
|
||||
/// import's preparation (95 % where a folder listing could not vouch for the names).
|
||||
#[test]
|
||||
fn sidecars_are_looked_up_for_several_photos_at_once() {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
|
||||
let (active, together) = (AtomicUsize::new(0), AtomicUsize::new(0));
|
||||
let names: Vec<String> = (0..6).map(|i| format!("photo-{i}.jpg")).collect();
|
||||
let files: Vec<&str> = names.iter().map(String::as_str).collect();
|
||||
let found = sidecars_of(&files, &std::sync::atomic::AtomicBool::new(false), |f| {
|
||||
active.fetch_add(1, Relaxed);
|
||||
let end = std::time::Instant::now() + std::time::Duration::from_secs(1);
|
||||
while active.load(Relaxed) < 2 && std::time::Instant::now() < end {
|
||||
std::thread::yield_now();
|
||||
}
|
||||
together.fetch_max(active.load(Relaxed), Relaxed);
|
||||
active.fetch_sub(1, Relaxed);
|
||||
f.ends_with("-3.jpg").then(|| PathBuf::from(f).with_extension("xmp"))
|
||||
});
|
||||
assert!(together.load(Relaxed) >= 2, "the photos were looked up one at a time");
|
||||
let expect: Vec<Option<PathBuf>> = (0..6).map(|i| (i == 3).then(|| PathBuf::from("photo-3.xmp"))).collect();
|
||||
assert_eq!(found, expect, "each sidecar stays with its photo");
|
||||
}
|
||||
|
||||
/// The sidecars found for a batch go to the photos they belong to, whichever have one, and a
|
||||
/// photo that failed to probe takes none of the others'.
|
||||
#[test]
|
||||
fn a_batchs_sidecars_go_to_their_own_photos() {
|
||||
let dir = std::env::temp_dir().join(format!("lightcraft-sidecars-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let xmp = r#"<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmp:Rating="4"/></rdf:RDF></x:xmpmeta>"#;
|
||||
let files: Vec<String> = ["a.jpg", "broken.jpg", "c.jpg", "d.jpg"].iter().map(|n| dir.join(n).to_string_lossy().to_string()).collect();
|
||||
for f in &files {
|
||||
std::fs::write(f, b"x").unwrap();
|
||||
}
|
||||
std::fs::write(dir.join("c.xmp"), xmp).unwrap();
|
||||
let mut s = Session::new();
|
||||
let mut job = ImportJob::new(&mut s, ImportOptions { mode: ImportMode::Add, ..ImportOptions::default() }).unwrap();
|
||||
job.probe = Some(std::sync::Arc::new(|p: &str| {
|
||||
if p.ends_with("broken.jpg") {
|
||||
Err("unreadable".into())
|
||||
} else {
|
||||
Ok(ProbeInfo { format: "JPEG".into(), file_size: 1, ..ProbeInfo::default() })
|
||||
}
|
||||
}));
|
||||
let prepared = job.prepare_files(files, &std::sync::atomic::AtomicBool::new(false));
|
||||
let rating = |name: &str| {
|
||||
prepared.items.iter().find_map(|i| match i {
|
||||
PreparedItem::Ready(r) if r.path.ends_with(name) => Some(r.sidecar.as_ref().and_then(|sc| sc.rating)),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
assert_eq!((rating("a.jpg"), rating("c.jpg"), rating("d.jpg")), (Some(None), Some(Some(4)), Some(None)));
|
||||
assert!(prepared.items.iter().any(|i| matches!(i, PreparedItem::Failed(p, _) if p.ends_with("broken.jpg"))));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,6 +289,20 @@ impl ImportJob {
|
||||
library.as_ref(),
|
||||
todo.iter().zip(&probed).filter_map(|(path, info)| info.as_ref().ok().map(|i| (path.as_str(), i.kind))),
|
||||
);
|
||||
// The sidecar of every file that was probed, looked up for the batch at once: each is a few
|
||||
// names asked of the disk, which on a network share made a folder of thousands of photos wait
|
||||
// a round trip per name, one name after another.
|
||||
let asked: Vec<&str> = todo.iter().zip(&probed).filter(|(_, p)| p.is_ok()).map(|(f, _)| f.as_str()).collect();
|
||||
// (a file whose content the library has is skipped below as a duplicate: it asks the disk nothing)
|
||||
let known: std::collections::HashSet<&str> = todo
|
||||
.iter()
|
||||
.zip(&probed)
|
||||
.filter(|(_, p)| !opts.local && p.as_ref().is_ok_and(|i| i.content_hash.as_ref().is_some_and(|h| self.by_hash.contains_key(h))))
|
||||
.map(|(f, _)| f.as_str())
|
||||
.collect();
|
||||
let mut found =
|
||||
sidecars_of(&asked, cancel, |f| if known.contains(f) { None } else { crate::sidecar::find_sidecar_for_import(&stems, self.naming, f) })
|
||||
.into_iter();
|
||||
// Files are planned one by one, in order (duplicates, sidecars, names, folders); their
|
||||
// copies or moves — the slow part, reading and writing every byte and verifying it — are
|
||||
// collected and run on several threads (issue #367), then their outcomes take their places.
|
||||
@@ -306,6 +320,8 @@ impl ImportJob {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// (this file's place in the lookups above: files that failed to probe have none)
|
||||
let found_sidecar = found.next().flatten();
|
||||
// a copy of a file whose transfer is still pending: a duplicate only if that one succeeds
|
||||
if !opts.local
|
||||
&& let Some(h) = &info.content_hash
|
||||
@@ -327,9 +343,7 @@ impl ImportJob {
|
||||
// the XMP sidecar (or a raw's embedded XMP), read before copying: its capture time files
|
||||
// and names the copy when the file itself has none
|
||||
let raw = info.kind == MediaKind::Raw;
|
||||
let packet = crate::sidecar::find_sidecar_for_import(&stems, self.naming, &path)
|
||||
.and_then(|f| std::fs::read_to_string(f).ok())
|
||||
.or_else(|| info.xmp.clone().filter(|_| raw));
|
||||
let packet = found_sidecar.and_then(|f| std::fs::read_to_string(f).ok()).or_else(|| info.xmp.clone().filter(|_| raw));
|
||||
let target = crate::crs::Target::for_file(info.kind, &info.format, info.preview_only.is_some(), &info.meta.camera);
|
||||
let sidecar = packet.and_then(|x| match crate::sidecar::parse_sidecar(&x, target) {
|
||||
Ok(sc) => Some(sc),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Lightroom parity checklist
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (LR-IMP-ADD-DIALOG: an import asks the disk about the probes a scan left 16 files at a time; LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (LR-IMP-ADD-DIALOG: an import asks the disk about the probes a scan left, and for each photo's sidecar, up to 16 files at a time; LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
|
||||
The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we
|
||||
have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good
|
||||
@@ -959,4 +959,4 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
|
||||
| 2026-10-23 | patch | LR-BEHAV-ACCESS: the name field of New Album, New Smart Album, Rename and the other one-field dialogs takes the keyboard focus once when the dialog opens instead of every frame, so Tab and Shift+Tab reach the check box, Cancel and OK; status unchanged (🟡) |
|
||||
| 2026-10-22 | minor | LR-CROP-AUTO: Auto straighten turns by at most 10° and only on at least 3 agreeing lines, and says when it finds nothing to level by; measured on 68 photos straightened by hand and 300 left as shot, unwanted turns of more than 1° fell from 79 to 19 of the 300. Downgraded ✅ → 🟡: it still leaves most tilted photos alone |
|
||||
| 2026-10-23 | patch | LRC-LIB-FILTERBAR: showing an album (`library.source`) while the filter holds an album drops the filter's album, as showing a library folder already dropped its folder filter; before, the old album stayed in the filter unseen (the filter bar has no album chip) and narrowed the next source such as All Photos; the row's note says so; status unchanged (✅) |
|
||||
| 2026-10-24 | patch | LR-IMP-ADD-DIALOG: an import checks that each file a scan already probed is as it was 16 files at a time instead of one after another; on a network share a folder of thousands of photos spent most of its import time waiting for those checks; status unchanged (✅) |
|
||||
| 2026-10-24 | patch | LR-IMP-ADD-DIALOG: an import checks that each file a scan already probed is as it was up to 16 files at a time (an import batch's files, at most) instead of one after another, and looks up a batch's sidecars the same way (`import::sidecars_of`); on a network share a folder of thousands of photos spent most of its import time waiting for those checks; status unchanged (✅) |
|
||||
|
||||
+3
-1
@@ -5,8 +5,10 @@
|
||||
### Importing a folder from a network share waits less
|
||||
- After the import review, the import used to check that each photo was still as the review had seen it by asking the
|
||||
disk about one photo after another, so on a network share every photo waited for a round trip to the server. It now
|
||||
asks about 16 photos at a time, the same wait for every 16 instead of for each one. Reading the photos in the review
|
||||
asks about up to 16 photos at a time, the same wait for every 16 instead of for each one. Reading the photos in the review
|
||||
is unchanged.
|
||||
- Looking for each photo's `.xmp` sidecar, which asks the disk several names for every photo, also goes up to 16
|
||||
photos at a time.
|
||||
|
||||
### Relative-date filters and smart albums follow the clock
|
||||
- A current view or source count using “in the last” rules now refreshes when the session clock moves into a new
|
||||
|
||||
Reference in New Issue
Block a user