From 2c42c35dfe1f8e3cd63941a10aa50e68ed41da53 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. --- emseries/src/series.rs | 15 ++- emseries/src/types.rs | 11 +++ fitnesstrax/app/src/components/day.rs | 54 ++++++++--- .../app/src/components/time_distance.rs | 14 ++- fitnesstrax/app/src/view_models/day_detail.rs | 91 +++++++++---------- fitnesstrax/core/src/types.rs | 12 --- 6 files changed, 114 insertions(+), 83 deletions(-) diff --git a/emseries/src/series.rs b/emseries/src/series.rs index 310b3f7..0e6441b 100644 --- a/emseries/src/series.rs +++ b/emseries/src/series.rs @@ -110,7 +110,7 @@ where .map_err(EmseriesReadError::JSONParseError) .and_then(Record::try_from) { - Ok(record) => records.insert(record.id.clone(), record.clone()), + Ok(record) => records.insert(record.id, record.clone()), Err(EmseriesReadError::RecordDeleted(id)) => records.remove(&id), Err(err) => return Err(err), }; @@ -124,19 +124,16 @@ where /// Put a new record into the database. A unique id will be assigned to the record and /// returned. pub fn put(&mut self, entry: T) -> Result { - let uuid = RecordId::default(); - let record = Record { - id: uuid.clone(), - data: entry, - }; + let id = RecordId::default(); + let record = Record { id, data: entry }; self.update(record)?; - Ok(uuid) + Ok(id) } /// Update an existing record. The [RecordId] of the record passed into this function must match /// the [RecordId] of a record already in the database. pub fn update(&mut self, record: Record) -> Result<(), EmseriesWriteError> { - self.records.insert(record.id.clone(), record.clone()); + self.records.insert(record.id, record.clone()); let write_res = match serde_json::to_string(&RecordOnDisk { id: record.id, data: Some(record.data), @@ -166,7 +163,7 @@ where self.records.remove(uuid); let rec: RecordOnDisk = RecordOnDisk { - id: uuid.clone(), + id: *uuid, data: None, }; match serde_json::to_string(&rec) { diff --git a/emseries/src/types.rs b/emseries/src/types.rs index a094830..e10298c 100644 --- a/emseries/src/types.rs +++ b/emseries/src/types.rs @@ -166,6 +166,17 @@ impl Record { pub fn timestamp(&self) -> Timestamp { self.data.timestamp() } + + pub fn map(self, map: Map) -> Record + where + Map: Fn(T) -> U, + U: Clone + Recordable, + { + Record { + id: self.id, + data: map(self.data), + } + } } #[cfg(test)] diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 0edcd5d..b024912 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -23,12 +23,13 @@ use crate::{ types::WeightFormatter, view_models::DayDetailViewModel, }; -use ft_core::TimeDistanceActivity; +use emseries::Record; +use ft_core::{TimeDistanceActivity, 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, @@ -237,9 +238,15 @@ impl DayEdit { *s.imp().on_finished.borrow_mut() = Box::new(on_finished); *s.imp().view_model.borrow_mut() = Some(view_model.clone()); + 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 } @@ -247,6 +254,17 @@ 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(); + + #[allow(clippy::single_match)] + match workout.data { + TraxRecord::TimeDistance(r) => workout_rows.append(&TimeDistanceEdit::new(r, |_| {})), + _ => {} + } + } } fn control_buttons(s: &DayEdit, view_model: &DayDetailViewModel) -> ActionGroup { @@ -303,24 +321,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_time_distance(TimeDistanceActivity::Walking); + add_row(workout.map(TraxRecord::TimeDistance)); + } + }); 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_time_distance(TimeDistanceActivity::Running); + add_row(workout.map(TraxRecord::TimeDistance)); + } + }); let layout = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) @@ -328,7 +359,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 3771556..bda7248 100644 --- a/fitnesstrax/app/src/components/time_distance.rs +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with Fit // use chrono::{Local, NaiveDate}; // use dimensioned::si; use dimensioned::si; +use ft_core::TimeDistance; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::cell::RefCell; @@ -130,6 +131,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 @@ -137,12 +139,18 @@ impl Default for TimeDistanceEdit { } impl TimeDistanceEdit { - #[allow(unused)] - fn empty(_on_update: OnUpdate) -> Self + pub fn new(record: TimeDistance, _on_update: OnUpdate) -> Self where OnUpdate: Fn(&ft_core::TimeDistance), { - Self::default() + 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 2751abe..7339168 100644 --- a/fitnesstrax/app/src/view_models/day_detail.rs +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -30,7 +30,7 @@ use std::{ #[derive(Clone, Debug)] enum RecordState { Original(Record), - New(T), + New(Record), Updated(Record), Deleted(Record), } @@ -57,19 +57,13 @@ impl RecordState { fn set_value(&mut self, value: T) { *self = match self { - RecordState::Original(r) => RecordState::Updated(Record { - id: r.id.clone(), - data: value, - }), - RecordState::New(_) => RecordState::New(value), - RecordState::Updated(r) => RecordState::Updated(Record { - id: r.id.clone(), - data: value, - }), - RecordState::Deleted(r) => RecordState::Updated(Record { - id: r.id.clone(), + 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 }), }; } @@ -94,7 +88,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, } @@ -105,7 +99,7 @@ impl std::ops::DerefMut for RecordState { fn deref_mut(&mut self) -> &mut Self::Target { match self { RecordState::Original(ref mut r) => &mut r.data, - RecordState::New(ref mut r) => r, + RecordState::New(ref mut r) => &mut r.data, RecordState::Updated(ref mut r) => &mut r.data, RecordState::Deleted(ref mut r) => &mut r.data, } @@ -149,7 +143,7 @@ impl DayDetailViewModel { weight_records .first() .and_then(|r| match r.data { - TraxRecord::Weight(ref w) => Some((r.id.clone(), w.clone())), + TraxRecord::Weight(ref w) => Some((r.id, w.clone())), _ => None, }) .map(|(id, w)| RecordState::Original(Record { id, data: w })), @@ -158,7 +152,7 @@ impl DayDetailViewModel { step_records .first() .and_then(|r| match r.data { - TraxRecord::Steps(ref w) => Some((r.id.clone(), w.clone())), + TraxRecord::Steps(ref w) => Some((r.id, w.clone())), _ => None, }) .map(|(id, w)| RecordState::Original(Record { id, data: w })), @@ -167,7 +161,7 @@ impl DayDetailViewModel { records: Arc::new(RwLock::new( records .into_iter() - .map(|r| (r.id.clone(), RecordState::Original(r))) + .map(|r| (r.id, RecordState::Original(r))) .collect::>>(), )), }) @@ -184,9 +178,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); @@ -203,9 +200,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); @@ -224,7 +224,7 @@ impl DayDetailViewModel { self.records .write() .unwrap() - .insert(id.clone(), RecordState::New(tr)); + .insert(id, RecordState::New(Record { id, data: tr })); println!( "records after new_time_distance: {:?}", self.records.read().unwrap() @@ -233,11 +233,10 @@ impl DayDetailViewModel { } pub fn update_time_distance(&self, workout: Record) { - let id = workout.id.clone(); let data = workout.data.clone(); let mut record_set = self.records.write().unwrap(); - record_set.entry(id).and_modify(|record_state| { + record_set.entry(workout.id).and_modify(|record_state| { record_state.set_value(TraxRecord::TimeDistance(data)); }); } @@ -250,7 +249,7 @@ impl DayDetailViewModel { .filter(|(_, record)| record.exists()) .filter_map(|(id, record_state)| match **record_state { TraxRecord::TimeDistance(ref workout) => Some(Record { - id: id.clone(), + id: *id, data: workout.clone(), }), _ => None, @@ -283,7 +282,7 @@ impl DayDetailViewModel { fn get_record(&self, id: &RecordId) -> Option> { let record_set = self.records.read().unwrap(); record_set.get(id).map(|record| Record { - id: id.clone(), + id: *id, data: (**record).clone(), }) } @@ -303,18 +302,19 @@ impl DayDetailViewModel { } pub fn save(&self) { - glib::spawn_future({ - let s = self.clone(); - async move { s.async_save().await } - }); + let s = self.clone(); + + glib::spawn_future(async move { s.async_save().await }); } pub async fn async_save(&self) { - println!("async_save"); let weight_record = self.weight.read().unwrap().clone(); match weight_record { Some(RecordState::New(data)) => { - let _ = self.provider.put_record(TraxRecord::Weight(data)).await; + let _ = self + .provider + .put_record(TraxRecord::Weight(data.data)) + .await; } Some(RecordState::Original(_)) => {} Some(RecordState::Updated(weight)) => { @@ -333,7 +333,7 @@ impl DayDetailViewModel { let steps_record = self.steps.read().unwrap().clone(); match steps_record { Some(RecordState::New(data)) => { - let _ = self.provider.put_record(TraxRecord::Steps(data)).await; + let _ = self.provider.put_record(TraxRecord::Steps(data.data)).await; } Some(RecordState::Original(_)) => {} Some(RecordState::Updated(steps)) => { @@ -361,7 +361,7 @@ impl DayDetailViewModel { println!("saving record: {:?}", record); match record { RecordState::New(data) => { - let _ = self.provider.put_record(data).await; + let _ = self.provider.put_record(data.data).await; } RecordState::Original(_) => {} RecordState::Updated(r) => { @@ -389,7 +389,7 @@ impl DayDetailViewModel { *self.weight.write().unwrap() = weight_records .first() .and_then(|r| match r.data { - TraxRecord::Weight(ref w) => Some((r.id.clone(), w.clone())), + TraxRecord::Weight(ref w) => Some((r.id, w.clone())), _ => None, }) .map(|(id, w)| RecordState::Original(Record { id, data: w })); @@ -397,14 +397,14 @@ impl DayDetailViewModel { *self.steps.write().unwrap() = step_records .first() .and_then(|r| match r.data { - TraxRecord::Steps(ref w) => Some((r.id.clone(), w.clone())), + TraxRecord::Steps(ref w) => Some((r.id, w.clone())), _ => None, }) .map(|(id, w)| RecordState::Original(Record { id, data: w })); *self.records.write().unwrap() = records .into_iter() - .map(|r| (r.id.clone(), RecordState::Original(r))) + .map(|r| (r.id, RecordState::Original(r))) .collect::>>(); } } @@ -430,7 +430,7 @@ mod test { fn new(records: Vec>) -> Self { let record_map = records .into_iter() - .map(|r| (r.id.clone(), r)) + .map(|r| (r.id, r)) .collect::>>(); Self { records: Arc::new(RwLock::new(record_map)), @@ -464,26 +464,23 @@ mod test { async fn put_record(&self, record: TraxRecord) -> Result { let id = RecordId::default(); let record = Record { - id: id.clone(), + id: id, data: record, }; self.put_records.write().unwrap().push(record.clone()); - self.records.write().unwrap().insert(id.clone(), record); + self.records.write().unwrap().insert(id, record); Ok(id) } async fn update_record(&self, record: Record) -> Result<(), WriteError> { println!("updated record: {:?}", record); self.updated_records.write().unwrap().push(record.clone()); - self.records - .write() - .unwrap() - .insert(record.id.clone(), record); + self.records.write().unwrap().insert(record.id, record); Ok(()) } async fn delete_record(&self, id: RecordId) -> Result<(), WriteError> { - self.deleted_records.write().unwrap().push(id.clone()); + self.deleted_records.write().unwrap().push(id); let _ = self.records.write().unwrap().remove(&id); Ok(()) } diff --git a/fitnesstrax/core/src/types.rs b/fitnesstrax/core/src/types.rs index c50d4eb..a48da32 100644 --- a/fitnesstrax/core/src/types.rs +++ b/fitnesstrax/core/src/types.rs @@ -134,18 +134,6 @@ impl TraxRecord { }) ) } - - /* - pub fn is_time_distance_type(&self, type_: TimeDistanceActivity) -> bool { - match type_ { - TimeDistanceWorkoutType::BikeRide => matches!(self, TraxRecord::BikeRide(_)), - TimeDistanceWorkoutType::Row => matches!(self, TraxRecord::Row(_)), - TimeDistanceWorkoutType::Run => matches!(self, TraxRecord::Run(_)), - TimeDistanceWorkoutType::Swim => matches!(self, TraxRecord::Swim(_)), - TimeDistanceWorkoutType::Walk => matches!(self, TraxRecord::Walk(_)), - } - } - */ } impl Recordable for TraxRecord {