diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 289f1a5..6c26f00 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -24,7 +24,7 @@ use crate::{ view_models::DayDetailViewModel, }; use emseries::{Record, RecordId}; -use ft_core::{TimeDistanceActivity, TraxRecord}; +use ft_core::{TimeDistanceActivity, TraxRecord, TIME_DISTANCE_ACTIVITIES}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::{cell::RefCell, rc::Rc}; @@ -105,12 +105,15 @@ impl DaySummary { row.append(&label); self.append(&row); - let biking_summary = view_model.time_distance_summary(TimeDistanceActivity::BikeRide); - if let Some(label) = time_distance_summary( - DistanceFormatter::from(biking_summary.0), - DurationFormatter::from(biking_summary.1), - ) { - self.append(&label); + for activity in TIME_DISTANCE_ACTIVITIES { + let summary = view_model.time_distance_summary(activity); + if let Some(label) = time_distance_summary( + activity, + DistanceFormatter::from(summary.0), + DurationFormatter::from(summary.1), + ) { + self.append(&label); + } } } } @@ -383,7 +386,7 @@ where biking_button.connect_clicked({ let view_model = view_model.clone(); move |_| { - let workout = view_model.new_time_distance(TimeDistanceActivity::BikeRide); + let workout = view_model.new_time_distance(TimeDistanceActivity::Biking); add_row(workout.map(TraxRecord::TimeDistance)); } }); diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs index 7b800c1..6ad5c3a 100644 --- a/fitnesstrax/app/src/components/time_distance.rs +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -14,31 +14,38 @@ 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::{EditView, ParseError, TextEntry}; -// use chrono::{Local, NaiveDate}; -// use dimensioned::si; use crate::{ components::{distance_field, duration_field, time_field}, types::{DistanceFormatter, DurationFormatter, FormatOption, TimeFormatter}, }; use dimensioned::si; -use ft_core::{TimeDistance, TimeDistanceActivity}; +use ft_core::{TimeDistance, TimeDistanceActivity, TIME_DISTANCE_ACTIVITIES}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use std::{rc::Rc, cell::RefCell}; +use std::{cell::RefCell, rc::Rc}; pub fn time_distance_summary( + activity: TimeDistanceActivity, distance: DistanceFormatter, duration: DurationFormatter, ) -> Option { let text = match (*distance > si::M, *duration > si::S) { (true, true) => Some(format!( - "{} of biking in {}", + "{} of {:?} in {}", distance.format(FormatOption::Full), + activity, duration.format(FormatOption::Full) )), - (true, false) => Some(format!("{} of biking", distance.format(FormatOption::Full))), - (false, true) => Some(format!("{} of biking", duration.format(FormatOption::Full))), + (true, false) => Some(format!( + "{} of {:?}", + distance.format(FormatOption::Full), + activity + )), + (false, true) => Some(format!( + "{} of {:?}", + duration.format(FormatOption::Full), + activity + )), (false, false) => None, }; @@ -122,7 +129,7 @@ impl Default for TimeDistanceEditPrivate { Self { workout: RefCell::new(TimeDistance { datetime: chrono::Utc::now().into(), - activity: TimeDistanceActivity::BikeRide, + activity: TimeDistanceActivity::Biking, duration: None, distance: None, comments: None, @@ -182,6 +189,7 @@ impl TimeDistanceEdit { ) .widget(), ); + details_row.append(&s.activity_menu(workout.activity)); details_row.append( &distance_field(workout.distance.map(DistanceFormatter::from), { let s = s.clone(); @@ -202,8 +210,26 @@ impl TimeDistanceEdit { s } - fn update_time(&self, _time: Option) { - unimplemented!() + fn update_time(&self, time: Option) { + if let Some(time_formatter) = time { + let mut workout = self.imp().workout.borrow_mut(); + let tz = workout.datetime.timezone(); + let new_time = workout + .datetime + .date_naive() + .and_time(*time_formatter) + .and_local_timezone(tz) + .unwrap() + .fixed_offset(); + workout.datetime = new_time; + (self.imp().on_update.borrow())(workout.clone()); + } + } + + fn update_workout_type(&self, type_: TimeDistanceActivity) { + let mut workout = self.imp().workout.borrow_mut(); + workout.activity = type_; + (self.imp().on_update.borrow())(workout.clone()) } fn update_distance(&self, distance: Option) { @@ -217,4 +243,29 @@ impl TimeDistanceEdit { workout.duration = duration.map(|d| *d); (self.imp().on_update.borrow())(workout.clone()); } + + fn activity_menu(&self, selected: TimeDistanceActivity) -> gtk::DropDown { + let options = TIME_DISTANCE_ACTIVITIES + .iter() + .map(|item| format!("{:?}", item)) + .collect::>(); + + let options = options.iter().map(|o| o.as_ref()).collect::>(); + + let selected_idx = TIME_DISTANCE_ACTIVITIES + .iter() + .position(|&v| v == selected) + .unwrap_or(0); + + let menu = gtk::DropDown::from_strings(&options); + menu.set_selected(selected_idx as u32); + menu.connect_selected_item_notify({ + let s = self.clone(); + move |menu| { + let new_item = TIME_DISTANCE_ACTIVITIES[menu.selected() as usize]; + s.update_workout_type(new_item); + } + }); + menu + } } diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs index a8f3c08..ab379d2 100644 --- a/fitnesstrax/app/src/view_models/day_detail.rs +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -176,9 +176,20 @@ impl DayDetailViewModel { } pub fn new_time_distance(&self, activity: TimeDistanceActivity) -> Record { + let now = chrono::Local::now(); + let base_time = now.time(); + let tz = now.timezone(); + let datetime = self + .date + .clone() + .and_time(base_time) + .and_local_timezone(tz) + .unwrap() + .into(); + let id = RecordId::default(); let workout = TimeDistance { - datetime: chrono::Local::now().into(), + datetime, activity, distance: None, duration: None, @@ -499,7 +510,7 @@ mod test { id: RecordId::default(), data: TraxRecord::TimeDistance(ft_core::TimeDistance { datetime: oct_13_am.clone(), - activity: TimeDistanceActivity::BikeRide, + activity: TimeDistanceActivity::Biking, distance: Some(15000. * si::M), duration: Some(3600. * si::S), comments: Some("somecomments present".to_owned()), @@ -549,11 +560,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(TimeDistanceActivity::BikeRide), + view_model.time_distance_summary(TimeDistanceActivity::Biking), (0. * si::M, 0. * si::S) ); - let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide); + let mut record = view_model.new_time_distance(TimeDistanceActivity::Biking); record.data.duration = Some(60. * si::S); view_model.async_save().await; @@ -566,17 +577,17 @@ 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(TimeDistanceActivity::BikeRide), + view_model.time_distance_summary(TimeDistanceActivity::Biking), (0. * si::M, 0. * si::S) ); - let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide); + let mut record = view_model.new_time_distance(TimeDistanceActivity::Biking); record.data.duration = Some(60. * si::S); let record = record.map(TraxRecord::TimeDistance); view_model.update_record(record.clone()); assert_eq!(view_model.get_record(&record.id), Some(record)); assert_eq!( - view_model.time_distance_summary(TimeDistanceActivity::BikeRide), + view_model.time_distance_summary(TimeDistanceActivity::Biking), (0. * si::M, 60. * si::S) ); assert_eq!( @@ -599,7 +610,7 @@ mod test { view_model.update_record(workout.map(TraxRecord::TimeDistance)); assert_eq!( - view_model.time_distance_summary(TimeDistanceActivity::BikeRide), + view_model.time_distance_summary(TimeDistanceActivity::Biking), (15000. * si::M, 1800. * si::S) ); @@ -614,11 +625,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(TimeDistanceActivity::BikeRide), + view_model.time_distance_summary(TimeDistanceActivity::Biking), (0. * si::M, 0. * si::S) ); - let record = view_model.new_time_distance(TimeDistanceActivity::BikeRide); + let record = view_model.new_time_distance(TimeDistanceActivity::Biking); view_model.remove_record(record.id); view_model.save(); @@ -634,7 +645,7 @@ mod test { view_model.remove_record(workout.id); assert_eq!( - view_model.time_distance_summary(TimeDistanceActivity::BikeRide), + view_model.time_distance_summary(TimeDistanceActivity::Biking), (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 89895ad..f579628 100644 --- a/fitnesstrax/core/src/lib.rs +++ b/fitnesstrax/core/src/lib.rs @@ -1,4 +1,6 @@ mod legacy; mod types; -pub use types::{Steps, TimeDistance, TimeDistanceActivity, TraxRecord, Weight}; +pub use types::{ + Steps, TimeDistance, TimeDistanceActivity, TraxRecord, Weight, TIME_DISTANCE_ACTIVITIES, +}; diff --git a/fitnesstrax/core/src/types.rs b/fitnesstrax/core/src/types.rs index a48da32..d2ef95c 100644 --- a/fitnesstrax/core/src/types.rs +++ b/fitnesstrax/core/src/types.rs @@ -1,3 +1,19 @@ +/* +Copyright 2023-2024, 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::{DateTime, FixedOffset, NaiveDate}; use dimensioned::si; use emseries::{Recordable, Timestamp}; @@ -35,13 +51,21 @@ impl Recordable for Steps { #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub enum TimeDistanceActivity { - BikeRide, + Biking, Running, Rowing, Swimming, Walking, } +pub const TIME_DISTANCE_ACTIVITIES: [TimeDistanceActivity; 5] = [ + TimeDistanceActivity::Biking, + TimeDistanceActivity::Rowing, + TimeDistanceActivity::Running, + TimeDistanceActivity::Swimming, + TimeDistanceActivity::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 @@ -117,7 +141,7 @@ impl TraxRecord { matches!( self, TraxRecord::TimeDistance(TimeDistance { - activity: TimeDistanceActivity::BikeRide, + activity: TimeDistanceActivity::Biking, .. }) | TraxRecord::TimeDistance(TimeDistance { activity: TimeDistanceActivity::Running,