/* 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 emseries::{Record, Recordable, Timestamp}; 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 {} #[glib::object_subclass] impl ObjectSubclass for HistoricalViewPrivate { const NAME: &'static str = "HistoricalView"; type Type = HistoricalView; type ParentType = gtk::Box; fn new() -> Self { Self {} } } 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 day_records: GroupedRecords = GroupedRecords::from( records .into_iter() .map(|r| r.data) .collect::>(), ); let model = gio::ListStore::new::(); model.extend_from_slice(&day_records.0); 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: TraxRecord) { self.imp().records.borrow_mut().push(record); } } struct GroupedRecords(Vec); impl From> for GroupedRecords { fn from(records: Vec) -> GroupedRecords { GroupedRecords( records .into_iter() .fold(HashMap::new(), |mut acc, rec| { let date = match rec.timestamp() { Timestamp::DateTime(dtz) => dtz.date_naive(), Timestamp::Date(date) => date, }; acc.entry(date) .and_modify(|entry: &mut DayRecords| (*entry).add_record(rec.clone())) .or_insert(DayRecords::new(date, vec![rec])); acc }) .values() .cloned() .collect::>(), ) } } #[cfg(test)] mod test { use super::GroupedRecords; use chrono::{FixedOffset, NaiveDate, TimeZone}; use chrono_tz::America::Anchorage; use dimensioned::si::{KG, M, S}; use emseries::{Record, RecordId}; use ft_core::{Steps, TimeDistance, TraxRecord, Weight}; #[test] fn groups_records() { let records = vec![ TraxRecord::Steps(Steps { date: NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(), count: 1500, }), TraxRecord::Weight(Weight { date: NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(), weight: 85. * KG, }), TraxRecord::Weight(Weight { date: NaiveDate::from_ymd_opt(2023, 10, 14).unwrap(), weight: 86. * KG, }), 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()), }), 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); } }