diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index b0b65ed..c50ab46 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -23,7 +23,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::*}; @@ -199,6 +199,7 @@ impl DayDetail { pub struct DayEditPrivate { on_finished: RefCell>, workout_rows: RefCell, + view_model: RefCell, } impl Default for DayEditPrivate { @@ -211,6 +212,7 @@ impl Default for DayEditPrivate { .hexpand(true) .build(), ), + view_model: RefCell::new(DayDetailViewModel::default()), } } } @@ -239,6 +241,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(); @@ -258,22 +261,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 { @@ -304,8 +328,9 @@ fn weight_and_steps_row(view_model: &DayDetailViewModel) -> gtk::Box { row.append( &weight_field(view_model.weight(), { let view_model = view_model.clone(); - move |w| { - view_model.set_weight(w); + move |w| match w { + Some(w) => view_model.set_weight(w), + None => unimplemented!("need to delete the weight entry"), } }) .widget(), @@ -314,7 +339,10 @@ fn weight_and_steps_row(view_model: &DayDetailViewModel) -> gtk::Box { row.append( &steps_editor(view_model.steps(), { let view_model = view_model.clone(); - move |s| view_model.set_steps(s) + move |s| match s { + Some(s) => view_model.set_steps(s), + None => unimplemented!("need to delete the steps entry"), + } }) .widget(), ); diff --git a/fitnesstrax/app/src/components/steps.rs b/fitnesstrax/app/src/components/steps.rs index e81c840..f86da50 100644 --- a/fitnesstrax/app/src/components/steps.rs +++ b/fitnesstrax/app/src/components/steps.rs @@ -14,7 +14,7 @@ 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::{ParseError, TextEntry}; +use crate::components::{text_entry::OnUpdate, ParseError, TextEntry}; use gtk::prelude::*; #[derive(Default)] @@ -44,18 +44,13 @@ impl Steps { pub fn steps_editor(value: Option, on_update: OnUpdate) -> TextEntry where - OnUpdate: Fn(u32) + 'static, + OnUpdate: Fn(Option) + 'static, { TextEntry::new( "0", value, |v| format!("{}", v), - move |v| match v.parse::() { - Ok(val) => { - on_update(val); - Ok(val) - } - Err(_) => 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 c3fc96b..b014b33 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -21,8 +21,9 @@ use std::{cell::RefCell, rc::Rc}; #[derive(Clone, Debug)] pub struct ParseError; -type Renderer = dyn Fn(&T) -> String; -type Parser = dyn Fn(&str) -> Result; +pub type Renderer = dyn Fn(&T) -> String; +pub type Parser = dyn Fn(&str) -> Result; +pub type OnUpdate = dyn Fn(Option); #[derive(Clone)] pub struct TextEntry { @@ -30,6 +31,7 @@ pub struct TextEntry { widget: gtk::Entry, renderer: Rc>, parser: Rc>, + on_update: Rc>, } impl std::fmt::Debug for TextEntry { @@ -44,10 +46,17 @@ impl std::fmt::Debug for TextEntry { // I do not understand why the data should be 'static. impl TextEntry { - pub fn new(placeholder: &str, value: Option, renderer: R, parser: V) -> Self + pub fn new( + placeholder: &str, + value: Option, + renderer: R, + parser: V, + on_update: U, + ) -> Self where R: Fn(&T) -> String + 'static, V: Fn(&str) -> Result + 'static, + U: Fn(Option) + 'static, { let widget = gtk::Entry::builder().placeholder_text(placeholder).build(); if let Some(ref v) = value { @@ -59,6 +68,7 @@ impl TextEntry { widget, renderer: Rc::new(renderer), parser: Rc::new(parser), + on_update: Rc::new(on_update), }; s.widget.buffer().connect_text_notify({ @@ -77,8 +87,9 @@ impl TextEntry { } match (self.parser)(buffer.text().as_str()) { Ok(v) => { - *self.value.borrow_mut() = Some(v); + *self.value.borrow_mut() = Some(v.clone()); self.widget.remove_css_class("error"); + (self.on_update)(Some(v)); } // need to change the border to provide a visual indicator of an error Err(_) => { @@ -115,35 +126,40 @@ pub fn weight_field( on_update: OnUpdate, ) -> TextEntry> where - OnUpdate: Fn(si::Kilogram) + 'static, + OnUpdate: Fn(Option>) + 'static, { TextEntry::new( "0 kg", weight, |val: &si::Kilogram| val.to_string(), - move |v: &str| { - let new_weight = v.parse::().map(|w| w * si::KG).map_err(|_| ParseError); - match new_weight { - Ok(w) => { - on_update(w); - Ok(w) - } - Err(err) => Err(err), - } - }, + move |v: &str| v.parse::().map(|w| w * si::KG).map_err(|_| ParseError), + on_update, ) } -pub fn time_field(value: chrono::NaiveTime) -> TextEntry { +pub fn time_field( + value: chrono::NaiveTime, + on_update: OnUpdate, +) -> TextEntry +where + OnUpdate: Fn(Option) + 'static, +{ TextEntry::new( "hh:mm", Some(value), |v| v.format("%H:%M").to_string(), |s| chrono::NaiveTime::parse_from_str(s, "%H:%M").map_err(|_| ParseError), + on_update, ) } -pub fn distance_field(value: Option>) -> TextEntry> { +pub fn distance_field( + value: Option>, + on_update: OnUpdate, +) -> TextEntry> +where + OnUpdate: Fn(Option>) + 'static, +{ TextEntry::new( "0 km", value, @@ -154,10 +170,17 @@ pub fn distance_field(value: Option>) -> TextEntry println!("value: {}", value); Ok(value * 1000. * si::M) }, + on_update, ) } -pub fn duration_field(value: Option>) -> TextEntry> { +pub fn duration_field( + value: Option>, + on_update: OnUpdate, +) -> TextEntry> +where + OnUpdate: Fn(Option>) + 'static, +{ TextEntry::new( "0 minutes", value, @@ -167,6 +190,7 @@ pub fn duration_field(value: Option>) -> TextEntry().map_err(|_| ParseError)?; Ok(value * 60. * si::S) }, + on_update, ) } diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs index c7297b9..f164540 100644 --- a/fitnesstrax/app/src/components/time_distance.rs +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -17,12 +17,12 @@ 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}; 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 +107,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 +141,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 +150,62 @@ 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(workout.datetime.naive_local().time(), { + let s = s.clone(); + move |t| s.update_time(t) + }) + .widget(), + ); + details_row.append( + &distance_field(workout.distance, { + let s = s.clone(); + move |d| s.update_distance(d) + }) + .widget(), + ); + details_row.append( + &duration_field(workout.duration, { + 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; + (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; + (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 6bd2d82..9681ba6 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 }), }; @@ -200,14 +197,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> { @@ -270,6 +273,7 @@ impl DayDetailViewModel { .collect::>>(); for record in records { + println!("saving record: {:?}", record); match record { RecordState::New(Record { data, .. }) => { let _ = app.put_record(data).await;