diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs index ac1ad9c..2721002 100644 --- a/fitnesstrax/app/src/components/time_distance.rs +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -1,5 +1,5 @@ /* -Copyright 2023, Savanni D'Gerinel +Copyright 2023-2024, Savanni D'Gerinel This file is part of FitnessTrax. @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with Fit // use crate::components::{EditView, ParseError, TextEntry}; // use chrono::{Local, NaiveDate}; // use dimensioned::si; -use ft_core::{RecordType, TimeDistance}; +use ft_core::TimeDistance; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::cell::RefCell; @@ -44,7 +44,7 @@ glib::wrapper! { } impl TimeDistanceView { - pub fn new(type_: RecordType, record: TimeDistance) -> Self { + pub fn new(record: TimeDistance) -> Self { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); s.set_hexpand(true); @@ -58,12 +58,14 @@ impl TimeDistanceView { .build(), ); + /* first_row.append( >k::Label::builder() .halign(gtk::Align::Start) .label(format!("{:?}", type_)) .build(), ); + */ first_row.append( >k::Label::builder() diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs index 07a70cb..029327a 100644 --- a/fitnesstrax/app/src/view_models/day_detail.rs +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -18,7 +18,7 @@ use crate::app::{ReadError, RecordProvider, WriteError}; use chrono::NaiveDate; use dimensioned::si; use emseries::{Record, RecordId, Recordable}; -use ft_core::{RecordType, TimeDistance, TimeDistanceWorkoutType, TraxRecord}; +use ft_core::{TimeDistance, TimeDistanceActivity, TraxRecord}; use std::{ collections::HashMap, ops::Deref, @@ -46,20 +46,34 @@ impl RecordState { fn exists(&self) -> bool { match self { - RecordState::Original(ref r) => true, - RecordState::New(ref r) => true, - RecordState::Updated(ref r) => true, - RecordState::Deleted(ref r) => false, + RecordState::Original(_) => true, + RecordState::New(_) => true, + RecordState::Updated(_) => true, + RecordState::Deleted(_) => false, } } - fn with_value(self, value: T) -> RecordState { - match self { - RecordState::Original(r) => RecordState::Updated(Record { data: value, ..r }), + fn set_value(&mut self, value: T) { + *self = match self { + RecordState::Original(r) => RecordState::Updated(Record { + id: r.id.clone(), + data: value, + }), RecordState::New(_) => RecordState::New(value), - RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..r }), - RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..r }), - } + RecordState::Updated(r) => RecordState::Updated(Record { + id: r.id.clone(), + data: value, + }), + RecordState::Deleted(r) => RecordState::Updated(Record { + id: r.id.clone(), + data: value, + }), + }; + } + + fn with_value(mut self, value: T) -> RecordState { + self.set_value(value); + self } #[allow(unused)] @@ -195,15 +209,16 @@ impl DayDetailViewModel { *record = Some(new_record); } - pub fn new_time_distance(&self, type_: TimeDistanceWorkoutType) -> Record { + pub fn new_time_distance(&self, activity: TimeDistanceActivity) -> Record { let id = RecordId::default(); let workout = TimeDistance { datetime: chrono::Local::now().into(), + activity, distance: None, duration: None, comments: None, }; - let tr = TraxRecord::from_time_distance(type_, workout.clone()); + let tr = TraxRecord::from(workout.clone()); self.records .write() .unwrap() @@ -220,44 +235,19 @@ impl DayDetailViewModel { let data = workout.data.clone(); let mut record_set = self.records.write().unwrap(); - if let Some(record_state) = record_set.get(&id).clone() { - let updated_state = match **record_state { - TraxRecord::BikeRide(_) => { - Some(record_state.clone().with_value(TraxRecord::BikeRide(data))) - } - TraxRecord::Row(_) => Some(record_state.clone().with_value(TraxRecord::Row(data))), - TraxRecord::Run(_) => Some(record_state.clone().with_value(TraxRecord::Run(data))), - TraxRecord::Swim(_) => { - Some(record_state.clone().with_value(TraxRecord::Swim(data))) - } - TraxRecord::Walk(_) => { - Some(record_state.clone().with_value(TraxRecord::Walk(data))) - } - _ => None, - }; - - if let Some(updated_state) = updated_state { - record_set.insert(id, updated_state); - } - } + record_set.entry(id).and_modify(|record_state| { + record_state.set_value(TraxRecord::TimeDistance(data)); + }); } - pub fn time_distance_records( - &self, - type_: TimeDistanceWorkoutType, - ) -> Vec> { + pub fn time_distance_records(&self) -> Vec> { self.records .read() .unwrap() .iter() .filter(|(_, record)| record.exists()) - .filter(|(_, workout_state)| workout_state.is_time_distance_type(type_)) .filter_map(|(id, record_state)| match **record_state { - TraxRecord::BikeRide(ref workout) - | TraxRecord::Row(ref workout) - | TraxRecord::Run(ref workout) - | TraxRecord::Swim(ref workout) - | TraxRecord::Walk(ref workout) => Some(Record { + TraxRecord::TimeDistance(ref workout) => Some(Record { id: id.clone(), data: workout.clone(), }), @@ -268,17 +258,23 @@ impl DayDetailViewModel { pub fn time_distance_summary( &self, - type_: TimeDistanceWorkoutType, + activity: TimeDistanceActivity, ) -> (si::Meter, si::Second) { - self.time_distance_records(type_).into_iter().fold( - (0. * si::M, 0. * si::S), - |(distance, duration), workout| match (workout.data.distance, workout.data.duration) { - (Some(distance_), Some(duration_)) => (distance + distance_, duration + duration_), - (Some(distance_), None) => (distance + distance_, duration), - (None, Some(duration_)) => (distance, duration + duration_), - (None, None) => (distance, duration), - }, - ) + self.time_distance_records() + .into_iter() + .filter(|rec| rec.data.activity == activity) + .fold( + (0. * si::M, 0. * si::S), + |(distance, duration), workout| match (workout.data.distance, workout.data.duration) + { + (Some(distance_), Some(duration_)) => { + (distance + distance_, duration + duration_) + } + (Some(distance_), None) => (distance + distance_, duration), + (None, Some(duration_)) => (distance, duration + duration_), + (None, None) => (distance, duration), + }, + ) } fn get_record(&self, id: &RecordId) -> Option> { @@ -506,8 +502,9 @@ mod test { }, Record { id: RecordId::default(), - data: TraxRecord::BikeRide(ft_core::TimeDistance { + data: TraxRecord::TimeDistance(ft_core::TimeDistance { datetime: oct_13_am.clone(), + activity: TimeDistanceActivity::BikeRide, distance: Some(15000. * si::M), duration: Some(3600. * si::S), comments: Some("somecomments present".to_owned()), @@ -557,11 +554,11 @@ mod test { async fn it_can_construct_new_records() { let (view_model, provider) = create_empty_view_model().await; assert_eq!( - view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), + view_model.time_distance_summary(TimeDistanceActivity::BikeRide), (0. * si::M, 0. * si::S) ); - let mut record = view_model.new_time_distance(TimeDistanceWorkoutType::BikeRide); + let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide); record.data.duration = Some(60. * si::S); view_model.async_save().await; @@ -574,24 +571,24 @@ mod test { async fn it_can_update_a_new_record_before_saving() { let (view_model, provider) = create_empty_view_model().await; assert_eq!( - view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), + view_model.time_distance_summary(TimeDistanceActivity::BikeRide), (0. * si::M, 0. * si::S) ); - let mut record = view_model.new_time_distance(TimeDistanceWorkoutType::BikeRide); + let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide); record.data.duration = Some(60. * si::S); view_model.update_time_distance(record.clone()); let record = Record { id: record.id, - data: TraxRecord::BikeRide(record.data), + data: TraxRecord::TimeDistance(record.data), }; assert_eq!(view_model.get_record(&record.id), Some(record)); assert_eq!( - view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), + view_model.time_distance_summary(TimeDistanceActivity::BikeRide), (0. * si::M, 60. * si::S) ); assert_eq!( - view_model.time_distance_summary(TimeDistanceWorkoutType::Run), + view_model.time_distance_summary(TimeDistanceActivity::Running), (0. * si::M, 0. * si::S) ); view_model.async_save().await; @@ -604,11 +601,7 @@ mod test { #[tokio::test] async fn it_can_update_an_existing_record() { let (view_model, provider) = create_view_model().await; - let mut workout = view_model - .time_distance_records(TimeDistanceWorkoutType::BikeRide) - .first() - .cloned() - .unwrap(); + let mut workout = view_model.time_distance_records().first().cloned().unwrap(); println!("found record: {:?}", workout); @@ -616,7 +609,7 @@ mod test { view_model.update_time_distance(workout.clone()); assert_eq!( - view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), + view_model.time_distance_summary(TimeDistanceActivity::BikeRide), (15000. * si::M, 1800. * si::S) ); @@ -631,11 +624,11 @@ mod test { async fn it_can_remove_a_new_record() { let (view_model, provider) = create_empty_view_model().await; assert_eq!( - view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), + view_model.time_distance_summary(TimeDistanceActivity::BikeRide), (0. * si::M, 0. * si::S) ); - let record = view_model.new_time_distance(TimeDistanceWorkoutType::BikeRide); + let record = view_model.new_time_distance(TimeDistanceActivity::BikeRide); view_model.remove_record(record.id); view_model.save(); @@ -647,15 +640,11 @@ mod test { #[tokio::test] async fn it_can_delete_an_existing_record() { let (view_model, provider) = create_view_model().await; - let mut workout = view_model - .time_distance_records(TimeDistanceWorkoutType::BikeRide) - .first() - .cloned() - .unwrap(); + let mut workout = view_model.time_distance_records().first().cloned().unwrap(); view_model.remove_record(workout.id); assert_eq!( - view_model.time_distance_summary(TimeDistanceWorkoutType::BikeRide), + view_model.time_distance_summary(TimeDistanceActivity::BikeRide), (0. * si::M, 0. * si::S) ); view_model.async_save().await; diff --git a/fitnesstrax/core/src/lib.rs b/fitnesstrax/core/src/lib.rs index 8e2144a..89895ad 100644 --- a/fitnesstrax/core/src/lib.rs +++ b/fitnesstrax/core/src/lib.rs @@ -1,4 +1,4 @@ mod legacy; mod types; -pub use types::{RecordType, Steps, TimeDistance, TimeDistanceWorkoutType, TraxRecord, Weight}; +pub use types::{Steps, TimeDistance, TimeDistanceActivity, TraxRecord, Weight}; diff --git a/fitnesstrax/core/src/types.rs b/fitnesstrax/core/src/types.rs index a475dba..c50d4eb 100644 --- a/fitnesstrax/core/src/types.rs +++ b/fitnesstrax/core/src/types.rs @@ -33,6 +33,15 @@ impl Recordable for Steps { } } +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +pub enum TimeDistanceActivity { + BikeRide, + Running, + Rowing, + Swimming, + Walking, +} + /// TimeDistance represents workouts characterized by a duration and a distance travelled. These /// sorts of workouts can occur many times a day, depending on how one records things. I might /// record a single 30-km workout if I go on a long-distanec ride. Or I might record multiple 5km @@ -48,6 +57,8 @@ pub struct TimeDistance { /// in the database, but we can still get a Naive Date from the DateTime, which will still read /// as the original day. pub datetime: DateTime, + /// The activity + pub activity: TimeDistanceActivity, /// The distance travelled. This is optional because such a workout makes sense even without /// the distance. pub distance: Option>, @@ -85,61 +96,15 @@ impl Recordable for Weight { } } -#[derive(Clone, Debug, PartialEq)] -pub enum RecordType { - BikeRide, - Row, - Run, - Steps, - Swim, - Walk, - Weight, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum TimeDistanceWorkoutType { - BikeRide, - Row, - Run, - Swim, - Walk, -} - /// The unified data structure for all records that are part of the app. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum TraxRecord { - BikeRide(TimeDistance), - Row(TimeDistance), - Run(TimeDistance), + TimeDistance(TimeDistance), Steps(Steps), - Swim(TimeDistance), - Walk(TimeDistance), Weight(Weight), } impl TraxRecord { - pub fn from_time_distance(type_: TimeDistanceWorkoutType, workout: TimeDistance) -> Self { - match type_ { - TimeDistanceWorkoutType::BikeRide => Self::BikeRide(workout), - TimeDistanceWorkoutType::Run => Self::Run(workout), - TimeDistanceWorkoutType::Row => Self::Row(workout), - TimeDistanceWorkoutType::Swim => Self::Swim(workout), - TimeDistanceWorkoutType::Walk => Self::Walk(workout), - } - } - - pub fn workout_type(&self) -> RecordType { - match self { - TraxRecord::BikeRide(_) => RecordType::BikeRide, - TraxRecord::Row(_) => RecordType::Row, - TraxRecord::Run(_) => RecordType::Run, - TraxRecord::Steps(_) => RecordType::Steps, - TraxRecord::Swim(_) => RecordType::Swim, - TraxRecord::Walk(_) => RecordType::Walk, - TraxRecord::Weight(_) => RecordType::Weight, - } - } - pub fn is_weight(&self) -> bool { matches!(self, TraxRecord::Weight(_)) } @@ -151,15 +116,27 @@ impl TraxRecord { pub fn is_time_distance(&self) -> bool { matches!( self, - TraxRecord::BikeRide(_) - | TraxRecord::Row(_) - | TraxRecord::Run(_) - | TraxRecord::Swim(_) - | TraxRecord::Walk(_) + TraxRecord::TimeDistance(TimeDistance { + activity: TimeDistanceActivity::BikeRide, + .. + }) | TraxRecord::TimeDistance(TimeDistance { + activity: TimeDistanceActivity::Running, + .. + }) | TraxRecord::TimeDistance(TimeDistance { + activity: TimeDistanceActivity::Rowing, + .. + }) | TraxRecord::TimeDistance(TimeDistance { + activity: TimeDistanceActivity::Swimming, + .. + }) | TraxRecord::TimeDistance(TimeDistance { + activity: TimeDistanceActivity::Walking, + .. + }) ) } - pub fn is_time_distance_type(&self, type_: TimeDistanceWorkoutType) -> bool { + /* + pub fn is_time_distance_type(&self, type_: TimeDistanceActivity) -> bool { match type_ { TimeDistanceWorkoutType::BikeRide => matches!(self, TraxRecord::BikeRide(_)), TimeDistanceWorkoutType::Row => matches!(self, TraxRecord::Row(_)), @@ -168,17 +145,14 @@ impl TraxRecord { TimeDistanceWorkoutType::Walk => matches!(self, TraxRecord::Walk(_)), } } + */ } impl Recordable for TraxRecord { fn timestamp(&self) -> Timestamp { match self { - TraxRecord::BikeRide(rec) => Timestamp::DateTime(rec.datetime), - TraxRecord::Row(rec) => Timestamp::DateTime(rec.datetime), - TraxRecord::Run(rec) => Timestamp::DateTime(rec.datetime), + TraxRecord::TimeDistance(rec) => Timestamp::DateTime(rec.datetime), TraxRecord::Steps(rec) => rec.timestamp(), - TraxRecord::Swim(rec) => Timestamp::DateTime(rec.datetime), - TraxRecord::Walk(rec) => Timestamp::DateTime(rec.datetime), TraxRecord::Weight(rec) => rec.timestamp(), } } @@ -188,6 +162,12 @@ impl Recordable for TraxRecord { } } +impl From for TraxRecord { + fn from(td: TimeDistance) -> Self { + Self::TimeDistance(td) + } +} + #[cfg(test)] mod test { use super::*;