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,