mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-02 22:49:48 -06:00
Merge PR #1490
# Conflicts: # docs/parity-checklist.md # docs/ui-parity.md
This commit is contained in:
@@ -196,6 +196,25 @@ fn change_rating(s: &mut Session, p: &Value, delta: i8, label: &str) -> Result<V
|
||||
Ok(json!({"changed": n}))
|
||||
}
|
||||
|
||||
/// Move each target's own flag by `step`: cycling wraps unflagged -> pick -> reject -> unflagged,
|
||||
/// raising/lowering clamps on reject < unflagged < pick. Unchanged photos make no history step.
|
||||
fn change_flag(s: &mut Session, p: &Value, label: &str, step: impl Fn(Flag) -> Flag) -> Result<Value> {
|
||||
let mut ops = Vec::new();
|
||||
for id in s.targets(p) {
|
||||
let old = s.catalog.photo(id).ok_or(lightcraft_catalog::CatalogError::NoPhoto(id))?.flag;
|
||||
let flag = step(old);
|
||||
if flag != old {
|
||||
ops.push(Op::SetFlag { id, flag });
|
||||
}
|
||||
}
|
||||
let n = ops.len();
|
||||
if n > 0 {
|
||||
s.commit(label, Op::Batch { ops })?;
|
||||
}
|
||||
advance_if(s, p);
|
||||
Ok(json!({"changed": n}))
|
||||
}
|
||||
|
||||
fn advance_if(s: &mut Session, p: &Value) {
|
||||
if bool_or(p, "advance", false) {
|
||||
let _ = step(s, 1);
|
||||
@@ -691,6 +710,40 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
advance_if(s, p);
|
||||
Ok(v)
|
||||
}),
|
||||
// Classic: ` cycles the flag; Cmd+Up / Cmd+Down raise / lower it (Ctrl on Windows and Linux)
|
||||
cmd!(
|
||||
"photo.cycleFlag",
|
||||
"Cycle Flag",
|
||||
["Photo", "Set Flag"],
|
||||
Some("`"),
|
||||
"{ids?, advance?: bool} — each photo's own flag: unflagged -> pick -> reject -> unflagged",
|
||||
has_selection,
|
||||
|s, p| {
|
||||
change_flag(s, p, "Cycle Flag", |f| match f {
|
||||
Flag::None => Flag::Pick,
|
||||
Flag::Pick => Flag::Reject,
|
||||
Flag::Reject => Flag::None,
|
||||
})
|
||||
}
|
||||
),
|
||||
cmd!(
|
||||
"photo.increaseFlag",
|
||||
"Increase Flag Status",
|
||||
["Photo", "Set Flag"],
|
||||
Some("Cmd+Up"),
|
||||
"{ids?, advance?: bool} — reject -> unflagged -> pick (stops at pick)",
|
||||
has_selection,
|
||||
|s, p| { change_flag(s, p, "Increase Flag Status", |f| if f == Flag::Reject { Flag::None } else { Flag::Pick }) }
|
||||
),
|
||||
cmd!(
|
||||
"photo.decreaseFlag",
|
||||
"Decrease Flag Status",
|
||||
["Photo", "Set Flag"],
|
||||
Some("Cmd+Down"),
|
||||
"{ids?, advance?: bool} — pick -> unflagged -> reject (stops at reject)",
|
||||
has_selection,
|
||||
|s, p| { change_flag(s, p, "Decrease Flag Status", |f| if f == Flag::Pick { Flag::None } else { Flag::Reject }) }
|
||||
),
|
||||
cmd!("photo.pick", "Flag as Pick", ["Photo", "Set Flag"], Some("P"), "{ids?}", has_selection, |s, p| {
|
||||
for_targets(s, p, "Flag as Pick", |id| Some(Op::SetFlag { id, flag: Flag::Pick }))
|
||||
}),
|
||||
@@ -1648,3 +1701,7 @@ mod gps_tests {
|
||||
#[cfg(test)]
|
||||
#[path = "library/tests_relative_ratings.rs"]
|
||||
mod relative_rating_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "library/tests_flag_cycle.rs"]
|
||||
mod flag_cycle_tests;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
//! `photo.cycleFlag` / `photo.increaseFlag` / `photo.decreaseFlag` through the real engine.
|
||||
use lightcraft_catalog::{Flag, Op, Photo, PhotoId, Source};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Result, Selection, Session};
|
||||
|
||||
fn session() -> Result<Session> {
|
||||
let mut s = Session::new();
|
||||
for (id, flag) in [(1, Flag::None), (2, Flag::Pick), (3, Flag::Reject), (4, Flag::None)] {
|
||||
let mut photo = Photo::new(
|
||||
PhotoId(id),
|
||||
Source::File { path: format!("/lightcraft-flag-cycle/{id}.jpg") },
|
||||
&format!("{id}.jpg"),
|
||||
"JPEG",
|
||||
40,
|
||||
30,
|
||||
"2026-10-13",
|
||||
);
|
||||
photo.flag = flag;
|
||||
s.catalog.apply(Op::AddPhoto { photo: Box::new(photo) })?;
|
||||
}
|
||||
s.execute("library.sort", &json!({"key": "fileName", "ascending": true}))?;
|
||||
s.execute("library.select", &json!({"ids": [1, 2, 3], "active": 2}))?;
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn flags(s: &Session) -> Vec<Flag> {
|
||||
(1..=4).map(|id| s.catalog.photo(PhotoId(id)).unwrap().flag).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_steps_unflagged_pick_reject_and_wraps() {
|
||||
let mut s = session().unwrap();
|
||||
s.execute("library.select", &json!({"ids": [1]})).unwrap();
|
||||
let seen: Vec<Flag> = (0..4)
|
||||
.map(|_| {
|
||||
s.execute("photo.cycleFlag", &json!({})).unwrap();
|
||||
flags(&s)[0]
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(seen, [Flag::Pick, Flag::Reject, Flag::None, Flag::Pick]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_uses_each_photos_own_flag_in_one_undo_step() {
|
||||
let mut s = session().unwrap();
|
||||
let undo = s.undo.len();
|
||||
assert_eq!(s.execute("photo.cycleFlag", &json!({})).unwrap()["changed"], 3);
|
||||
assert_eq!(flags(&s), [Flag::Pick, Flag::Reject, Flag::None, Flag::None]);
|
||||
assert_eq!(s.undo.len(), undo + 1);
|
||||
assert_eq!(s.active(), Some(PhotoId(2)));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(flags(&s), [Flag::None, Flag::Pick, Flag::Reject, Flag::None]);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(flags(&s), [Flag::Pick, Flag::Reject, Flag::None, Flag::None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_ids_active_fallback_advance_and_errors() {
|
||||
let mut s = session().unwrap();
|
||||
s.execute("photo.cycleFlag", &json!({"ids": [4]})).unwrap();
|
||||
assert_eq!(flags(&s), [Flag::None, Flag::Pick, Flag::Reject, Flag::Pick]);
|
||||
s.selection = Selection { ids: vec![], active: Some(PhotoId(2)) };
|
||||
s.execute("photo.cycleFlag", &json!({})).unwrap();
|
||||
assert_eq!(flags(&s)[1], Flag::Reject);
|
||||
s.execute("photo.cycleFlag", &json!({"advance": true})).unwrap();
|
||||
assert_eq!(flags(&s)[1], Flag::None);
|
||||
assert_eq!(s.active(), Some(PhotoId(3)));
|
||||
let undo = s.undo.len();
|
||||
assert!(s.execute("photo.cycleFlag", &json!({"ids": [1, 999]})).is_err());
|
||||
assert_eq!(flags(&s)[0], Flag::None);
|
||||
assert_eq!(s.undo.len(), undo);
|
||||
s.selection = Selection::default();
|
||||
assert!(s.execute("photo.cycleFlag", &json!({})).is_err());
|
||||
assert_eq!(s.undo.len(), undo);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn increase_and_decrease_clamp_without_wrapping() {
|
||||
let mut s = session().unwrap();
|
||||
assert_eq!(s.execute("photo.increaseFlag", &json!({})).unwrap()["changed"], 2);
|
||||
assert_eq!(flags(&s), [Flag::Pick, Flag::Pick, Flag::None, Flag::None]);
|
||||
assert_eq!(s.execute("photo.increaseFlag", &json!({"ids": [2]})).unwrap()["changed"], 0);
|
||||
s.execute("photo.decreaseFlag", &json!({})).unwrap();
|
||||
assert_eq!(flags(&s), [Flag::None, Flag::None, Flag::Reject, Flag::None]);
|
||||
s.execute("photo.decreaseFlag", &json!({})).unwrap();
|
||||
assert_eq!(flags(&s), [Flag::Reject, Flag::Reject, Flag::Reject, Flag::None]);
|
||||
let undo = s.undo.len();
|
||||
assert_eq!(s.execute("photo.decreaseFlag", &json!({})).unwrap()["changed"], 0);
|
||||
assert_eq!(s.undo.len(), undo, "no change, no history step");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registered_with_classic_shortcuts() {
|
||||
let specs = crate::command_specs();
|
||||
let key = |id: &str| specs.iter().find(|c| c.id == id).and_then(|c| c.shortcut);
|
||||
assert_eq!(key("photo.cycleFlag"), Some("`"));
|
||||
assert_eq!(key("photo.increaseFlag"), Some("Cmd+Up"));
|
||||
assert_eq!(key("photo.decreaseFlag"), Some("Cmd+Down"));
|
||||
}
|
||||
Reference in New Issue
Block a user