Render and be able to edit bike rides (and sorta other time distance workouts) #169

Merged
savanni merged 13 commits from fitnesstrax/time-distance-workout into main 2024-02-09 00:05:26 +00:00
5 changed files with 120 additions and 46 deletions
Showing only changes of commit 73052a0694 - Show all commits

View File

@ -23,7 +23,7 @@ use crate::{
types::WeightFormatter, types::WeightFormatter,
view_models::DayDetailViewModel, view_models::DayDetailViewModel,
}; };
use emseries::Record; use emseries::{Record, RecordId};
use ft_core::{TimeDistanceActivity, TraxRecord}; use ft_core::{TimeDistanceActivity, TraxRecord};
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
@ -256,15 +256,32 @@ 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();
#[allow(clippy::single_match)] #[allow(clippy::single_match)]
match workout.data { 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 { fn control_buttons(s: &DayEdit, view_model: &DayDetailViewModel) -> ActionGroup {

View File

@ -50,7 +50,7 @@ where
"0", "0",
value, value,
|v| format!("{}", v), |v| format!("{}", v),
move |v| v.parse::<u32>().map_err(|_| ParseError), |v| v.parse::<u32>().map_err(|_| ParseError),
on_update, on_update,
) )
} }

View File

@ -20,8 +20,8 @@ use crate::types::{
use gtk::prelude::*; use gtk::prelude::*;
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>; pub type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>;
type OnUpdate<T> = dyn Fn(Option<T>); 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> {

View File

@ -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 crate::components::{EditView, ParseError, TextEntry};
// use chrono::{Local, NaiveDate}; // use chrono::{Local, NaiveDate};
// use dimensioned::si; // use dimensioned::si;
use crate::{
components::{distance_field, duration_field, time_field},
types::{DistanceFormatter, DurationFormatter, TimeFormatter},
};
use dimensioned::si; use dimensioned::si;
use ft_core::TimeDistance; use ft_core::{TimeDistance, TimeDistanceActivity};
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>,
@ -106,10 +110,27 @@ pub fn time_distance_detail(record: ft_core::TimeDistance) -> gtk::Box {
layout layout
} }
#[derive(Default)] type OnUpdate = Rc<RefCell<Box<dyn Fn(TimeDistance)>>>;
pub struct TimeDistanceEditPrivate { pub struct TimeDistanceEditPrivate {
#[allow(unused)] #[allow(unused)]
record: RefCell<Option<ft_core::TimeDistance>>, workout: RefCell<ft_core::TimeDistance>,
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] #[glib::object_subclass]
@ -130,7 +151,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"]);
@ -139,23 +160,62 @@ impl Default for TimeDistanceEdit {
} }
impl TimeDistanceEdit { impl TimeDistanceEdit {
pub fn new<OnUpdate>(record: TimeDistance, _on_update: OnUpdate) -> Self pub fn new<OnUpdate>(workout: TimeDistance, on_update: OnUpdate) -> Self
where where
OnUpdate: Fn(&ft_core::TimeDistance), OnUpdate: Fn(TimeDistance) + 'static,
{ {
println!("new TimeDistanceEdit");
let s = Self::default(); let s = Self::default();
s.append(&gtk::Label::new(Some( *s.imp().workout.borrow_mut() = workout.clone();
record.datetime.format("%H:%M").to_string().as_ref(), *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(&gtk::Entry::new());
s s
} }
/* fn update_time(&self, _time: Option<TimeFormatter>) {
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<DistanceFormatter>) {
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<DurationFormatter>) {
let mut workout = self.imp().workout.borrow_mut();
workout.duration = duration.map(|d| *d);
(self.imp().on_update.borrow())(workout.clone());
} }
*/
} }

View File

@ -58,10 +58,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 }),
}; };
@ -225,22 +222,9 @@ impl DayDetailViewModel {
.write() .write()
.unwrap() .unwrap()
.insert(id, RecordState::New(Record { id, data: tr })); .insert(id, RecordState::New(Record { id, data: tr }));
println!(
"records after new_time_distance: {:?}",
self.records.read().unwrap()
);
Record { id, data: workout } Record { id, data: workout }
} }
pub fn update_time_distance(&self, workout: Record<TimeDistance>) {
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<Record<TimeDistance>> { pub fn time_distance_records(&self) -> Vec<Record<TimeDistance>> {
self.records self.records
.read() .read()
@ -278,6 +262,24 @@ impl DayDetailViewModel {
) )
} }
pub fn update_record(&self, update: Record<TraxRecord>) {
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<Record<TraxRecord>> {
let read_lock = self.records.read().unwrap();
read_lock
.iter()
.filter_map(|(_, record_state)| record_state.data())
.cloned()
.collect::<Vec<Record<TraxRecord>>>()
}
#[allow(unused)] #[allow(unused)]
fn get_record(&self, id: &RecordId) -> Option<Record<TraxRecord>> { fn get_record(&self, id: &RecordId) -> Option<Record<TraxRecord>> {
let record_set = self.records.read().unwrap(); let record_set = self.records.read().unwrap();
@ -604,11 +606,8 @@ mod test {
let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide); let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide);
record.data.duration = Some(60. * si::S); record.data.duration = Some(60. * si::S);
view_model.update_time_distance(record.clone()); let record = record.map(TraxRecord::TimeDistance);
let record = Record { view_model.update_record(record.clone());
id: record.id,
data: TraxRecord::TimeDistance(record.data),
};
assert_eq!(view_model.get_record(&record.id), Some(record)); assert_eq!(view_model.get_record(&record.id), Some(record));
assert_eq!( assert_eq!(
view_model.time_distance_summary(TimeDistanceActivity::BikeRide), view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
@ -630,10 +629,8 @@ mod test {
let (view_model, provider) = create_view_model().await; let (view_model, provider) = create_view_model().await;
let mut workout = view_model.time_distance_records().first().cloned().unwrap(); let mut workout = view_model.time_distance_records().first().cloned().unwrap();
println!("found record: {:?}", workout);
workout.data.duration = Some(1800. * si::S); workout.data.duration = Some(1800. * si::S);
view_model.update_time_distance(workout.clone()); view_model.update_record(workout.map(TraxRecord::TimeDistance));
assert_eq!( assert_eq!(
view_model.time_distance_summary(TimeDistanceActivity::BikeRide), view_model.time_distance_summary(TimeDistanceActivity::BikeRide),