Skip to main content

smriti/models/
timeline_group.rs

1//! Timeline date-grouping (pure logic, no UI deps).
2//!
3//! Lives under `models` rather than `views` so it can be exercised
4//! from integration tests that don't depend on iced / the view layer.
5
6use chrono::{DateTime, Datelike, Utc};
7
8use super::Photo;
9
10/// Zero-copy view of a contiguous slice of `photos: &[Photo]` taken
11/// on the same date. Built once per photos-load (not every render),
12/// stored in app state, and consumed by the timeline view to avoid
13/// re-grouping the entire library on every scroll event.
14#[derive(Debug, Clone)]
15pub struct DateGroupRange {
16    pub day_key: String,
17    pub display_date: String,
18    /// Half-open `[start, end)` range into the parent `photos` slice.
19    pub start: usize,
20    pub end: usize,
21}
22
23/// Pre-compute timeline date groups from a date-descending photo
24/// slice. Called by the `photos_loaded` handler once per load,
25/// NOT per render frame.
26///
27/// Photos must already be sorted by `date_taken DESC` (the loader
28/// guarantees this via SQL `ORDER BY date_taken DESC`). Photos
29/// without a `date_taken` cluster under a single "Unknown Date"
30/// group at the end.
31pub fn compute_groups(photos: &[Photo]) -> Vec<DateGroupRange> {
32    let mut out: Vec<DateGroupRange> = Vec::new();
33    let mut i = 0;
34    while i < photos.len() {
35        let head_date = photos[i].date_taken.map(|d| d.date_naive());
36
37        // Extend the group while subsequent photos share the same
38        // calendar date. Compare `NaiveDate` values directly — much
39        // cheaper than formatting both sides to strings, which would
40        // run `format!("%Y-%m-%d")` once per photo (~50K times for a
41        // 50K-photo library).
42        let mut j = i + 1;
43        while j < photos.len() {
44            let next_date = photos[j].date_taken.map(|d| d.date_naive());
45            if next_date != head_date {
46                break;
47            }
48            j += 1;
49        }
50
51        // Format only once per group.
52        let (day_key, display_date) = match photos[i].date_taken {
53            Some(d) => (d.format("%Y-%m-%d").to_string(), format_display_date(&d)),
54            None => ("0000-00-00".to_string(), "Unknown Date".to_string()),
55        };
56
57        out.push(DateGroupRange {
58            day_key,
59            display_date,
60            start: i,
61            end: j,
62        });
63        i = j;
64    }
65    out
66}
67
68/// Human-friendly display date relative to today.
69pub fn format_display_date(date: &DateTime<Utc>) -> String {
70    let now = Utc::now();
71    let today = now.date_naive();
72    let photo_date = date.date_naive();
73
74    if photo_date == today {
75        "Today".to_string()
76    } else if photo_date == today.pred_opt().unwrap_or(today) {
77        "Yesterday".to_string()
78    } else if photo_date.year() == today.year() {
79        date.format("%B %d").to_string() // "March 15"
80    } else {
81        date.format("%B %d, %Y").to_string() // "March 15, 2019"
82    }
83}