LR-IMP-DEVICE: Show the import review when a card is connected

Adds an option under Settings > Import > Cameras and Cards that opens Import from Device when a camera or memory card shows up. It's off by default, and a card that was already in at startup doesn't ask.
This commit is contained in:
Michael Anderson
2026-10-14 07:32:47 -05:00
parent a4b53b28cc
commit 441f46683a
20 changed files with 149 additions and 4 deletions
+4 -1
View File
@@ -43,6 +43,9 @@ fn prefs(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(v) = i.get("perCamera").and_then(Value::as_bool) {
d.per_camera = v;
}
if let Some(v) = i.get("showOnCard").and_then(Value::as_bool) {
d.show_on_card = v;
}
if let Some(v) = i.get("copyright").and_then(Value::as_str) {
d.copyright = v.trim().to_string();
}
@@ -107,7 +110,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Library Preferences",
[],
None,
"{import?: {rawPreset?: presetId|\"default\", otherPreset?: presetId|\"default\", perCamera?: bool, cameras?: [{camera: \"Make Model\", preset: presetId|null}], copyright?: text, creator?: text (given to imported photos without one)}, camera?: {camera, preset?, remove?: bool}, cacheMb?: n (0 = default), forgetLocalDays?: n (forget untouched Local photos of folders not browsed for n days; 0 = never)} — develop defaults applied on import (raws / other images / per camera) and the thumbnail cache size, saved with the library → {xmp, import, cacheMb, forgetLocalDays, persistent}",
"{import?: {rawPreset?: presetId|\"default\", otherPreset?: presetId|\"default\", perCamera?: bool, cameras?: [{camera: \"Make Model\", preset: presetId|null}], copyright?: text, creator?: text (given to imported photos without one), showOnCard?: bool (open the import review when a camera or card is connected)}, camera?: {camera, preset?, remove?: bool}, cacheMb?: n (0 = default), forgetLocalDays?: n (forget untouched Local photos of folders not browsed for n days; 0 = never)} — develop defaults applied on import (raws / other images / per camera) and the thumbnail cache size, saved with the library → {xmp, import, cacheMb, forgetLocalDays, persistent}",
always,
prefs
)]
+38
View File
@@ -104,6 +104,18 @@ impl DeviceWatch {
s.list.clone()
}
/// Like [`DeviceWatch::get`], but `None` until the first scan has landed, so an empty list
/// means no cards rather than not looked yet.
pub fn get_scanned(self: &Arc<Self>) -> Option<Vec<Device>> {
let _ = self.get();
let s = self.state();
#[cfg(not(target_arch = "wasm32"))]
let scanned = s.scanned.is_some();
#[cfg(target_arch = "wasm32")]
let scanned = false;
scanned.then(|| s.list.clone())
}
/// Scan now on this thread, and serve the result to later [`DeviceWatch::get`] calls.
pub fn scan_now(&self) -> Vec<Device> {
let list = (self.scanner)();
@@ -148,6 +160,12 @@ pub fn devices() -> Vec<Device> {
global().get()
}
/// [`devices`], but `None` until the first scan lands. The import prompt uses it so a card that was
/// already in when LightCraft started isn't taken for a new one.
pub fn devices_scanned() -> Option<Vec<Device>> {
global().get_scanned()
}
/// Scan the mounted volumes now (blocking), for explicit requests.
pub fn devices_now() -> Vec<Device> {
global().scan_now()
@@ -285,4 +303,24 @@ mod tests {
assert!(watch.scan_now().is_empty());
assert_eq!(changes.load(Ordering::SeqCst), 2);
}
#[test]
fn get_scanned_waits_for_the_first_scan() {
let gate = Arc::new(std::sync::atomic::AtomicBool::new(false));
let g = gate.clone();
let watch = DeviceWatch::new(
move || {
while !g.load(Ordering::SeqCst) {
std::thread::sleep(Duration::from_millis(5));
}
vec![card("NIKON Z 8")]
},
Duration::from_secs(60),
);
assert_eq!(watch.get_scanned(), None, "nothing scanned yet");
assert_eq!(watch.get_scanned(), None);
gate.store(true, Ordering::SeqCst);
assert!(until(|| watch.get_scanned().is_some()));
assert_eq!(watch.get_scanned(), Some(vec![card("NIKON Z 8")]));
}
}
+2
View File
@@ -310,6 +310,8 @@ pub struct ImportDefaults {
pub auto_folder: Option<String>,
pub auto_copy: bool,
pub auto_album: Option<String>,
/// Open the import review when a camera or memory card is connected.
pub show_on_card: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
+2 -1
View File
@@ -108,7 +108,7 @@ fn preferences_persist_with_the_library() {
s.execute("library.xmpPreferences", &json!({"autoWrite": true})).unwrap();
s.execute(
"library.preferences",
&json!({"import": {"rawPreset": "user.rawlook", "perCamera": true}, "camera": {"camera": "Canon EOS R5", "preset": "user.canonlook"}, "cacheMb": 512}),
&json!({"import": {"rawPreset": "user.rawlook", "perCamera": true, "showOnCard": true}, "camera": {"camera": "Canon EOS R5", "preset": "user.canonlook"}, "cacheMb": 512}),
)
.unwrap();
s.close_library().unwrap();
@@ -118,6 +118,7 @@ fn preferences_persist_with_the_library() {
assert!(s.xmp.auto_write);
assert_eq!(s.import_defaults.raw_preset.as_deref(), Some("user.rawlook"));
assert!(s.import_defaults.per_camera);
assert!(s.import_defaults.show_on_card);
assert_eq!(s.import_defaults.cameras.len(), 1);
assert_eq!(s.import_defaults.preset_for(true, "Canon EOS R5"), Some("user.canonlook"));
assert_eq!(s.cache_mb, 512);