/* 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::{ app::App, components::DaySummary, types::DayInterval, view_models::DayDetailViewModel, }; use chrono::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 { app: Rc>>, time_window: Rc>, list_view: gtk::ListView, } #[glib::object_subclass] impl ObjectSubclass for HistoricalViewPrivate { const NAME: &'static str = "HistoricalView"; type Type = HistoricalView; type ParentType = gtk::Box; fn new() -> Self { 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())); }); let s = Self { app: Rc::new(RefCell::new(None)), time_window: Rc::new(RefCell::new(DayInterval::default())), list_view: gtk::ListView::builder() .factory(&factory) .single_click_activate(true) .build(), }; factory.connect_bind({ let app = s.app.clone(); 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"); if let Some(app) = app.borrow().clone() { summary.set_data(DayDetailViewModel::new( records.date(), records.records(), app.clone(), )); } } }); s } } 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( app: App, 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"]); *s.imp().app.borrow_mut() = Some(app); 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()); s.imp() .list_view .set_model(Some(>k::NoSelection::new(Some(model)))); s.imp().list_view.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. let item = s.model().unwrap().item(idx).unwrap(); let records = item.downcast_ref::().unwrap(); on_select_day(records.date(), records.records()); } }); s.append(&s.imp().list_view); s } pub fn set_records(&self, records: Vec>) { println!("set_records: {:?}", records); let grouped_records = GroupedRecords::new((self.imp().time_window.borrow()).clone()).with_data(records); let mut model = gio::ListStore::new::(); model.extend(grouped_records.items()); self.imp() .list_view .set_model(Some(>k::NoSelection::new(Some(model)))); } pub fn time_window(&self) -> DayInterval { self.imp().time_window.borrow().clone() } } #[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() } pub fn records(&self) -> Vec> { self.imp().records.borrow().clone() } pub fn add_record(&self, record: Record) { self.imp().records.borrow_mut().push(record); } } // This isn't feeling quite right. DayRecords is a glib object, but I'm not sure that I want to // really be passing that around. It seems not generic enough. I feel like this whole grouped // records thing can be made more generic. 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(&self) -> impl Iterator + '_ { self.interval.days().map(|date| { self.data .get(&date) .cloned() .unwrap_or(DayRecords::new(date, vec![])) }) } } #[cfg(test)] mod test { use super::GroupedRecords; use crate::types::DayInterval; 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::new(DayInterval { start: NaiveDate::from_ymd_opt(2023, 10, 14).unwrap(), end: NaiveDate::from_ymd_opt(2023, 10, 14).unwrap(), }) .with_data(records) .data; assert_eq!(groups.len(), 3); } }