From 6e2692362967bb5b9d2337ff9c101c44ed2d54b4 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 | 40 +++++++-- fitnesstrax/app/src/components/steps.rs | 2 +- fitnesstrax/app/src/components/text_entry.rs | 4 +- .../app/src/components/time_distance.rs | 85 +++++++++++++++---- fitnesstrax/app/src/view_models/day_detail.rs | 12 ++- 5 files changed, 113 insertions(+), 30 deletions(-) diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 7ea2852..c23bebd 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -22,7 +22,7 @@ use crate::{ }, view_models::DayDetailViewModel, }; -use emseries::Record; +use emseries::{Record, RecordId}; use ft_core::{RecordType, TraxRecord}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; @@ -198,6 +198,7 @@ impl DayDetail { pub struct DayEditPrivate { on_finished: RefCell>, workout_rows: RefCell, + view_model: RefCell, } impl Default for DayEditPrivate { @@ -210,6 +211,7 @@ impl Default for DayEditPrivate { .hexpand(true) .build(), ), + view_model: RefCell::new(DayDetailViewModel::default()), } } } @@ -238,6 +240,7 @@ impl DayEdit { s.set_orientation(gtk::Orientation::Vertical); s.set_hexpand(true); *s.imp().on_finished.borrow_mut() = Box::new(on_finished); + *s.imp().view_model.borrow_mut() = view_model.clone(); let workout_buttons = workout_buttons(view_model.clone(), { let s = s.clone(); @@ -257,22 +260,43 @@ 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(); + let workout_id = workout.id; let workout_type = workout.data.workout_type(); match workout.data { - TraxRecord::BikeRide(w) - | TraxRecord::Row(w) - | TraxRecord::Swim(w) - | TraxRecord::Run(w) - | TraxRecord::Walk(w) => { - workout_rows.append(&TimeDistanceEdit::new(workout_type, w, |_, _| {})) + TraxRecord::BikeRide(ref w) + | TraxRecord::Row(ref w) + | TraxRecord::Swim(ref w) + | TraxRecord::Run(ref w) + | TraxRecord::Walk(ref w) => { + workout_rows.append(&TimeDistanceEdit::new(workout_type, w.clone(), { + let s = self.clone(); + move |type_, data| { + println!("update workout callback on workout: {:?}", workout_id); + s.update_workout(workout_id, type_, data) + } + })); } _ => {} } } + + fn update_workout(&self, id: RecordId, type_: RecordType, data: ft_core::TimeDistance) { + println!("update workout"); + let data = match type_ { + RecordType::BikeRide => TraxRecord::BikeRide(data), + RecordType::Row => TraxRecord::Row(data), + RecordType::Swim => TraxRecord::Swim(data), + RecordType::Run => TraxRecord::Run(data), + RecordType::Walk => TraxRecord::Walk(data), + _ => panic!("Record type {:?} is not a Time/Distance record", type_), + }; + let record = Record { id, data }; + self.imp().view_model.borrow().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 0cf4ad5..76004df 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -21,8 +21,8 @@ use dimensioned::si; 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 c7297b9..1b09a22 100644 --- a/fitnesstrax/app/src/components/time_distance.rs +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -17,12 +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; +use crate::{ + components::{distance_field, duration_field, time_field}, + types::{DistanceFormatter, DurationFormatter, TimeFormatter}, +}; use dimensioned::si; use ft_core::{RecordType, TimeDistance}; 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, @@ -107,10 +110,20 @@ pub fn time_distance_detail(type_: ft_core::RecordType, record: ft_core::TimeDis layout } -#[derive(Default)] pub struct TimeDistanceEditPrivate { - type_: RefCell>, - record: RefCell>, + type_: RefCell, + workout: RefCell, + on_update: Rc>>, +} + +impl Default for TimeDistanceEditPrivate { + fn default() -> Self { + Self { + type_: RefCell::new(RecordType::BikeRide), + workout: RefCell::new(TimeDistance::new(chrono::Utc::now().into())), + on_update: Rc::new(RefCell::new(Box::new(|_, _| {}))), + } + } } #[glib::object_subclass] @@ -131,7 +144,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"]); @@ -140,23 +153,65 @@ impl Default for TimeDistanceEdit { } impl TimeDistanceEdit { - pub fn new(type_: RecordType, record: TimeDistance, on_update: OnUpdate) -> Self + pub fn new(type_: RecordType, workout: TimeDistance, on_update: OnUpdate) -> Self where - OnUpdate: Fn(&ft_core::RecordType, &ft_core::TimeDistance), + OnUpdate: Fn(ft_core::RecordType, ft_core::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().type_.borrow_mut() = type_; + *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) { + println!("update distance"); + let mut workout = self.imp().workout.borrow_mut(); + workout.distance = distance.map(|d| *d); + (self.imp().on_update.borrow())(self.imp().type_.borrow().clone(), 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())(self.imp().type_.borrow().clone(), workout.clone()); } - */ } diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs index 0d5bf48..e82166d 100644 --- a/fitnesstrax/app/src/view_models/day_detail.rs +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -56,10 +56,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 }), }; @@ -202,14 +199,20 @@ impl DayDetailViewModel { .write() .unwrap() .insert(new_record.id.clone(), RecordState::New(new_record.clone())); + println!( + "record added: {:?}", + self.records.read().unwrap().get(&new_record.id) + ); new_record } 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(|mut record| record.set_value(update.data)); + println!("record updated: {:?}", records.get(&update.id)); } pub fn records(&self) -> Vec> { @@ -272,6 +275,7 @@ impl DayDetailViewModel { .collect::>>(); for record in records { + println!("saving record: {:?}", record); match record { RecordState::New(Record { data, .. }) => { let _ = app.put_record(data).await;