# Conflicts:
#	docs/whats-new.md
This commit is contained in:
Claude committed 2026-10-11 11:42:45 +00:00
commit cb5cd22bd3
3 files changed
+78 -1

No files matched your search

+71
View File
@@ -1488,3 +1488,74 @@ fn heic_imports_as_a_normal_photo_or_says_why_not() {
assert!(mean(&brighter.bytes) > mean(&plain.bytes) * 1.3, "exposure +1 brightens the HEIC");
let _ = std::fs::remove_dir_all(&src);
}
/// Issue #705: Nikon (and most cameras) write `.JPG` and `.NEF`, and a user may rename to `.Jpg`.
/// The folder scan, `library.browse` and the sidecar lookup match extensions in any letter
/// case. (The file dialog's filters, the one place that was case-sensitive on Linux before
/// #342, are checked in `apps/lightcraft/src/dialog_filter.rs`.)
#[test]
fn upper_and_mixed_case_extensions_are_found_by_import_browse_and_sidecar_lookup() {
use lightcraft_codecs::{ChromaSubsampling, EncodeImage, EncodeMeta, Samples, encode_jpeg};
let src = temp_dir("upper-exts");
let jpeg = |seed: u8| {
let px: Vec<u8> = (0..40 * 30).flat_map(|i| [(i % 251) as u8, seed, 200]).collect();
encode_jpeg(&EncodeImage::new(40, 30, 3, Samples::U8(&px)), 90, ChromaSubsampling::S444, &EncodeMeta::default()).unwrap()
};
std::fs::write(src.join("a.JPG"), jpeg(1)).unwrap();
std::fs::write(src.join("b.Jpg"), jpeg(2)).unwrap();
std::fs::write(src.join("c.JPEG"), jpeg(3)).unwrap();
write_png(&src.join("d.PNG"), 4);
// a raw container LightCraft shows by its embedded JPEG (a TIFF whose IFD0 points at one)
let nef = {
let j = jpeg(5);
let off = 8 + 2 + 24 + 4;
let mut f = b"II*\0\x08\0\0\0\x02\0".to_vec();
for (tag, v) in [(513u16, off as u32), (514, j.len() as u32)] {
f.extend_from_slice(&tag.to_le_bytes());
f.extend_from_slice(&[4, 0, 1, 0, 0, 0]);
f.extend_from_slice(&v.to_le_bytes());
}
f.extend_from_slice(&[0, 0, 0, 0]);
f.extend_from_slice(&j);
f
};
std::fs::write(src.join("e.NEF"), nef).unwrap();
std::fs::write(src.join("notes.TXT"), "not a photo").unwrap();
// sidecars: stem-named with an upper-case extension, and full-named beside a mixed-case photo
let sidecar = |rating: u8| {
format!(
r#"<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmp:Rating="{rating}"/></rdf:RDF></x:xmpmeta>"#
)
};
std::fs::write(src.join("a.XMP"), sidecar(4)).unwrap();
std::fs::write(src.join("b.Jpg.xmp"), sidecar(2)).unwrap();
let mut s = Session::new().with_fs();
let r = s.execute("library.import", &json!({"paths": [src.to_string_lossy()]})).unwrap();
assert_eq!(r["scanned"], 5, "{r}"); // the five photos; notes.TXT is not one
assert_eq!(ids(&r, "imported"), 5, "{r}");
assert_eq!(ids(&r, "failed"), 0, "{r}");
assert_eq!(r["sidecars"], 2, "{r}");
let names: Vec<(String, String, u8)> = s.catalog.photos().map(|p| (p.file_name.clone(), p.format.clone(), p.rating)).collect();
let by_name = |n: &str| names.iter().find(|(f, _, _)| f == n).cloned().unwrap_or_else(|| panic!("{n} missing from {names:?}"));
assert_eq!(by_name("a.JPG").2, 4, "a.XMP read");
assert_eq!(by_name("b.Jpg").2, 2, "b.Jpg.xmp read");
assert_eq!(by_name("c.JPEG").1, "JPEG");
assert_eq!(by_name("d.PNG").1, "PNG");
let nef = s.catalog.photos().find(|p| p.file_name == "e.NEF").unwrap();
assert!(nef.kind == lightcraft_catalog::MediaKind::Raw && nef.preview_only.is_some(), "{nef:?}");
// Browse Folder (the Local section) lists the same five, without adding them to the library
let mut b = Session::new().with_fs();
let r = b.execute("library.browse", &json!({"path": src.to_string_lossy()})).unwrap();
assert_eq!(r["photos"], 5, "{r}");
assert_eq!(r["new"], 5, "{r}");
assert_eq!(r["failed"], 0, "{r}");
let visible = b.visible_cloned();
let shown: Vec<String> = visible.iter().filter_map(|id| b.catalog.photo(*id).map(|p| p.file_name.clone())).collect();
for n in ["a.JPG", "b.Jpg", "c.JPEG", "d.PNG", "e.NEF"] {
assert!(shown.iter().any(|f| f == n), "{n} not shown: {shown:?}");
}
let _ = std::fs::remove_dir_all(&src);
}
+1 -1
View File
@@ -69,7 +69,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
| Id | Feature | Tier | Status | Evidence | Missing / notes |
|---|---|---|---|---|---|
| LR-IMP-ADD-DIALOG | Add photos/folders | P0 | ✅ | `cmd:file.addPhotos`, `cmd:library.importPreview`, `cmd:library.import` (`mode`, `album`, `albumName`, `preset`, `keywords`), `crates/ui-egui/src/import.rs` | review dialog (Import Photos): the scanned source (a scanned folder is not added to Local), candidate grid with thumbnails and checkboxes (duplicates by path/content marked and unchecked; with several file types, a toggle per type checks or unchecks all of its files, e.g. the raws without the JPEGs beside them, issue #344) filling a resizable window (issue #337), transfer mode (add in place / copy / move, each explained), album (existing/new), preset, keywords; the folder scan runs on a background thread, shown in the activity stack with ✕ to cancel (a NAS folder no longer freezes the window); the import itself (probing, sidecars, copying / moving files) runs on a worker thread and its batches join the library between frames, shown in the activity stack with ✕ to cancel; one undo step. Drag-and-drop goes through the same background import. File dialogs list the importable extensions (`lightcraft_engine::import::EXTENSIONS`) in any letter case on Linux, where GTK and the desktop portal match globs case-sensitively (`apps/lightcraft/src/dialog_filter.rs`, issue #342) |
| LR-IMP-ADD-DIALOG | Add photos/folders | P0 | ✅ | `cmd:file.addPhotos`, `cmd:library.importPreview`, `cmd:library.import` (`mode`, `album`, `albumName`, `preset`, `keywords`), `crates/ui-egui/src/import.rs` | review dialog (Import Photos): the scanned source (a scanned folder is not added to Local), candidate grid with thumbnails and checkboxes (duplicates by path/content marked and unchecked; with several file types, a toggle per type checks or unchecks all of its files, e.g. the raws without the JPEGs beside them, issue #344) filling a resizable window (issue #337), transfer mode (add in place / copy / move, each explained), album (existing/new), preset, keywords; the folder scan runs on a background thread, shown in the activity stack with ✕ to cancel (a NAS folder no longer freezes the window); the import itself (probing, sidecars, copying / moving files) runs on a worker thread and its batches join the library between frames, shown in the activity stack with ✕ to cancel; one undo step. Drag-and-drop goes through the same background import. File dialogs list the importable extensions (`lightcraft_engine::import::EXTENSIONS`) in any letter case on Linux, where GTK and the desktop portal match globs case-sensitively (`apps/lightcraft/src/dialog_filter.rs`, issue #342; 0.4.0 hid `.JPG` / `.NEF`, issue #705). The folder scan, Browse Folder, drag-and-drop and the sidecar lookup match extensions in any case (`crates/engine/src/tests_import.rs`) |
| LR-IMP-DRAGDROP | Drop files/folders to import | P0 | ✅ | `crates/ui-egui/src/lib.rs` (dropped files → `cmd:library.import`) | dropping onto a specific album not supported |
| LR-IMP-DUPES | Skip duplicates by content | P1 | ✅ | `crates/engine/src/import.rs`, `crates/engine/src/tests_import.rs` | |
| LR-IMP-DEVICE | Import from camera/card | P1 | ✅ | `cmd:library.devices`, `cmd:file.addFromDevice`, `crates/engine/src/devices.rs` | mounted volumes with a DCIM folder (macOS /Volumes, Linux /media and /run/media, Windows drive letters); File → Import from Device → the import review, copying into the library by default; menus serve the last scan and rescan in the background (never blocking a frame; hot-plugs repaint); copies are verified (a new file, synced, checked against the content hash the scan computed — byte compare with the source when there is none, issue #134; a bad copy is removed and reported as failed, never counted as imported; taken names get -1, -2…, never replaced — issue #96, `crates/engine/src/import_move.rs`); no PTP/MTP (cameras that don't mount as a disk) |
+6
View File
@@ -21,6 +21,12 @@
never restore anything. Agents: `library.import` → `onDeleted` defaults to `restore` (`skip` keeps the old
behaviour, `fresh` imports a clean copy and deletes the old record).
### Formats
- File extensions match whatever their letter case: `IMG_0001.JPG` and `DSC_0002.NEF` as Nikon writes them, `.Jpg`
after a rename, an `.XMP` sidecar. Adding photos or a folder, Browse Folder (Local), drag-and-drop, the CLI and the
sidecar lookup always did; the Add Photos dialog on Linux did not until 0.5.0 (GTK and the desktop portal match
`*.jpg` case-sensitively, so `.JPG` and `.NEF` files were hidden — issues #342 and #705).
## 2026-10-09
### Smart album rules