/* Copyright 2023, Savanni D'Gerinel This file is part of FitnessTrax. FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ use crate::components::DaySummary; use chrono::{Duration, Local, NaiveDate}; use emseries::Record; use ft_core::TraxRecord; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::{cell::RefCell, collections::HashMap, rc::Rc}; /// The historical view will show a window into the main database. It will show some version of /// daily summaries, daily details, and will provide all functions the user may need for editing /// records. pub struct HistoricalViewPrivate { time_window: RefCell, } #[glib::object_subclass] impl ObjectSubclass for HistoricalViewPrivate { const NAME: &'static str = "HistoricalView"; type Type = HistoricalView; type ParentType = gtk::Box; fn new() -> Self { Self { time_window: RefCell::new(DayInterval::default()), } } } impl ObjectImpl for HistoricalViewPrivate {} impl WidgetImpl for HistoricalViewPrivate {} impl BoxImpl for HistoricalViewPrivate {} glib::wrapper! { pub struct HistoricalView(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; } impl HistoricalView { pub fn new(records: Vec>, on_select_day: Rc) -> Self where SelectFn: Fn(chrono::NaiveDate, Vec>) + 'static, { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); s.set_css_classes(&["historical"]); let grouped_records = GroupedRecords::new((*s.imp().time_window.borrow()).clone()).with_data(records); let mut model = gio::ListStore::new::(); model.extend(grouped_records.items()); let factory = gtk::SignalListItemFactory::new(); factory.connect_setup(move |_, list_item| { list_item .downcast_ref::() .expect("should be a ListItem") .set_child(Some(&DaySummary::new())); }); factory.connect_bind(move |_, list_item| { let records = list_item .downcast_ref::() .expect("should be a ListItem") .item() .and_downcast::() .expect("should be a DaySummary"); let summary = list_item .downcast_ref::() .expect("should be a ListItem") .child() .and_downcast::() .expect("should be a DaySummary"); summary.set_data(records.date(), records.records()); }); let lst = gtk::ListView::builder() .model(>k::NoSelection::new(Some(model))) .factory(&factory) .single_click_activate(true) .build(); lst.connect_activate({ let on_select_day = on_select_day.clone(); move |s, idx| { // This gets triggered whenever the user clicks on an item on the list. What we // actually want to do here is to open a modal dialog that shows all of the details of // the day and which allows the user to edit items within that dialog. let item = s.model().unwrap().item(idx).unwrap(); let records = item.downcast_ref::().unwrap(); on_select_day(records.date(), records.records()); } }); s.append(&lst); s } } #[derive(Default)] pub struct DayRecordsPrivate { date: RefCell, records: RefCell>>, } #[glib::object_subclass] impl ObjectSubclass for DayRecordsPrivate { const NAME: &'static str = "DayRecords"; type Type = DayRecords; } impl ObjectImpl for DayRecordsPrivate {} glib::wrapper! { pub struct DayRecords(ObjectSubclass); } impl DayRecords { pub fn new(date: chrono::NaiveDate, records: Vec>) -> Self { let s: Self = Object::builder().build(); *s.imp().date.borrow_mut() = date; *s.imp().records.borrow_mut() = records; s } pub fn date(&self) -> chrono::NaiveDate { self.imp().date.borrow().clone() } pub fn records(&self) -> Vec> { self.imp().records.borrow().clone() } pub fn add_record(&self, record: Record) { self.imp().records.borrow_mut().push(record); } } struct GroupedRecords { interval: DayInterval, data: HashMap, } impl GroupedRecords { fn new(interval: DayInterval) -> Self { let mut s = Self { interval: interval.clone(), data: HashMap::new(), }; interval.days().for_each(|date| { let _ = s.data.insert(date, DayRecords::new(date, vec![])); }); s } fn with_data(mut self, records: Vec>) -> Self { records.into_iter().for_each(|record| { self.data .entry(record.date()) .and_modify(|entry: &mut DayRecords| (*entry).add_record(record.clone())) .or_insert(DayRecords::new(record.date(), vec![record])); }); self } fn items<'a>(&'a self) -> impl Iterator + 'a { /* GroupedRecordIterator { interval: self.interval.clone(), data: &self.data, } */ self.interval.days().map(|date| { self.data .get(&date) .map(|rec| rec.clone()) .unwrap_or(DayRecords::new(date, vec![])) }) } } /* struct GroupedRecordIterator<'a> { interval: DayInterval, data: &'a HashMap, } impl <'a> Iterator for GroupedRecordIterator<'a> { type Item = &'a DayRecords; fn next(&mut self) -> Option<&'a DayRecords> { } } */ #[derive(Clone, Debug, PartialEq, Eq)] struct DayInterval { start: NaiveDate, end: NaiveDate, } impl Default for DayInterval { fn default() -> Self { Self { start: (Local::now() - Duration::days(7)).date_naive(), end: Local::now().date_naive(), } } } impl DayInterval { fn days(&self) -> impl Iterator { DayIterator { current: self.start.clone(), end: self.end.clone(), } } } struct DayIterator { current: NaiveDate, end: NaiveDate, } impl Iterator for DayIterator { type Item = NaiveDate; fn next(&mut self) -> Option { if self.current <= self.end { let val = self.current.clone(); self.current += Duration::days(1); Some(val) } else { None } } } #[cfg(test)] mod test { use super::GroupedRecords; use chrono::{FixedOffset, NaiveDate, TimeZone}; use dimensioned::si::{KG, M, S}; use emseries::{Record, RecordId}; use ft_core::{Steps, TimeDistance, TraxRecord, Weight}; #[test] fn groups_records() { let records = vec![ Record { id: RecordId::default(), data: TraxRecord::Steps(Steps { date: NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(), count: 1500, }), }, Record { id: RecordId::default(), data: TraxRecord::Weight(Weight { date: NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(), weight: 85. * KG, }), }, Record { id: RecordId::default(), data: TraxRecord::Weight(Weight { date: NaiveDate::from_ymd_opt(2023, 10, 14).unwrap(), weight: 86. * KG, }), }, Record { id: RecordId::default(), data: TraxRecord::BikeRide(TimeDistance { datetime: FixedOffset::west_opt(10 * 60 * 60) .unwrap() .with_ymd_and_hms(2019, 6, 15, 12, 0, 0) .unwrap(), distance: Some(1000. * M), duration: Some(150. * S), comments: Some("Test Comments".to_owned()), }), }, Record { id: RecordId::default(), data: TraxRecord::BikeRide(TimeDistance { datetime: FixedOffset::west_opt(10 * 60 * 60) .unwrap() .with_ymd_and_hms(2019, 6, 15, 23, 0, 0) .unwrap(), distance: Some(1000. * M), duration: Some(150. * S), comments: Some("Test Comments".to_owned()), }), }, ]; let groups = GroupedRecords::from(records).0; assert_eq!(groups.len(), 3); } }