LR-LIB-CAPTURETIME: reject impossible anchor dates before shifting photos

Co-authored-by: Codex <[email protected]>
This commit is contained in:
sanyamsingh001andCodex committed 2026-10-09 22:23:04 +10:00
1 parent c435d143de
commit 4266b0ad7f
3 files changed
+44 -2

No files matched your search

+5 -1
View File
@@ -197,7 +197,11 @@ pub fn specs() -> Vec<CommandSpec> {
each = Some(t);
} else {
let anchor = s.active().filter(|a| targets.contains(a)).unwrap_or(targets[0]);
let from = iso_seconds(&base(s, anchor)).ok_or_else(|| bad(c, "the photo's date doesn't parse"))?;
let anchor_date = base(s, anchor);
// An impossible calendar date cannot anchor changes to the other photos.
let from = normalize_iso(anchor_date.trim().get(..10).unwrap_or(""))
.and_then(|_| iso_seconds(&anchor_date))
.ok_or_else(|| bad(c, "the photo's date doesn't parse"))?;
delta += iso_seconds(&t).unwrap_or(from) - from;
}
}
+38
View File
@@ -551,3 +551,41 @@ fn choosing_random_starts_a_fresh_shuffle() {
assert_eq!(s.sort.seed, 42, "an explicit seed is honoured");
assert!(s.sort.seed < 1 << 53);
}
fn capture_time_raw_metadata_session(raw: Option<&str>) -> Session {
use lightcraft_catalog::{Op, Photo, PhotoId, Source};
let mut s = Session::new();
let id = PhotoId(1);
let mut p = Photo::new(id, Source::Demo { scene: 0 }, "one.jpg", "JPEG", 4, 3, "2026-04-01T00:00:00");
p.captured = raw.map(str::to_string);
s.catalog.apply(Op::AddPhoto { photo: Box::new(p) }).unwrap();
s.selection = crate::Selection::single(id);
s
}
#[test]
fn capture_time_invalid_calendar_anchor_keeps_other_targets_and_redo() {
use lightcraft_catalog::{Op, Photo, PhotoId, Source};
for raw in ["2026-02-30T12:00:00", "2026x03x01T12:00:00"] {
let mut s = capture_time_raw_metadata_session(Some(raw));
let peer = PhotoId(2);
let mut p = Photo::new(peer, Source::Demo { scene: 0 }, "peer.jpg", "JPEG", 4, 3, "2026-10-09T00:00:00");
p.captured = Some("2026-03-01T12:00:00.25+02:00".into());
s.catalog.apply(Op::AddPhoto { photo: Box::new(p) }).unwrap();
s.commit("Earlier rating", Op::SetRating { id: peer, rating: 4 }).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
s.selection.ids = vec![PhotoId(1), peer];
s.selection.active = Some(PhotoId(1));
let before = s.catalog.clone();
let selection = s.selection.clone();
let undo: Vec<_> = s.undo.iter().map(|e| (e.label.clone(), e.op.clone())).collect();
let redo: Vec<_> = s.redo.iter().map(|e| (e.label.clone(), e.op.clone())).collect();
let log = s.pending_log.clone();
assert!(s.execute("photo.setCaptureTime", &json!({"time": "2026-03-03T12:00:00"})).is_err(), "{raw}");
assert_eq!(s.catalog, before);
assert_eq!(s.selection, selection);
assert_eq!(s.undo.iter().map(|e| (e.label.clone(), e.op.clone())).collect::<Vec<_>>(), undo);
assert_eq!(s.redo.iter().map(|e| (e.label.clone(), e.op.clone())).collect::<Vec<_>>(), redo);
assert_eq!(s.pending_log, log);
}
}
+1 -1
View File
@@ -172,7 +172,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
| LR-LIB-REMOVE-ALBUM | Remove from album | P0 | ✅ | `cmd:album.removePhotos` | |
| LR-LIB-DUPLICATE | Duplicate a photo | P2 | ✅ | `cmd:photo.duplicate` | Photo ▸ Duplicate: a real `-copy` file with the same settings, metadata and albums (virtual copies share the file) |
| LR-LIB-RENAME | Batch rename | P1 | ✅ | `cmd:photo.rename`, `cmd:photo.renamePreview`, `cmd:photo.renameTokens`, `cmd:dialog.rename`, `crates/engine/src/rename.rs` (`TOKENS`) | template tokens {name} {num} {seq:N} {date:%Y%m%d} {folder} {camera} {lens} {iso} {rating} {title} {creator} {ext} (shared with export naming and import rename); a Tags picker beside every template field (Rename Photos, import Rename, export File name) lists each tag with meaning + live example, the date directives and the rules, and inserts the clicked tag at the text cursor; unknown tags are flagged; preview; renames files on disk with their XMP sidecars (a stem sidecar another file still shares, e.g. raw + JPEG, is copied rather than taken), never overwriting (-1, -2… suffixes; a change of letter case only is checked by file identity, so on case-sensitive volumes `img_1.jpg` next to `IMG_1.JPG` is a collision), rolls back on failure (a file that can't be moved back is reported old → new and the library follows it as an undoable partial rename; the same for undo/redo); undo/redo move the files; virtual copies follow |
| LR-LIB-CAPTURETIME | Edit capture time | P1 | ✅ | `cmd:photo.setCaptureTime`, `cmd:dialog.captureTime`, `crates/catalog/src/dates.rs` | set (the other selected photos shift by the same amount, or `each`), shift by days/hours/minutes, time-zone shift; one undo step, journaled; Info panel button; no “revert to original capture time” |
| LR-LIB-CAPTURETIME | Edit capture time | P1 | ✅ | `cmd:photo.setCaptureTime`, `cmd:dialog.captureTime`, `crates/catalog/src/dates.rs` | set (the other selected photos shift by the same amount, or `each`), shift by days/hours/minutes, time-zone shift; one undo step, journaled; Info panel button; no “revert to original capture time”. Impossible calendar dates cannot anchor relative shifts; rejection preserves metadata, selection, Undo and Redo. |
| LR-LIB-SHOWFINDER | Reveal original in file manager | P0 | ✅ | `cmd:app.showInFinder` | ⌘R (see MENU-FILE-SHOWFINDER) |
| LR-LIB-COVER | Album cover | P2 | ✅ | `cmd:album.setCover` | |
| LR-LIB-CULL | Assisted culling | P2 | 🟡 | `cmd:photo.analyze`, `cmd:dialog.cull`, `crates/pipeline/src/cull.rs` | Photo ▸ Assisted Culling…: focus score (0–100), clipping, burst grouping (look-alike shots within 10 s) with the sharpest marked; reject below a focus score, pick each burst's best; classical measures — no eyes-closed / expression detection |