LR-LIB-BYDATE + LR-META-COPYRIGHT-DEFAULT: By Date day tree, default copyright/creator on import

- By Date: year → month → day rows with counts and disclosure triangles; a row filters by
  that date prefix (catalog date_groups now carries days).
- Settings → Import → Metadata: copyright and creator given to imported photos that don't
  carry their own (library.preferences {import: {copyright, creator}}), saved with the library.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-01 23:31:48 -04:00
co-authored by Claude Opus 5.5
parent 852e8fca45
commit b9f7ceb2d7
8 changed files with 139 additions and 18 deletions
+7 -1
View File
@@ -42,6 +42,12 @@ 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("copyright").and_then(Value::as_str) {
d.copyright = v.trim().to_string();
}
if let Some(v) = i.get("creator").and_then(Value::as_str) {
d.creator = v.trim().to_string();
}
if let Some(a) = i.get("cameras").and_then(Value::as_array) {
d.cameras.clear();
for c in a {
@@ -84,7 +90,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}]}, camera?: {camera, preset?, remove?: bool}, cacheMb?: n (0 = default)} — develop defaults applied on import (raws / other images / per camera) and the thumbnail cache size, saved with the library → {xmp, import, cacheMb, 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)}, camera?: {camera, preset?, remove?: bool}, cacheMb?: n (0 = default)} — develop defaults applied on import (raws / other images / per camera) and the thumbnail cache size, saved with the library → {xmp, import, cacheMb, persistent}",
always,
prefs
)]
+12
View File
@@ -120,6 +120,10 @@ pub struct ImportDefaults {
pub cameras: Vec<CameraDefault>,
/// Preset applied to non-raw images (JPEG, PNG, TIFF, HEIC…; `None` = none).
pub other_preset: Option<String>,
/// Copyright notice given to imported photos that don't carry one (empty = none).
pub copyright: String,
/// Creator given to imported photos that don't name one (empty = none).
pub creator: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
@@ -149,6 +153,14 @@ impl ImportDefaults {
/// Give a freshly imported photo its default settings: the camera defaults, then the matching
/// default preset ([`ImportDefaults::preset_for`]), remembered as the photo's import look.
pub fn apply_import_defaults(s: &Session, p: &mut Photo) {
// metadata defaults fill gaps only: the file's own copyright / creator win
let d = &s.import_defaults;
if p.meta.copyright.trim().is_empty() && !d.copyright.trim().is_empty() {
p.meta.copyright = d.copyright.trim().to_string();
}
if p.meta.creator.trim().is_empty() && !d.creator.trim().is_empty() {
p.meta.creator = d.creator.trim().to_string();
}
let base = p.camera_defaults();
let raw = p.kind == lightcraft_catalog::MediaKind::Raw;
let preset = s.import_defaults.preset_for(raw, &p.meta.camera).and_then(|id| s.presets.iter().find(|x| x.id == id));
+26
View File
@@ -137,3 +137,29 @@ fn preferences_persist_with_the_library() {
assert_eq!(s.import_defaults, Default::default());
let _ = std::fs::remove_dir_all(&lib);
}
#[test]
fn default_copyright_and_creator_fill_gaps_on_import() {
let dir = temp_dir("copyright");
let lib = dir.join("lib");
png(&dir.join("plain.png"));
// a DNG that names its own artist keeps it
let meta = lightcraft_meta::Metadata { artist: Some("Someone Else".into()), ..Default::default() };
std::fs::write(dir.join("own.dng"), crate::tests_xmp::synthetic_dng_with(None, meta)).unwrap();
let mut s = Session::new().with_fs();
s.open_library(&lib, false).unwrap();
let r = s.execute("library.preferences", &json!({"import": {"copyright": " © 2026 Me ", "creator": "Me"}})).unwrap();
assert_eq!(r["import"]["copyright"], "© 2026 Me");
s.execute("library.import", &json!({"paths": [dir.join("plain.png").to_string_lossy(), dir.join("own.dng").to_string_lossy()]})).unwrap();
let plain = photo_named(&s, "plain.png");
assert_eq!((plain.meta.copyright.as_str(), plain.meta.creator.as_str()), ("© 2026 Me", "Me"));
let own = photo_named(&s, "own.dng");
assert_eq!(own.meta.creator, "Someone Else", "the file's own creator wins");
assert_eq!(own.meta.copyright, "© 2026 Me");
// saved with the library
drop(s);
let mut s = Session::new().with_fs();
s.open_library(&lib, false).unwrap();
assert_eq!(s.import_defaults.creator, "Me");
let _ = std::fs::remove_dir_all(&dir);
}