mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
LR-LIB-CAPTURETIME: reject impossible anchor dates before shifting photos
Co-authored-by: Codex <[email protected]>
This commit is contained in:
1 parent
c435d143de
commit
4266b0ad7f
3 files changed
+44
-2
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
Reference in new issue
Block a user