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.
This commit is contained in:
Savanni D'Gerinel 2024-01-28 14:00:09 -05:00
parent 39acfe7950
commit 2277055f84
5 changed files with 157 additions and 57 deletions

View File

@ -23,7 +23,7 @@ use crate::{
}, },
view_models::DayDetailViewModel, view_models::DayDetailViewModel,
}; };
use emseries::Record; use emseries::{Record, RecordId};
use ft_core::{RecordType, TraxRecord}; use ft_core::{RecordType, TraxRecord};
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
@ -199,6 +199,7 @@ impl DayDetail {
pub struct DayEditPrivate { pub struct DayEditPrivate {
on_finished: RefCell<Box<dyn Fn()>>, on_finished: RefCell<Box<dyn Fn()>>,
workout_rows: RefCell<gtk::Box>, workout_rows: RefCell<gtk::Box>,
view_model: RefCell<DayDetailViewModel>,
} }
impl Default for DayEditPrivate { impl Default for DayEditPrivate {
@ -211,6 +212,7 @@ impl Default for DayEditPrivate {
.hexpand(true) .hexpand(true)
.build(), .build(),
), ),
view_model: RefCell::new(DayDetailViewModel::default()),
} }
} }
} }
@ -239,6 +241,7 @@ impl DayEdit {
s.set_orientation(gtk::Orientation::Vertical); s.set_orientation(gtk::Orientation::Vertical);
s.set_hexpand(true); s.set_hexpand(true);
*s.imp().on_finished.borrow_mut() = Box::new(on_finished); *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 workout_buttons = workout_buttons(view_model.clone(), {
let s = s.clone(); let s = s.clone();
@ -258,22 +261,43 @@ impl DayEdit {
} }
fn add_row(&self, workout: Record<TraxRecord>) { fn add_row(&self, workout: Record<TraxRecord>) {
println!("add_row: {:?}", workout); println!("adding a row for {:?}", workout);
let workout_rows = self.imp().workout_rows.borrow(); let workout_rows = self.imp().workout_rows.borrow();
let workout_id = workout.id;
let workout_type = workout.data.workout_type(); let workout_type = workout.data.workout_type();
match workout.data { match workout.data {
TraxRecord::BikeRide(w) TraxRecord::BikeRide(ref w)
| TraxRecord::Row(w) | TraxRecord::Row(ref w)
| TraxRecord::Swim(w) | TraxRecord::Swim(ref w)
| TraxRecord::Run(w) | TraxRecord::Run(ref w)
| TraxRecord::Walk(w) => { | TraxRecord::Walk(ref w) => {
workout_rows.append(&TimeDistanceEdit::new(workout_type, 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 { 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( row.append(
&weight_field(view_model.weight(), { &weight_field(view_model.weight(), {
let view_model = view_model.clone(); let view_model = view_model.clone();
move |w| { move |w| match w {
view_model.set_weight(w); Some(w) => view_model.set_weight(w),
None => unimplemented!("need to delete the weight entry"),
} }
}) })
.widget(), .widget(),
@ -314,7 +339,10 @@ fn weight_and_steps_row(view_model: &DayDetailViewModel) -> gtk::Box {
row.append( row.append(
&steps_editor(view_model.steps(), { &steps_editor(view_model.steps(), {
let view_model = view_model.clone(); 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(), .widget(),
); );

View File

@ -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 <https://www.gnu.org/licenses/>. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::components::{ParseError, TextEntry}; use crate::components::{text_entry::OnUpdate, ParseError, TextEntry};
use gtk::prelude::*; use gtk::prelude::*;
#[derive(Default)] #[derive(Default)]
@ -44,18 +44,13 @@ impl Steps {
pub fn steps_editor<OnUpdate>(value: Option<u32>, on_update: OnUpdate) -> TextEntry<u32> pub fn steps_editor<OnUpdate>(value: Option<u32>, on_update: OnUpdate) -> TextEntry<u32>
where where
OnUpdate: Fn(u32) + 'static, OnUpdate: Fn(Option<u32>) + 'static,
{ {
TextEntry::new( TextEntry::new(
"0", "0",
value, value,
|v| format!("{}", v), |v| format!("{}", v),
move |v| match v.parse::<u32>() { |v| v.parse::<u32>().map_err(|_| ParseError),
Ok(val) => { on_update,
on_update(val);
Ok(val)
}
Err(_) => Err(ParseError),
},
) )
} }

View File

@ -21,8 +21,9 @@ use std::{cell::RefCell, rc::Rc};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ParseError; pub struct ParseError;
type Renderer<T> = dyn Fn(&T) -> String; pub type Renderer<T> = dyn Fn(&T) -> String;
type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>; pub type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>;
pub type OnUpdate<T> = dyn Fn(Option<T>);
#[derive(Clone)] #[derive(Clone)]
pub struct TextEntry<T: Clone + std::fmt::Debug> { pub struct TextEntry<T: Clone + std::fmt::Debug> {
@ -30,6 +31,7 @@ pub struct TextEntry<T: Clone + std::fmt::Debug> {
widget: gtk::Entry, widget: gtk::Entry,
renderer: Rc<Renderer<T>>, renderer: Rc<Renderer<T>>,
parser: Rc<Parser<T>>, parser: Rc<Parser<T>>,
on_update: Rc<OnUpdate<T>>,
} }
impl<T: Clone + std::fmt::Debug> std::fmt::Debug for TextEntry<T> { impl<T: Clone + std::fmt::Debug> std::fmt::Debug for TextEntry<T> {
@ -44,10 +46,17 @@ impl<T: Clone + std::fmt::Debug> std::fmt::Debug for TextEntry<T> {
// I do not understand why the data should be 'static. // I do not understand why the data should be 'static.
impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> { impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
pub fn new<R, V>(placeholder: &str, value: Option<T>, renderer: R, parser: V) -> Self pub fn new<R, V, U>(
placeholder: &str,
value: Option<T>,
renderer: R,
parser: V,
on_update: U,
) -> Self
where where
R: Fn(&T) -> String + 'static, R: Fn(&T) -> String + 'static,
V: Fn(&str) -> Result<T, ParseError> + 'static, V: Fn(&str) -> Result<T, ParseError> + 'static,
U: Fn(Option<T>) + 'static,
{ {
let widget = gtk::Entry::builder().placeholder_text(placeholder).build(); let widget = gtk::Entry::builder().placeholder_text(placeholder).build();
if let Some(ref v) = value { if let Some(ref v) = value {
@ -59,6 +68,7 @@ impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
widget, widget,
renderer: Rc::new(renderer), renderer: Rc::new(renderer),
parser: Rc::new(parser), parser: Rc::new(parser),
on_update: Rc::new(on_update),
}; };
s.widget.buffer().connect_text_notify({ s.widget.buffer().connect_text_notify({
@ -77,8 +87,9 @@ impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
} }
match (self.parser)(buffer.text().as_str()) { match (self.parser)(buffer.text().as_str()) {
Ok(v) => { Ok(v) => {
*self.value.borrow_mut() = Some(v); *self.value.borrow_mut() = Some(v.clone());
self.widget.remove_css_class("error"); self.widget.remove_css_class("error");
(self.on_update)(Some(v));
} }
// need to change the border to provide a visual indicator of an error // need to change the border to provide a visual indicator of an error
Err(_) => { Err(_) => {
@ -115,35 +126,40 @@ pub fn weight_field<OnUpdate>(
on_update: OnUpdate, on_update: OnUpdate,
) -> TextEntry<si::Kilogram<f64>> ) -> TextEntry<si::Kilogram<f64>>
where where
OnUpdate: Fn(si::Kilogram<f64>) + 'static, OnUpdate: Fn(Option<si::Kilogram<f64>>) + 'static,
{ {
TextEntry::new( TextEntry::new(
"0 kg", "0 kg",
weight, weight,
|val: &si::Kilogram<f64>| val.to_string(), |val: &si::Kilogram<f64>| val.to_string(),
move |v: &str| { move |v: &str| v.parse::<f64>().map(|w| w * si::KG).map_err(|_| ParseError),
let new_weight = v.parse::<f64>().map(|w| w * si::KG).map_err(|_| ParseError); on_update,
match new_weight {
Ok(w) => {
on_update(w);
Ok(w)
}
Err(err) => Err(err),
}
},
) )
} }
pub fn time_field(value: chrono::NaiveTime) -> TextEntry<chrono::NaiveTime> { pub fn time_field<OnUpdate>(
value: chrono::NaiveTime,
on_update: OnUpdate,
) -> TextEntry<chrono::NaiveTime>
where
OnUpdate: Fn(Option<chrono::NaiveTime>) + 'static,
{
TextEntry::new( TextEntry::new(
"hh:mm", "hh:mm",
Some(value), Some(value),
|v| v.format("%H:%M").to_string(), |v| v.format("%H:%M").to_string(),
|s| chrono::NaiveTime::parse_from_str(s, "%H:%M").map_err(|_| ParseError), |s| chrono::NaiveTime::parse_from_str(s, "%H:%M").map_err(|_| ParseError),
on_update,
) )
} }
pub fn distance_field(value: Option<si::Meter<f64>>) -> TextEntry<si::Meter<f64>> { pub fn distance_field<OnUpdate>(
value: Option<si::Meter<f64>>,
on_update: OnUpdate,
) -> TextEntry<si::Meter<f64>>
where
OnUpdate: Fn(Option<si::Meter<f64>>) + 'static,
{
TextEntry::new( TextEntry::new(
"0 km", "0 km",
value, value,
@ -154,10 +170,17 @@ pub fn distance_field(value: Option<si::Meter<f64>>) -> TextEntry<si::Meter<f64>
println!("value: {}", value); println!("value: {}", value);
Ok(value * 1000. * si::M) Ok(value * 1000. * si::M)
}, },
on_update,
) )
} }
pub fn duration_field(value: Option<si::Second<f64>>) -> TextEntry<si::Second<f64>> { pub fn duration_field<OnUpdate>(
value: Option<si::Second<f64>>,
on_update: OnUpdate,
) -> TextEntry<si::Second<f64>>
where
OnUpdate: Fn(Option<si::Second<f64>>) + 'static,
{
TextEntry::new( TextEntry::new(
"0 minutes", "0 minutes",
value, value,
@ -167,6 +190,7 @@ pub fn duration_field(value: Option<si::Second<f64>>) -> TextEntry<si::Second<f6
let value = digits.parse::<f64>().map_err(|_| ParseError)?; let value = digits.parse::<f64>().map_err(|_| ParseError)?;
Ok(value * 60. * si::S) Ok(value * 60. * si::S)
}, },
on_update,
) )
} }

View File

@ -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 crate::components::{EditView, ParseError, TextEntry};
// use chrono::{Local, NaiveDate}; // use chrono::{Local, NaiveDate};
// use dimensioned::si; // use dimensioned::si;
use crate::components::distance_field; use crate::components::{distance_field, duration_field, time_field};
use dimensioned::si; use dimensioned::si;
use ft_core::{RecordType, TimeDistance}; use ft_core::{RecordType, TimeDistance};
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use std::cell::RefCell; use std::{cell::RefCell, rc::Rc};
pub fn time_distance_summary( pub fn time_distance_summary(
distance: si::Meter<f64>, distance: si::Meter<f64>,
@ -107,10 +107,20 @@ pub fn time_distance_detail(type_: ft_core::RecordType, record: ft_core::TimeDis
layout layout
} }
#[derive(Default)]
pub struct TimeDistanceEditPrivate { pub struct TimeDistanceEditPrivate {
type_: RefCell<Option<ft_core::RecordType>>, type_: RefCell<RecordType>,
record: RefCell<Option<ft_core::RecordType>>, workout: RefCell<TimeDistance>,
on_update: Rc<RefCell<Box<dyn Fn(RecordType, TimeDistance)>>>,
}
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] #[glib::object_subclass]
@ -131,7 +141,7 @@ glib::wrapper! {
impl Default for TimeDistanceEdit { impl Default for TimeDistanceEdit {
fn default() -> Self { fn default() -> Self {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
s.set_orientation(gtk::Orientation::Horizontal); s.set_orientation(gtk::Orientation::Vertical);
s.set_hexpand(true); s.set_hexpand(true);
s.set_css_classes(&["time-distance-edit"]); s.set_css_classes(&["time-distance-edit"]);
@ -140,23 +150,62 @@ impl Default for TimeDistanceEdit {
} }
impl TimeDistanceEdit { impl TimeDistanceEdit {
pub fn new<OnUpdate>(type_: RecordType, record: TimeDistance, on_update: OnUpdate) -> Self pub fn new<OnUpdate>(type_: RecordType, workout: TimeDistance, on_update: OnUpdate) -> Self
where where
OnUpdate: Fn(&ft_core::RecordType, &ft_core::TimeDistance), OnUpdate: Fn(ft_core::RecordType, ft_core::TimeDistance) + 'static,
{ {
println!("new TimeDistanceEdit"); println!("new TimeDistanceEdit");
let s = Self::default(); let s = Self::default();
s.append(&gtk::Label::new(Some( *s.imp().type_.borrow_mut() = type_;
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(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(&gtk::Entry::new());
s s
} }
/* fn update_time(&self, time: Option<chrono::NaiveTime>) {
fn with_record<OnUpdate>(type_: ft_core::RecordType, record: ft_core::TimeDistance, on_update: OnUpdate) -> Self unimplemented!()
where OnUpdate: Fn(&ft_core::RecordType, &ft_core::TimeDistance) { }
fn update_distance(&self, distance: Option<si::Meter<f64>>) {
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<si::Second<f64>>) {
let mut workout = self.imp().workout.borrow_mut();
workout.duration = duration;
(self.imp().on_update.borrow())(self.imp().type_.borrow().clone(), workout.clone());
} }
*/
} }

View File

@ -56,10 +56,7 @@ impl<T: Clone + emseries::Recordable> RecordState<T> {
fn set_value(&mut self, value: T) { fn set_value(&mut self, value: T) {
*self = match self { *self = match self {
RecordState::Original(r) => RecordState::Updated(Record { data: value, ..*r }), RecordState::Original(r) => RecordState::Updated(Record { data: value, ..*r }),
RecordState::New(_) => RecordState::New(Record { RecordState::New(r) => RecordState::New(Record { data: value, ..*r }),
id: RecordId::default(),
data: value,
}),
RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..*r }), RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..*r }),
RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..*r }), RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..*r }),
}; };
@ -200,14 +197,20 @@ impl DayDetailViewModel {
.write() .write()
.unwrap() .unwrap()
.insert(new_record.id.clone(), RecordState::New(new_record.clone())); .insert(new_record.id.clone(), RecordState::New(new_record.clone()));
println!(
"record added: {:?}",
self.records.read().unwrap().get(&new_record.id)
);
new_record new_record
} }
pub fn update_record(&self, update: Record<TraxRecord>) { pub fn update_record(&self, update: Record<TraxRecord>) {
println!("updating a record: {:?}", update);
let mut records = self.records.write().unwrap(); let mut records = self.records.write().unwrap();
records records
.entry(update.id) .entry(update.id)
.and_modify(|mut record| record.set_value(update.data)); .and_modify(|mut record| record.set_value(update.data));
println!("record updated: {:?}", records.get(&update.id));
} }
pub fn records(&self) -> Vec<Record<TraxRecord>> { pub fn records(&self) -> Vec<Record<TraxRecord>> {
@ -270,6 +273,7 @@ impl DayDetailViewModel {
.collect::<Vec<RecordState<TraxRecord>>>(); .collect::<Vec<RecordState<TraxRecord>>>();
for record in records { for record in records {
println!("saving record: {:?}", record);
match record { match record {
RecordState::New(Record { data, .. }) => { RecordState::New(Record { data, .. }) => {
let _ = app.put_record(data).await; let _ = app.put_record(data).await;