From 39acfe7950014f31fcf899109a9a8670340b0a47 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sat, 27 Jan 2024 10:37:18 -0500 Subject: [PATCH] Build the facilities to add a new time/distance workout This adds the code to show the new records in the UI, plus it adds them to the view model. Some of the representation changed in order to facilitate linking UI elements to particular records. There are now some buttons to create workouts of various types, clicking on a button adds a new row to the UI, and it also adds a new record to the view model. Saving the view model writes the records to the database. --- fitnesstrax/app/src/components/day.rs | 67 ++++++++++++++--- .../app/src/components/time_distance.rs | 8 +- fitnesstrax/app/src/view_models/day_detail.rs | 75 +++++++++++++------ fitnesstrax/core/src/types.rs | 23 ++++++ 4 files changed, 139 insertions(+), 34 deletions(-) diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index e2dc78c..b0b65ed 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -23,13 +23,13 @@ use crate::{ }, view_models::DayDetailViewModel, }; -use dimensioned::si; +use emseries::Record; use ft_core::{RecordType, TraxRecord}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use std::cell::RefCell; +use std::{cell::RefCell, rc::Rc}; -use super::time_distance_detail; +use super::{time_distance::TimeDistanceEdit, time_distance_detail}; pub struct DaySummaryPrivate { date: gtk::Label, @@ -198,12 +198,19 @@ impl DayDetail { pub struct DayEditPrivate { on_finished: RefCell>, + workout_rows: RefCell, } impl Default for DayEditPrivate { fn default() -> Self { Self { on_finished: RefCell::new(Box::new(|| {})), + workout_rows: RefCell::new( + gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .hexpand(true) + .build(), + ), } } } @@ -233,9 +240,15 @@ impl DayEdit { s.set_hexpand(true); *s.imp().on_finished.borrow_mut() = Box::new(on_finished); + let workout_buttons = workout_buttons(view_model.clone(), { + let s = s.clone(); + move |workout| s.add_row(workout) + }); + s.append(&control_buttons(&s, &view_model)); s.append(&weight_and_steps_row(&view_model)); - s.append(&workout_buttons()); + s.append(&*s.imp().workout_rows.borrow()); + s.append(&workout_buttons); s } @@ -243,6 +256,24 @@ impl DayEdit { fn finish(&self) { (self.imp().on_finished.borrow())() } + + fn add_row(&self, workout: Record) { + println!("add_row: {:?}", workout); + let workout_rows = self.imp().workout_rows.borrow(); + + 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, |_, _| {})) + } + _ => {} + } + } } fn control_buttons(s: &DayEdit, view_model: &DayDetailViewModel) -> ActionGroup { @@ -291,24 +322,37 @@ fn weight_and_steps_row(view_model: &DayDetailViewModel) -> gtk::Box { row } -fn workout_buttons() -> gtk::Box { - let sunrise_button = gtk::Button::builder() - .icon_name("daytime-sunrise-symbolic") - .width_request(64) - .height_request(64) - .build(); - +fn workout_buttons(view_model: DayDetailViewModel, add_row: AddRow) -> gtk::Box +where + AddRow: Fn(Record) + 'static, +{ + let add_row = Rc::new(add_row); let walking_button = gtk::Button::builder() .icon_name("walking2-symbolic") .width_request(64) .height_request(64) .build(); + walking_button.connect_clicked({ + let view_model = view_model.clone(); + let add_row = add_row.clone(); + move |_| { + let workout = view_model.new_record(RecordType::Walk); + &add_row(workout); + } + }); let running_button = gtk::Button::builder() .icon_name("running-symbolic") .width_request(64) .height_request(64) .build(); + running_button.connect_clicked({ + let view_model = view_model.clone(); + move |_| { + let workout = view_model.new_record(RecordType::Walk); + add_row(workout); + } + }); let layout = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) @@ -316,7 +360,6 @@ fn workout_buttons() -> gtk::Box { let row = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .build(); - row.append(&sunrise_button); row.append(&walking_button); row.append(&running_button); layout.append(&row); diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs index f7fe9d7..c7297b9 100644 --- a/fitnesstrax/app/src/components/time_distance.rs +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -132,6 +132,7 @@ impl Default for TimeDistanceEdit { fn default() -> Self { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Horizontal); + s.set_hexpand(true); s.set_css_classes(&["time-distance-edit"]); s @@ -139,12 +140,17 @@ impl Default for TimeDistanceEdit { } impl TimeDistanceEdit { - fn empty(on_update: OnUpdate) -> Self + pub fn new(type_: RecordType, record: TimeDistance, on_update: OnUpdate) -> Self where OnUpdate: Fn(&ft_core::RecordType, &ft_core::TimeDistance), { + println!("new TimeDistanceEdit"); let s = Self::default(); + s.append(>k::Label::new(Some( + record.datetime.format("%H:%M").to_string().as_ref(), + ))); + s } diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs index e69cb85..6bd2d82 100644 --- a/fitnesstrax/app/src/view_models/day_detail.rs +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with Fit use crate::app::App; use dimensioned::si; use emseries::{Record, RecordId, Recordable}; -use ft_core::{TimeDistance, TraxRecord}; +use ft_core::{RecordType, TimeDistance, TraxRecord}; use std::{ collections::HashMap, ops::Deref, @@ -27,7 +27,7 @@ use std::{ #[derive(Clone, Debug)] enum RecordState { Original(Record), - New(T), + New(Record), Updated(Record), #[allow(unused)] Deleted(Record), @@ -53,13 +53,21 @@ impl RecordState { } } - fn with_value(self, value: T) -> RecordState { - match self { - RecordState::Original(r) => RecordState::Updated(Record { data: value, ..r }), - RecordState::New(_) => RecordState::New(value), - RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..r }), - RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..r }), - } + 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::Updated(r) => RecordState::Updated(Record { data: value, ..*r }), + RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..*r }), + }; + } + + fn with_value(mut self, value: T) -> RecordState { + self.set_value(value); + self } #[allow(unused)] @@ -78,7 +86,7 @@ impl Deref for RecordState { fn deref(&self) -> &Self::Target { match self { RecordState::Original(ref r) => &r.data, - RecordState::New(ref r) => r, + RecordState::New(ref r) => &r.data, RecordState::Updated(ref r) => &r.data, RecordState::Deleted(ref r) => &r.data, } @@ -126,9 +134,12 @@ impl DayDetailViewModel { date: self.date, weight: new_weight, }), - None => RecordState::New(ft_core::Weight { - date: self.date, - weight: new_weight, + None => RecordState::New(Record { + id: RecordId::default(), + data: ft_core::Weight { + date: self.date, + weight: new_weight, + }, }), }; *record = Some(new_record); @@ -145,9 +156,12 @@ impl DayDetailViewModel { date: self.date, count: new_count, }), - None => RecordState::New(ft_core::Steps { - date: self.date, - count: new_count, + None => RecordState::New(Record { + id: RecordId::default(), + data: ft_core::Steps { + date: self.date, + count: new_count, + }, }), }; *record = Some(new_record); @@ -177,6 +191,25 @@ impl DayDetailViewModel { ) } + pub fn new_record(&self, type_: RecordType) -> Record { + let new_record = Record { + id: RecordId::default(), + data: ft_core::TraxRecord::new(type_, chrono::Local::now().into()), + }; + self.records + .write() + .unwrap() + .insert(new_record.id.clone(), RecordState::New(new_record.clone())); + new_record + } + + pub fn update_record(&self, update: Record) { + let mut records = self.records.write().unwrap(); + records + .entry(update.id) + .and_modify(|mut record| record.set_value(update.data)); + } + pub fn records(&self) -> Vec> { let read_lock = self.records.read().unwrap(); read_lock @@ -194,8 +227,8 @@ impl DayDetailViewModel { if let Some(app) = s.app { let weight_record = s.weight.read().unwrap().clone(); match weight_record { - Some(RecordState::New(weight)) => { - let _ = app.put_record(TraxRecord::Weight(weight)).await; + Some(RecordState::New(Record { data, .. })) => { + let _ = app.put_record(TraxRecord::Weight(data)).await; } Some(RecordState::Original(_)) => {} Some(RecordState::Updated(weight)) => { @@ -212,8 +245,8 @@ impl DayDetailViewModel { let steps_record = s.steps.read().unwrap().clone(); match steps_record { - Some(RecordState::New(steps)) => { - let _ = app.put_record(TraxRecord::Steps(steps)).await; + Some(RecordState::New(Record { data, .. })) => { + let _ = app.put_record(TraxRecord::Steps(data)).await; } Some(RecordState::Original(_)) => {} Some(RecordState::Updated(steps)) => { @@ -238,7 +271,7 @@ impl DayDetailViewModel { for record in records { match record { - RecordState::New(data) => { + RecordState::New(Record { data, .. }) => { let _ = app.put_record(data).await; } RecordState::Original(_) => {} diff --git a/fitnesstrax/core/src/types.rs b/fitnesstrax/core/src/types.rs index e61959e..135c89e 100644 --- a/fitnesstrax/core/src/types.rs +++ b/fitnesstrax/core/src/types.rs @@ -57,6 +57,17 @@ pub struct TimeDistance { pub comments: Option, } +impl TimeDistance { + pub fn new(time: DateTime) -> Self { + Self { + datetime: time, + distance: None, + duration: None, + comments: None, + } + } +} + /// A singular daily weight measurement. Weight changes slowly enough that it seems unlikely to /// need to track more than a single weight in a day. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -99,6 +110,18 @@ pub enum TraxRecord { } impl TraxRecord { + pub fn new(type_: RecordType, time: DateTime) -> TraxRecord { + match type_ { + RecordType::BikeRide => TraxRecord::BikeRide(TimeDistance::new(time)), + RecordType::Row => TraxRecord::Row(TimeDistance::new(time)), + RecordType::Run => TraxRecord::Run(TimeDistance::new(time)), + RecordType::Steps => unimplemented!(), + RecordType::Swim => unimplemented!(), + RecordType::Walk => TraxRecord::Walk(TimeDistance::new(time)), + RecordType::Weight => unimplemented!(), + } + } + pub fn workout_type(&self) -> RecordType { match self { TraxRecord::BikeRide(_) => RecordType::BikeRide,