From 43cd408e2cb4812fcc90a6219afc028afe3d9649 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 22 Dec 2023 17:32:45 -0500 Subject: [PATCH 1/4] Start elaborating upon the HistoricalView I've created the DaySummary structure and set up a list view to go into the historical view. One hard-coded date is visible as a placeholder to start filling things into the day summary. --- Cargo.lock | 1 + fitnesstrax/app/Cargo.toml | 1 + fitnesstrax/app/src/app_window.rs | 2 +- fitnesstrax/app/src/components/day.rs | 63 ++++++++++++++ fitnesstrax/app/src/components/mod.rs | 3 + fitnesstrax/app/src/views/historical_view.rs | 87 +++++++++++++++++++- 6 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 fitnesstrax/app/src/components/day.rs diff --git a/Cargo.lock b/Cargo.lock index fe046b8..acdedd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1021,6 +1021,7 @@ name = "fitnesstrax" version = "0.1.0" dependencies = [ "async-channel", + "chrono", "emseries", "ft-core", "gio", diff --git a/fitnesstrax/app/Cargo.toml b/fitnesstrax/app/Cargo.toml index b450f5d..3f36189 100644 --- a/fitnesstrax/app/Cargo.toml +++ b/fitnesstrax/app/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] } async-channel = { version = "2.1" } +chrono = { version = "0.4" } emseries = { path = "../../emseries" } ft-core = { path = "../core" } gio = { version = "0.18" } diff --git a/fitnesstrax/app/src/app_window.rs b/fitnesstrax/app/src/app_window.rs index c6d834b..a7651ae 100644 --- a/fitnesstrax/app/src/app_window.rs +++ b/fitnesstrax/app/src/app_window.rs @@ -144,7 +144,7 @@ impl AppWindow { }) .upcast(), ), - ViewName::Historical => View::Historical(HistoricalView::new().upcast()), + ViewName::Historical => View::Historical(HistoricalView::new(vec![]).upcast()), } } } diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs new file mode 100644 index 0000000..3b8bb75 --- /dev/null +++ b/fitnesstrax/app/src/components/day.rs @@ -0,0 +1,63 @@ +/* +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 chrono::NaiveDate; +// use ft_core::TraxRecord; +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; + +#[derive(Default)] +pub struct DaySummaryPrivate { + date: gtk::Label, +} + +#[glib::object_subclass] +impl ObjectSubclass for DaySummaryPrivate { + const NAME: &'static str = "DaySummary"; + type Type = DaySummary; + type ParentType = gtk::Box; + + fn new() -> Self { + Self { + date: gtk::Label::new(None), + } + } +} + +impl ObjectImpl for DaySummaryPrivate {} +impl WidgetImpl for DaySummaryPrivate {} +impl BoxImpl for DaySummaryPrivate {} + +glib::wrapper! { + pub struct DaySummary(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; +} + +impl DaySummary { + pub fn new() -> Self { + let s: Self = Object::builder().build(); + s.set_orientation(gtk::Orientation::Vertical); + + s.append(&s.imp().date); + + s + } + + pub fn set_date(&self, date: chrono::NaiveDate) { + self.imp() + .date + .set_text(&date.format("%y-%m-%d").to_string()); + } +} diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index 7fcba76..858f6c2 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -14,6 +14,9 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ +mod day; + +pub use day::DaySummary; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::{cell::RefCell, path::PathBuf, rc::Rc}; diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index b69e960..89cd6a3 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -14,8 +14,11 @@ 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 ft_core::TraxRecord; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; +use std::cell::RefCell; /// 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 @@ -38,17 +41,97 @@ impl WidgetImpl for HistoricalViewPrivate {} impl BoxImpl for HistoricalViewPrivate {} glib::wrapper! { - pub struct HistoricalView(ObjectSubclass) @extends gtk::Box, gtk::Widget; + pub struct HistoricalView(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; } impl HistoricalView { - pub fn new() -> Self { + pub fn new(_records: Vec) -> Self { let s: Self = Object::builder().build(); + s.set_orientation(gtk::Orientation::Vertical); let label = gtk::Label::builder() .label("Database has been configured and now it is time to show data") .build(); s.append(&label); + + let day_records: Vec = vec![DayRecords::new( + chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(), + vec![], + )]; + + let model = gio::ListStore::new::(); + model.extend_from_slice(&day_records); + + 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_date(records.date()); + }); + + let lst = gtk::ListView::builder() + .model(>k::NoSelection::new(Some(model))) + .factory(&factory) + .build(); + + 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() + } +} -- 2.44.1 From 3dc8be0d260f3ceafb8aa7ea3261a7ddac21254d Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 22 Dec 2023 18:53:29 -0500 Subject: [PATCH 2/4] Render a weight record --- Cargo.lock | 1 + fitnesstrax/app/Cargo.toml | 1 + fitnesstrax/app/src/components/day.rs | 18 ++++++++++++++++++ fitnesstrax/app/src/views/historical_view.rs | 9 +++++++-- fitnesstrax/core/src/lib.rs | 2 +- fitnesstrax/core/src/types.rs | 13 +++++++++++-- 6 files changed, 39 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index acdedd3..cef9966 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1022,6 +1022,7 @@ version = "0.1.0" dependencies = [ "async-channel", "chrono", + "dimensioned 0.8.0", "emseries", "ft-core", "gio", diff --git a/fitnesstrax/app/Cargo.toml b/fitnesstrax/app/Cargo.toml index 3f36189..b35999e 100644 --- a/fitnesstrax/app/Cargo.toml +++ b/fitnesstrax/app/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] } async-channel = { version = "2.1" } chrono = { version = "0.4" } +dimensioned = { version = "0.8", features = [ "serde" ] } emseries = { path = "../../emseries" } ft-core = { path = "../core" } gio = { version = "0.18" } diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 3b8bb75..db87773 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -16,12 +16,15 @@ You should have received a copy of the GNU General Public License along with Fit // use chrono::NaiveDate; // use ft_core::TraxRecord; +use ft_core::TraxRecord; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; +use std::cell::RefCell; #[derive(Default)] pub struct DaySummaryPrivate { date: gtk::Label, + weight: RefCell>, } #[glib::object_subclass] @@ -33,6 +36,7 @@ impl ObjectSubclass for DaySummaryPrivate { fn new() -> Self { Self { date: gtk::Label::new(None), + weight: RefCell::new(None), } } } @@ -60,4 +64,18 @@ impl DaySummary { .date .set_text(&date.format("%y-%m-%d").to_string()); } + + pub fn set_records(&self, records: Vec) { + if let Some(ref weight_label) = *self.imp().weight.borrow() { + self.remove(weight_label); + } + + if let Some(TraxRecord::Weight(weight_record)) = + records.iter().filter(|f| f.is_weight()).next() + { + let label = gtk::Label::new(Some(&format!("{}", weight_record.weight))); + self.append(&label); + *self.imp().weight.borrow_mut() = Some(label); + } + } } diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index 89cd6a3..3ad535e 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -15,7 +15,8 @@ You should have received a copy of the GNU General Public License along with Fit */ use crate::components::DaySummary; -use ft_core::TraxRecord; +use dimensioned::si::KG; +use ft_core::{TraxRecord, Weight}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::cell::RefCell; @@ -56,7 +57,10 @@ impl HistoricalView { let day_records: Vec = vec![DayRecords::new( chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(), - vec![], + vec![TraxRecord::Weight(Weight { + date: chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(), + weight: 100. * KG, + })], )]; let model = gio::ListStore::new::(); @@ -86,6 +90,7 @@ impl HistoricalView { .expect("should be a DaySummary"); summary.set_date(records.date()); + summary.set_records(records.records()); }); let lst = gtk::ListView::builder() diff --git a/fitnesstrax/core/src/lib.rs b/fitnesstrax/core/src/lib.rs index 9d7103a..b0c9d9d 100644 --- a/fitnesstrax/core/src/lib.rs +++ b/fitnesstrax/core/src/lib.rs @@ -4,4 +4,4 @@ use emseries::DateTimeTz; mod legacy; mod types; -pub use types::TraxRecord; +pub use types::{TraxRecord, Weight}; diff --git a/fitnesstrax/core/src/types.rs b/fitnesstrax/core/src/types.rs index 4c332d1..ca526d4 100644 --- a/fitnesstrax/core/src/types.rs +++ b/fitnesstrax/core/src/types.rs @@ -48,8 +48,8 @@ pub struct TimeDistance { /// need to track more than a single weight in a day. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Weight { - date: NaiveDate, - weight: si::Kilogram, + pub date: NaiveDate, + pub weight: si::Kilogram, } /// The unified data structure for all records that are part of the app. @@ -64,6 +64,15 @@ pub enum TraxRecord { Weight(Weight), } +impl TraxRecord { + pub fn is_weight(&self) -> bool { + match self { + TraxRecord::Weight(_) => true, + _ => false, + } + } +} + impl Recordable for TraxRecord { fn timestamp(&self) -> Timestamp { match self { -- 2.44.1 From 1b3ca7439dc2d35e59e9a785413196eeca93fd42 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 24 Dec 2023 12:00:12 -0500 Subject: [PATCH 3/4] Add styling to the day summary --- fitnesstrax/app/resources/style.css | 12 ++++++++++++ fitnesstrax/app/src/app_window.rs | 2 +- fitnesstrax/app/src/components/day.rs | 19 ++++++++++++++----- fitnesstrax/app/src/views/historical_view.rs | 17 ++++++++++------- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/fitnesstrax/app/resources/style.css b/fitnesstrax/app/resources/style.css index 314fd86..1f3aea4 100644 --- a/fitnesstrax/app/resources/style.css +++ b/fitnesstrax/app/resources/style.css @@ -19,3 +19,15 @@ padding: 8px; } +.daysummary { + padding: 8px; +} + +.daysummary-date { + font-size: larger; + margin-bottom: 8px; +} + +.daysummary-weight { + margin: 4px; +} diff --git a/fitnesstrax/app/src/app_window.rs b/fitnesstrax/app/src/app_window.rs index a7651ae..4268277 100644 --- a/fitnesstrax/app/src/app_window.rs +++ b/fitnesstrax/app/src/app_window.rs @@ -41,7 +41,7 @@ impl AppWindow { /// adw_app is an Adwaita application. Application windows need to have access to this, but /// otherwise we don't use this. /// - /// app is a core [App] object which encapsulates all of the basic logic. + /// app is a core [crate::app::App] object which encapsulates all of the basic logic. pub fn new( app_id: &str, resource_path: &str, diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index db87773..0899262 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -34,8 +34,12 @@ impl ObjectSubclass for DaySummaryPrivate { type ParentType = gtk::Box; fn new() -> Self { + let date = gtk::Label::builder() + .css_classes(["daysummary-date"]) + .halign(gtk::Align::Start) + .build(); Self { - date: gtk::Label::new(None), + date, weight: RefCell::new(None), } } @@ -46,6 +50,8 @@ impl WidgetImpl for DaySummaryPrivate {} impl BoxImpl for DaySummaryPrivate {} glib::wrapper! { + /// The DaySummary displays one day's activities in a narrative style. This is meant to give + /// an overall feel of everything that happened during the day without going into details. pub struct DaySummary(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; } @@ -53,19 +59,18 @@ impl DaySummary { pub fn new() -> Self { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); + s.set_css_classes(&["daysummary"]); s.append(&s.imp().date); s } - pub fn set_date(&self, date: chrono::NaiveDate) { + pub fn set_data(&self, date: chrono::NaiveDate, records: Vec) { self.imp() .date .set_text(&date.format("%y-%m-%d").to_string()); - } - pub fn set_records(&self, records: Vec) { if let Some(ref weight_label) = *self.imp().weight.borrow() { self.remove(weight_label); } @@ -73,7 +78,11 @@ impl DaySummary { if let Some(TraxRecord::Weight(weight_record)) = records.iter().filter(|f| f.is_weight()).next() { - let label = gtk::Label::new(Some(&format!("{}", weight_record.weight))); + let label = gtk::Label::builder() + .halign(gtk::Align::Start) + .label(&format!("{}", weight_record.weight)) + .css_classes(["daysummary-weight"]) + .build(); self.append(&label); *self.imp().weight.borrow_mut() = Some(label); } diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index 3ad535e..07ace8d 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -50,11 +50,6 @@ impl HistoricalView { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); - let label = gtk::Label::builder() - .label("Database has been configured and now it is time to show data") - .build(); - s.append(&label); - let day_records: Vec = vec![DayRecords::new( chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(), vec![TraxRecord::Weight(Weight { @@ -89,14 +84,22 @@ impl HistoricalView { .and_downcast::() .expect("should be a DaySummary"); - summary.set_date(records.date()); - summary.set_records(records.records()); + 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(|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(); + println!("list item activated: [{:?}] {:?}", idx, records.date()); + }); s.append(&lst); -- 2.44.1 From af8f9b024434abff24bb9ca58712e94738edd50f Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 24 Dec 2023 19:13:49 -0500 Subject: [PATCH 4/4] Generate some random data and feed it into hte historical view --- Cargo.lock | 1 + fitnesstrax/app/Cargo.toml | 1 + fitnesstrax/app/src/app_window.rs | 39 ++++++++- fitnesstrax/app/src/components/day.rs | 2 +- fitnesstrax/app/src/views/historical_view.rs | 89 +++++++++++++++++--- fitnesstrax/core/src/lib.rs | 2 +- fitnesstrax/core/src/types.rs | 12 +-- 7 files changed, 125 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cef9966..c2c4b54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1022,6 +1022,7 @@ version = "0.1.0" dependencies = [ "async-channel", "chrono", + "chrono-tz", "dimensioned 0.8.0", "emseries", "ft-core", diff --git a/fitnesstrax/app/Cargo.toml b/fitnesstrax/app/Cargo.toml index b35999e..bfb1eca 100644 --- a/fitnesstrax/app/Cargo.toml +++ b/fitnesstrax/app/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] } async-channel = { version = "2.1" } chrono = { version = "0.4" } +chrono-tz = { version = "0.8" } dimensioned = { version = "0.8", features = [ "serde" ] } emseries = { path = "../../emseries" } ft-core = { path = "../core" } diff --git a/fitnesstrax/app/src/app_window.rs b/fitnesstrax/app/src/app_window.rs index 4268277..0326a22 100644 --- a/fitnesstrax/app/src/app_window.rs +++ b/fitnesstrax/app/src/app_window.rs @@ -20,6 +20,11 @@ use crate::{ }; use adw::prelude::*; use async_channel::Sender; +use chrono::{NaiveDate, TimeZone}; +use chrono_tz::America::Anchorage; +use dimensioned::si::{KG, M, S}; +use emseries::DateTimeTz; +use ft_core::{Steps, TimeDistance, TraxRecord, Weight}; use gio::resources_lookup_data; use gtk::STYLE_PROVIDER_PRIORITY_USER; use std::path::PathBuf; @@ -144,7 +149,39 @@ impl AppWindow { }) .upcast(), ), - ViewName::Historical => View::Historical(HistoricalView::new(vec![]).upcast()), + ViewName::Historical => View::Historical( + HistoricalView::new(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: DateTimeTz( + Anchorage.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: DateTimeTz( + Anchorage.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()), + }), + ]) + .upcast(), + ), } } } diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 0899262..1026a20 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -69,7 +69,7 @@ impl DaySummary { pub fn set_data(&self, date: chrono::NaiveDate, records: Vec) { self.imp() .date - .set_text(&date.format("%y-%m-%d").to_string()); + .set_text(&date.format("%Y-%m-%d").to_string()); if let Some(ref weight_label) = *self.imp().weight.borrow() { self.remove(weight_label); diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index 07ace8d..c3d1cf0 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -15,11 +15,11 @@ You should have received a copy of the GNU General Public License along with Fit */ use crate::components::DaySummary; -use dimensioned::si::KG; -use ft_core::{TraxRecord, Weight}; +use emseries::{Recordable, Timestamp}; +use ft_core::TraxRecord; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use std::cell::RefCell; +use std::{cell::RefCell, collections::HashMap}; /// 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 @@ -46,20 +46,14 @@ glib::wrapper! { } impl HistoricalView { - pub fn new(_records: Vec) -> Self { + pub fn new(records: Vec) -> Self { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); - let day_records: Vec = vec![DayRecords::new( - chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(), - vec![TraxRecord::Weight(Weight { - date: chrono::NaiveDate::from_ymd_opt(2023, 10, 13).unwrap(), - weight: 100. * KG, - })], - )]; + let day_records: GroupedRecords = GroupedRecords::from(records); let model = gio::ListStore::new::(); - model.extend_from_slice(&day_records); + model.extend_from_slice(&day_records.0); let factory = gtk::SignalListItemFactory::new(); factory.connect_setup(move |_, list_item| { @@ -142,4 +136,75 @@ impl DayRecords { 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.0.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::{NaiveDate, TimeZone}; + use chrono_tz::America::Anchorage; + use dimensioned::si::{KG, M, S}; + use emseries::DateTimeTz; + 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: DateTimeTz(Anchorage.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: DateTimeTz(Anchorage.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); + } } diff --git a/fitnesstrax/core/src/lib.rs b/fitnesstrax/core/src/lib.rs index b0c9d9d..d454169 100644 --- a/fitnesstrax/core/src/lib.rs +++ b/fitnesstrax/core/src/lib.rs @@ -4,4 +4,4 @@ use emseries::DateTimeTz; mod legacy; mod types; -pub use types::{TraxRecord, Weight}; +pub use types::{Steps, TimeDistance, TraxRecord, Weight}; diff --git a/fitnesstrax/core/src/types.rs b/fitnesstrax/core/src/types.rs index ca526d4..4de0da4 100644 --- a/fitnesstrax/core/src/types.rs +++ b/fitnesstrax/core/src/types.rs @@ -18,8 +18,8 @@ pub struct SetRep { /// The number of steps one takes in a single day. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Steps { - date: NaiveDate, - count: u32, + pub date: NaiveDate, + pub count: u32, } /// TimeDistance represents workouts characterized by a duration and a distance travelled. These @@ -34,14 +34,14 @@ pub struct TimeDistance { /// if one moved two timezones to the east. This is kind of nonsensical from a human /// perspective, so the DateTimeTz keeps track of the precise time in UTC, but also the /// timezone in which the event was recorded. - datetime: DateTimeTz, + pub datetime: DateTimeTz, /// The distance travelled. This is optional because such a workout makes sense even without /// the distance. - distance: Option>, + pub distance: Option>, /// The duration of the workout, which is also optional. Some people may keep track of the /// amount of distance travelled without tracking the duration. - duration: Option>, - comments: Option, + pub duration: Option>, + pub comments: Option, } /// A singular daily weight measurement. Weight changes slowly enough that it seems unlikely to -- 2.44.1