From 73052a0694338c5ce7dcff53a91657c9169674aa Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 28 Jan 2024 14:00:09 -0500 Subject: [PATCH] Save new time/distance records This sets up a bunch of callbacks. We're starting to get into Callback Hell, where there are things that need knowledge that I really don't want them to have. However, edit fields for TimeDistanceEdit now propogate data back into the view model, which is then able to save the results. --- fitnesstrax/app/src/components/day.rs | 23 ++++- fitnesstrax/app/src/components/steps.rs | 2 +- fitnesstrax/app/src/components/text_entry.rs | 4 +- .../app/src/components/time_distance.rs | 90 +++++++++++++++---- fitnesstrax/app/src/view_models/day_detail.rs | 47 +++++----- 5 files changed, 120 insertions(+), 46 deletions(-) diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index b024912..ede6968 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -23,7 +23,7 @@ use crate::{ types::WeightFormatter, view_models::DayDetailViewModel, }; -use emseries::Record; +use emseries::{Record, RecordId}; use ft_core::{TimeDistanceActivity, TraxRecord}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; @@ -256,15 +256,32 @@ impl DayEdit { } fn add_row(&self, workout: Record) { - println!("add_row: {:?}", workout); + println!("adding a row for {:?}", workout); let workout_rows = self.imp().workout_rows.borrow(); #[allow(clippy::single_match)] match workout.data { - TraxRecord::TimeDistance(r) => workout_rows.append(&TimeDistanceEdit::new(r, |_| {})), + TraxRecord::TimeDistance(r) => workout_rows.append(&TimeDistanceEdit::new(r, { + let s = self.clone(); + move |data| { + println!("update workout callback on workout: {:?}", workout.id); + s.update_workout(workout.id, data) + } + })), + _ => {} } } + + fn update_workout(&self, id: RecordId, data: ft_core::TimeDistance) { + if let Some(ref view_model) = *self.imp().view_model.borrow() { + let record = Record { + id, + data: TraxRecord::TimeDistance(data), + }; + view_model.update_record(record); + } + } } fn control_buttons(s: &DayEdit, view_model: &DayDetailViewModel) -> ActionGroup { diff --git a/fitnesstrax/app/src/components/steps.rs b/fitnesstrax/app/src/components/steps.rs index 391b43a..11ba591 100644 --- a/fitnesstrax/app/src/components/steps.rs +++ b/fitnesstrax/app/src/components/steps.rs @@ -50,7 +50,7 @@ where "0", value, |v| format!("{}", v), - move |v| v.parse::().map_err(|_| ParseError), + |v| v.parse::().map_err(|_| ParseError), on_update, ) } diff --git a/fitnesstrax/app/src/components/text_entry.rs b/fitnesstrax/app/src/components/text_entry.rs index 9cf0542..a8f89cb 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -20,8 +20,8 @@ use crate::types::{ use gtk::prelude::*; use std::{cell::RefCell, rc::Rc}; -type Parser = dyn Fn(&str) -> Result; -type OnUpdate = dyn Fn(Option); +pub type Parser = dyn Fn(&str) -> Result; +pub type OnUpdate = dyn Fn(Option); #[derive(Clone)] pub struct TextEntry { diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs index bda7248..ee770fb 100644 --- a/fitnesstrax/app/src/components/time_distance.rs +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -17,11 +17,15 @@ 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 crate::{ + components::{distance_field, duration_field, time_field}, + types::{DistanceFormatter, DurationFormatter, TimeFormatter}, +}; use dimensioned::si; -use ft_core::TimeDistance; +use ft_core::{TimeDistance, TimeDistanceActivity}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use std::cell::RefCell; +use std::{cell::RefCell, rc::Rc}; pub fn time_distance_summary( distance: si::Meter, @@ -106,10 +110,27 @@ pub fn time_distance_detail(record: ft_core::TimeDistance) -> gtk::Box { layout } -#[derive(Default)] +type OnUpdate = Rc>>; + pub struct TimeDistanceEditPrivate { #[allow(unused)] - record: RefCell>, + workout: RefCell, + on_update: OnUpdate, +} + +impl Default for TimeDistanceEditPrivate { + fn default() -> Self { + Self { + workout: RefCell::new(TimeDistance { + datetime: chrono::Utc::now().into(), + activity: TimeDistanceActivity::BikeRide, + duration: None, + distance: None, + comments: None, + }), + on_update: Rc::new(RefCell::new(Box::new(|_| {}))), + } + } } #[glib::object_subclass] @@ -130,7 +151,7 @@ glib::wrapper! { impl Default for TimeDistanceEdit { fn default() -> Self { let s: Self = Object::builder().build(); - s.set_orientation(gtk::Orientation::Horizontal); + s.set_orientation(gtk::Orientation::Vertical); s.set_hexpand(true); s.set_css_classes(&["time-distance-edit"]); @@ -139,23 +160,62 @@ impl Default for TimeDistanceEdit { } impl TimeDistanceEdit { - pub fn new(record: TimeDistance, _on_update: OnUpdate) -> Self + pub fn new(workout: TimeDistance, on_update: OnUpdate) -> Self where - OnUpdate: Fn(&ft_core::TimeDistance), + OnUpdate: Fn(TimeDistance) + 'static, { - println!("new TimeDistanceEdit"); let s = Self::default(); - s.append(>k::Label::new(Some( - record.datetime.format("%H:%M").to_string().as_ref(), - ))); + *s.imp().workout.borrow_mut() = workout.clone(); + *s.imp().on_update.borrow_mut() = Box::new(on_update); + + let details_row = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .build(); + + details_row.append( + &time_field( + Some(TimeFormatter::from(workout.datetime.naive_local().time())), + { + let s = s.clone(); + move |t| s.update_time(t) + }, + ) + .widget(), + ); + details_row.append( + &distance_field(workout.distance.map(DistanceFormatter::from), { + let s = s.clone(); + move |d| s.update_distance(d) + }) + .widget(), + ); + details_row.append( + &duration_field(workout.duration.map(DurationFormatter::from), { + let s = s.clone(); + move |d| s.update_duration(d) + }) + .widget(), + ); + s.append(&details_row); + s.append(>k::Entry::new()); s } - /* - fn with_record(type_: ft_core::RecordType, record: ft_core::TimeDistance, on_update: OnUpdate) -> Self - where OnUpdate: Fn(&ft_core::RecordType, &ft_core::TimeDistance) { + fn update_time(&self, _time: Option) { + unimplemented!() + } + + fn update_distance(&self, distance: Option) { + let mut workout = self.imp().workout.borrow_mut(); + workout.distance = distance.map(|d| *d); + (self.imp().on_update.borrow())(workout.clone()); + } + + fn update_duration(&self, duration: Option) { + let mut workout = self.imp().workout.borrow_mut(); + workout.duration = duration.map(|d| *d); + (self.imp().on_update.borrow())(workout.clone()); } - */ } diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs index 7339168..730be85 100644 --- a/fitnesstrax/app/src/view_models/day_detail.rs +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -58,10 +58,7 @@ impl RecordState { fn set_value(&mut self, value: T) { *self = match self { RecordState::Original(r) => RecordState::Updated(Record { data: value, ..*r }), - RecordState::New(_) => RecordState::New(Record { - id: RecordId::default(), - data: value, - }), + RecordState::New(r) => RecordState::New(Record { data: value, ..*r }), RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..*r }), RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..*r }), }; @@ -225,22 +222,9 @@ impl DayDetailViewModel { .write() .unwrap() .insert(id, RecordState::New(Record { id, data: tr })); - println!( - "records after new_time_distance: {:?}", - self.records.read().unwrap() - ); Record { id, data: workout } } - pub fn update_time_distance(&self, workout: Record) { - let data = workout.data.clone(); - - let mut record_set = self.records.write().unwrap(); - record_set.entry(workout.id).and_modify(|record_state| { - record_state.set_value(TraxRecord::TimeDistance(data)); - }); - } - pub fn time_distance_records(&self) -> Vec> { self.records .read() @@ -278,6 +262,24 @@ impl DayDetailViewModel { ) } + pub fn update_record(&self, update: Record) { + println!("updating a record: {:?}", update); + let mut records = self.records.write().unwrap(); + records + .entry(update.id) + .and_modify(|record| record.set_value(update.data)); + println!("record updated: {:?}", records.get(&update.id)); + } + + pub fn records(&self) -> Vec> { + let read_lock = self.records.read().unwrap(); + read_lock + .iter() + .filter_map(|(_, record_state)| record_state.data()) + .cloned() + .collect::>>() + } + #[allow(unused)] fn get_record(&self, id: &RecordId) -> Option> { let record_set = self.records.read().unwrap(); @@ -604,11 +606,8 @@ mod test { 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::TimeDistance(record.data), - }; + 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), @@ -630,10 +629,8 @@ mod test { let (view_model, provider) = create_view_model().await; let mut workout = view_model.time_distance_records().first().cloned().unwrap(); - println!("found record: {:?}", workout); - workout.data.duration = Some(1800. * si::S); - view_model.update_time_distance(workout.clone()); + view_model.update_record(workout.map(TraxRecord::TimeDistance)); assert_eq!( view_model.time_distance_summary(TimeDistanceActivity::BikeRide),