mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-29 04:27:30 -06:00
LR-VIEW-PHOTOGRID: group the photo/square grid by date with day/month/year headers
Catalog date runs (stacks never split), Sort.group (auto/day/month/year/none) in the sort menus, library.groups query, sticky headers, header click selects the group; layout unit tests and a headless test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
cec012d032
commit
89b8aa3f77
@@ -1,7 +1,7 @@
|
||||
//! Library commands: view source, filter/sort, selection, ratings/flags/labels, rotate, delete,
|
||||
//! metadata, albums, import.
|
||||
|
||||
use lightcraft_catalog::{Album, AlbumId, ColorLabel, Flag, Op, PhotoId, Sort, SortKey};
|
||||
use lightcraft_catalog::{Album, AlbumId, ColorLabel, Flag, GroupBy, Op, PhotoId, Sort, SortKey};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, always, bad, bool_or, cmd, has_active, has_selection, ok, str_param};
|
||||
@@ -130,17 +130,37 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Sort",
|
||||
["View", "Sort"],
|
||||
None,
|
||||
"{key: captureDate|importDate|editDate|fileName|rating|fileSize, ascending?: bool}",
|
||||
"{key?: captureDate|importDate|editDate|fileName|rating|fileSize, ascending?: bool, group?: auto|none|day|month|year}",
|
||||
always,
|
||||
|s, p| {
|
||||
let key: SortKey = match p.get("key") {
|
||||
Some(k) => serde_json::from_value(k.clone()).map_err(|e| bad("library.sort", e.to_string()))?,
|
||||
None => s.sort.key,
|
||||
};
|
||||
s.sort = Sort { key, ascending: bool_or(p, "ascending", s.sort.ascending) };
|
||||
let group = match str_param(p, "group") {
|
||||
Some(g) => GroupBy::parse(g).ok_or_else(|| bad("library.sort", "group must be auto|none|day|month|year"))?,
|
||||
None => s.sort.group,
|
||||
};
|
||||
s.sort = Sort { key, ascending: bool_or(p, "ascending", s.sort.ascending), group };
|
||||
ok()
|
||||
}
|
||||
),
|
||||
cmd!(
|
||||
query "library.groups",
|
||||
"Date Groups",
|
||||
[],
|
||||
None,
|
||||
"{by?: day|month|year (default: the sort's grouping; auto = day)} → [{key, label, start, count}] date headers of the grid",
|
||||
always,
|
||||
|s, p| {
|
||||
let by = match str_param(p, "by") {
|
||||
Some(g) => GroupBy::parse(g).ok_or_else(|| bad("library.groups", "by must be auto|none|day|month|year"))?,
|
||||
None => s.sort.group,
|
||||
};
|
||||
let vis = s.visible_cloned();
|
||||
Ok(serde_json::to_value(s.catalog.date_runs(&vis, s.sort.key, by)).unwrap_or_default())
|
||||
}
|
||||
),
|
||||
// ---- selection
|
||||
cmd!("library.select", "Select Photos", [], None, "{ids: [id], active?: id, mode?: replace|add|toggle|range}", always, |s, p| {
|
||||
let ids = ids_param(p).unwrap_or_default();
|
||||
|
||||
@@ -274,20 +274,9 @@ pub fn system_clock() -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Unix seconds → `YYYY-MM-DDTHH:MM:SS` (UTC), proleptic Gregorian (H. Hinnant's algorithm).
|
||||
/// Unix seconds → `YYYY-MM-DDTHH:MM:SS` (UTC), proleptic Gregorian.
|
||||
pub fn civil(secs: i64) -> String {
|
||||
let days = secs.div_euclid(86_400);
|
||||
let rem = secs.rem_euclid(86_400);
|
||||
let z = days + 719_468;
|
||||
let era = z.div_euclid(146_097);
|
||||
let doe = z - era * 146_097;
|
||||
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let y = yoe + era * 400 + i64::from(m <= 2);
|
||||
format!("{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}", rem / 3600, rem % 3600 / 60, rem % 60)
|
||||
lightcraft_catalog::dates::civil(secs)
|
||||
}
|
||||
|
||||
impl Session {
|
||||
|
||||
@@ -340,6 +340,8 @@ mod tests_color;
|
||||
#[cfg(test)]
|
||||
mod tests_import;
|
||||
#[cfg(test)]
|
||||
mod tests_libops;
|
||||
#[cfg(test)]
|
||||
mod tests_library;
|
||||
#[cfg(test)]
|
||||
mod tests_merge;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Library management commands: date groups, keyword rename/delete/merge, batch rename, capture
|
||||
//! time, label names, import review.
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
/// Date headers follow the grid order and the sort's grouping.
|
||||
#[test]
|
||||
fn date_groups_follow_sort_and_grouping() {
|
||||
let mut s = Session::with_demo();
|
||||
let vis = s.visible_cloned();
|
||||
let g = s.execute("library.groups", &json!({})).unwrap();
|
||||
let groups = g.as_array().unwrap();
|
||||
assert!(groups.len() > 3, "{g}");
|
||||
let total: u64 = groups.iter().map(|x| x["count"].as_u64().unwrap()).sum();
|
||||
assert_eq!(total as usize, vis.len());
|
||||
// contiguous runs in grid order, labelled with weekday and date
|
||||
let mut next = 0;
|
||||
for x in groups {
|
||||
assert_eq!(x["start"].as_u64().unwrap(), next);
|
||||
next += x["count"].as_u64().unwrap();
|
||||
assert!(x["label"].as_str().unwrap().contains(", "), "{x}");
|
||||
}
|
||||
let months = s.execute("library.groups", &json!({"by": "month"})).unwrap();
|
||||
assert!(months.as_array().unwrap().len() < groups.len());
|
||||
assert_eq!(months[0]["key"].as_str().unwrap().len(), 7);
|
||||
s.execute("library.sort", &json!({"group": "none"})).unwrap();
|
||||
assert_eq!(s.execute("library.groups", &json!({})).unwrap(), json!([]));
|
||||
s.execute("library.sort", &json!({"group": "year", "key": "fileName"})).unwrap();
|
||||
assert_eq!(s.execute("library.groups", &json!({})).unwrap(), json!([]), "no date headers when sorting by name");
|
||||
assert_eq!(s.sort.group, lightcraft_catalog::GroupBy::Year);
|
||||
assert!(s.execute("library.sort", &json!({"group": "week"})).is_err());
|
||||
}
|
||||
Reference in New Issue
Block a user