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.
This commit is contained in:
Savanni D'Gerinel 2024-01-27 10:37:18 -05:00
parent afe693fe10
commit 2c42c35dfe
6 changed files with 114 additions and 83 deletions

View File

@ -110,7 +110,7 @@ where
.map_err(EmseriesReadError::JSONParseError) .map_err(EmseriesReadError::JSONParseError)
.and_then(Record::try_from) .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(EmseriesReadError::RecordDeleted(id)) => records.remove(&id),
Err(err) => return Err(err), 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 /// Put a new record into the database. A unique id will be assigned to the record and
/// returned. /// returned.
pub fn put(&mut self, entry: T) -> Result<RecordId, EmseriesWriteError> { pub fn put(&mut self, entry: T) -> Result<RecordId, EmseriesWriteError> {
let uuid = RecordId::default(); let id = RecordId::default();
let record = Record { let record = Record { id, data: entry };
id: uuid.clone(),
data: entry,
};
self.update(record)?; self.update(record)?;
Ok(uuid) Ok(id)
} }
/// Update an existing record. The [RecordId] of the record passed into this function must match /// Update an existing record. The [RecordId] of the record passed into this function must match
/// the [RecordId] of a record already in the database. /// the [RecordId] of a record already in the database.
pub fn update(&mut self, record: Record<T>) -> Result<(), EmseriesWriteError> { pub fn update(&mut self, record: Record<T>) -> 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 { let write_res = match serde_json::to_string(&RecordOnDisk {
id: record.id, id: record.id,
data: Some(record.data), data: Some(record.data),
@ -166,7 +163,7 @@ where
self.records.remove(uuid); self.records.remove(uuid);
let rec: RecordOnDisk<T> = RecordOnDisk { let rec: RecordOnDisk<T> = RecordOnDisk {
id: uuid.clone(), id: *uuid,
data: None, data: None,
}; };
match serde_json::to_string(&rec) { match serde_json::to_string(&rec) {

View File

@ -166,6 +166,17 @@ impl<T: Clone + Recordable> Record<T> {
pub fn timestamp(&self) -> Timestamp { pub fn timestamp(&self) -> Timestamp {
self.data.timestamp() self.data.timestamp()
} }
pub fn map<Map, U>(self, map: Map) -> Record<U>
where
Map: Fn(T) -> U,
U: Clone + Recordable,
{
Record {
id: self.id,
data: map(self.data),
}
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -23,12 +23,13 @@ use crate::{
types::WeightFormatter, types::WeightFormatter,
view_models::DayDetailViewModel, view_models::DayDetailViewModel,
}; };
use ft_core::TimeDistanceActivity; use emseries::Record;
use ft_core::{TimeDistanceActivity, TraxRecord};
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};
use super::time_distance_detail; use super::{time_distance::TimeDistanceEdit, time_distance_detail};
pub struct DaySummaryPrivate { pub struct DaySummaryPrivate {
date: gtk::Label, date: gtk::Label,
@ -237,9 +238,15 @@ impl DayEdit {
*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() = Some(view_model.clone()); *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(&control_buttons(&s, &view_model));
s.append(&weight_and_steps_row(&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 s
} }
@ -247,6 +254,17 @@ impl DayEdit {
fn finish(&self) { fn finish(&self) {
(self.imp().on_finished.borrow())() (self.imp().on_finished.borrow())()
} }
fn add_row(&self, workout: Record<TraxRecord>) {
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 { fn control_buttons(s: &DayEdit, view_model: &DayDetailViewModel) -> ActionGroup {
@ -303,24 +321,37 @@ fn weight_and_steps_row(view_model: &DayDetailViewModel) -> gtk::Box {
row row
} }
fn workout_buttons() -> gtk::Box { fn workout_buttons<AddRow>(view_model: DayDetailViewModel, add_row: AddRow) -> gtk::Box
let sunrise_button = gtk::Button::builder() where
.icon_name("daytime-sunrise-symbolic") AddRow: Fn(Record<TraxRecord>) + 'static,
.width_request(64) {
.height_request(64) let add_row = Rc::new(add_row);
.build();
let walking_button = gtk::Button::builder() let walking_button = gtk::Button::builder()
.icon_name("walking2-symbolic") .icon_name("walking2-symbolic")
.width_request(64) .width_request(64)
.height_request(64) .height_request(64)
.build(); .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() let running_button = gtk::Button::builder()
.icon_name("running-symbolic") .icon_name("running-symbolic")
.width_request(64) .width_request(64)
.height_request(64) .height_request(64)
.build(); .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() let layout = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical) .orientation(gtk::Orientation::Vertical)
@ -328,7 +359,6 @@ fn workout_buttons() -> gtk::Box {
let row = gtk::Box::builder() let row = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal) .orientation(gtk::Orientation::Horizontal)
.build(); .build();
row.append(&sunrise_button);
row.append(&walking_button); row.append(&walking_button);
row.append(&running_button); row.append(&running_button);
layout.append(&row); layout.append(&row);

View File

@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with Fit
// use chrono::{Local, NaiveDate}; // use chrono::{Local, NaiveDate};
// use dimensioned::si; // use dimensioned::si;
use dimensioned::si; use dimensioned::si;
use ft_core::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;
@ -130,6 +131,7 @@ 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::Horizontal);
s.set_hexpand(true);
s.set_css_classes(&["time-distance-edit"]); s.set_css_classes(&["time-distance-edit"]);
s s
@ -137,12 +139,18 @@ impl Default for TimeDistanceEdit {
} }
impl TimeDistanceEdit { impl TimeDistanceEdit {
#[allow(unused)] pub fn new<OnUpdate>(record: TimeDistance, _on_update: OnUpdate) -> Self
fn empty<OnUpdate>(_on_update: OnUpdate) -> Self
where where
OnUpdate: Fn(&ft_core::TimeDistance), OnUpdate: Fn(&ft_core::TimeDistance),
{ {
Self::default() println!("new TimeDistanceEdit");
let s = Self::default();
s.append(&gtk::Label::new(Some(
record.datetime.format("%H:%M").to_string().as_ref(),
)));
s
} }
/* /*

View File

@ -30,7 +30,7 @@ use std::{
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
enum RecordState<T: Clone + Recordable> { enum RecordState<T: Clone + Recordable> {
Original(Record<T>), Original(Record<T>),
New(T), New(Record<T>),
Updated(Record<T>), Updated(Record<T>),
Deleted(Record<T>), Deleted(Record<T>),
} }
@ -57,19 +57,13 @@ 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 { RecordState::Original(r) => RecordState::Updated(Record { data: value, ..*r }),
id: r.id.clone(), RecordState::New(_) => RecordState::New(Record {
data: value, id: RecordId::default(),
}),
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(),
data: value, 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<T: Clone + emseries::Recordable> Deref for RecordState<T> {
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
match self { match self {
RecordState::Original(ref r) => &r.data, RecordState::Original(ref r) => &r.data,
RecordState::New(ref r) => r, RecordState::New(ref r) => &r.data,
RecordState::Updated(ref r) => &r.data, RecordState::Updated(ref r) => &r.data,
RecordState::Deleted(ref r) => &r.data, RecordState::Deleted(ref r) => &r.data,
} }
@ -105,7 +99,7 @@ impl<T: Clone + emseries::Recordable> std::ops::DerefMut for RecordState<T> {
fn deref_mut(&mut self) -> &mut Self::Target { fn deref_mut(&mut self) -> &mut Self::Target {
match self { match self {
RecordState::Original(ref mut r) => &mut r.data, 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::Updated(ref mut r) => &mut r.data,
RecordState::Deleted(ref mut r) => &mut r.data, RecordState::Deleted(ref mut r) => &mut r.data,
} }
@ -149,7 +143,7 @@ impl DayDetailViewModel {
weight_records weight_records
.first() .first()
.and_then(|r| match r.data { .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, _ => None,
}) })
.map(|(id, w)| RecordState::Original(Record { id, data: w })), .map(|(id, w)| RecordState::Original(Record { id, data: w })),
@ -158,7 +152,7 @@ impl DayDetailViewModel {
step_records step_records
.first() .first()
.and_then(|r| match r.data { .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, _ => None,
}) })
.map(|(id, w)| RecordState::Original(Record { id, data: w })), .map(|(id, w)| RecordState::Original(Record { id, data: w })),
@ -167,7 +161,7 @@ impl DayDetailViewModel {
records: Arc::new(RwLock::new( records: Arc::new(RwLock::new(
records records
.into_iter() .into_iter()
.map(|r| (r.id.clone(), RecordState::Original(r))) .map(|r| (r.id, RecordState::Original(r)))
.collect::<HashMap<RecordId, RecordState<TraxRecord>>>(), .collect::<HashMap<RecordId, RecordState<TraxRecord>>>(),
)), )),
}) })
@ -184,9 +178,12 @@ impl DayDetailViewModel {
date: self.date, date: self.date,
weight: new_weight, weight: new_weight,
}), }),
None => RecordState::New(ft_core::Weight { None => RecordState::New(Record {
date: self.date, id: RecordId::default(),
weight: new_weight, data: ft_core::Weight {
date: self.date,
weight: new_weight,
},
}), }),
}; };
*record = Some(new_record); *record = Some(new_record);
@ -203,9 +200,12 @@ impl DayDetailViewModel {
date: self.date, date: self.date,
count: new_count, count: new_count,
}), }),
None => RecordState::New(ft_core::Steps { None => RecordState::New(Record {
date: self.date, id: RecordId::default(),
count: new_count, data: ft_core::Steps {
date: self.date,
count: new_count,
},
}), }),
}; };
*record = Some(new_record); *record = Some(new_record);
@ -224,7 +224,7 @@ impl DayDetailViewModel {
self.records self.records
.write() .write()
.unwrap() .unwrap()
.insert(id.clone(), RecordState::New(tr)); .insert(id, RecordState::New(Record { id, data: tr }));
println!( println!(
"records after new_time_distance: {:?}", "records after new_time_distance: {:?}",
self.records.read().unwrap() self.records.read().unwrap()
@ -233,11 +233,10 @@ impl DayDetailViewModel {
} }
pub fn update_time_distance(&self, workout: Record<TimeDistance>) { pub fn update_time_distance(&self, workout: Record<TimeDistance>) {
let id = workout.id.clone();
let data = workout.data.clone(); let data = workout.data.clone();
let mut record_set = self.records.write().unwrap(); 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)); record_state.set_value(TraxRecord::TimeDistance(data));
}); });
} }
@ -250,7 +249,7 @@ impl DayDetailViewModel {
.filter(|(_, record)| record.exists()) .filter(|(_, record)| record.exists())
.filter_map(|(id, record_state)| match **record_state { .filter_map(|(id, record_state)| match **record_state {
TraxRecord::TimeDistance(ref workout) => Some(Record { TraxRecord::TimeDistance(ref workout) => Some(Record {
id: id.clone(), id: *id,
data: workout.clone(), data: workout.clone(),
}), }),
_ => None, _ => None,
@ -283,7 +282,7 @@ impl DayDetailViewModel {
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();
record_set.get(id).map(|record| Record { record_set.get(id).map(|record| Record {
id: id.clone(), id: *id,
data: (**record).clone(), data: (**record).clone(),
}) })
} }
@ -303,18 +302,19 @@ impl DayDetailViewModel {
} }
pub fn save(&self) { pub fn save(&self) {
glib::spawn_future({ let s = self.clone();
let s = self.clone();
async move { s.async_save().await } glib::spawn_future(async move { s.async_save().await });
});
} }
pub async fn async_save(&self) { pub async fn async_save(&self) {
println!("async_save");
let weight_record = self.weight.read().unwrap().clone(); let weight_record = self.weight.read().unwrap().clone();
match weight_record { match weight_record {
Some(RecordState::New(data)) => { 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::Original(_)) => {}
Some(RecordState::Updated(weight)) => { Some(RecordState::Updated(weight)) => {
@ -333,7 +333,7 @@ impl DayDetailViewModel {
let steps_record = self.steps.read().unwrap().clone(); let steps_record = self.steps.read().unwrap().clone();
match steps_record { match steps_record {
Some(RecordState::New(data)) => { 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::Original(_)) => {}
Some(RecordState::Updated(steps)) => { Some(RecordState::Updated(steps)) => {
@ -361,7 +361,7 @@ impl DayDetailViewModel {
println!("saving record: {:?}", record); println!("saving record: {:?}", record);
match record { match record {
RecordState::New(data) => { RecordState::New(data) => {
let _ = self.provider.put_record(data).await; let _ = self.provider.put_record(data.data).await;
} }
RecordState::Original(_) => {} RecordState::Original(_) => {}
RecordState::Updated(r) => { RecordState::Updated(r) => {
@ -389,7 +389,7 @@ impl DayDetailViewModel {
*self.weight.write().unwrap() = weight_records *self.weight.write().unwrap() = weight_records
.first() .first()
.and_then(|r| match r.data { .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, _ => None,
}) })
.map(|(id, w)| RecordState::Original(Record { id, data: w })); .map(|(id, w)| RecordState::Original(Record { id, data: w }));
@ -397,14 +397,14 @@ impl DayDetailViewModel {
*self.steps.write().unwrap() = step_records *self.steps.write().unwrap() = step_records
.first() .first()
.and_then(|r| match r.data { .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, _ => None,
}) })
.map(|(id, w)| RecordState::Original(Record { id, data: w })); .map(|(id, w)| RecordState::Original(Record { id, data: w }));
*self.records.write().unwrap() = records *self.records.write().unwrap() = records
.into_iter() .into_iter()
.map(|r| (r.id.clone(), RecordState::Original(r))) .map(|r| (r.id, RecordState::Original(r)))
.collect::<HashMap<RecordId, RecordState<TraxRecord>>>(); .collect::<HashMap<RecordId, RecordState<TraxRecord>>>();
} }
} }
@ -430,7 +430,7 @@ mod test {
fn new(records: Vec<Record<TraxRecord>>) -> Self { fn new(records: Vec<Record<TraxRecord>>) -> Self {
let record_map = records let record_map = records
.into_iter() .into_iter()
.map(|r| (r.id.clone(), r)) .map(|r| (r.id, r))
.collect::<HashMap<RecordId, Record<TraxRecord>>>(); .collect::<HashMap<RecordId, Record<TraxRecord>>>();
Self { Self {
records: Arc::new(RwLock::new(record_map)), records: Arc::new(RwLock::new(record_map)),
@ -464,26 +464,23 @@ mod test {
async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError> { async fn put_record(&self, record: TraxRecord) -> Result<RecordId, WriteError> {
let id = RecordId::default(); let id = RecordId::default();
let record = Record { let record = Record {
id: id.clone(), id: id,
data: record, data: record,
}; };
self.put_records.write().unwrap().push(record.clone()); 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) Ok(id)
} }
async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError> { async fn update_record(&self, record: Record<TraxRecord>) -> Result<(), WriteError> {
println!("updated record: {:?}", record); println!("updated record: {:?}", record);
self.updated_records.write().unwrap().push(record.clone()); self.updated_records.write().unwrap().push(record.clone());
self.records self.records.write().unwrap().insert(record.id, record);
.write()
.unwrap()
.insert(record.id.clone(), record);
Ok(()) Ok(())
} }
async fn delete_record(&self, id: RecordId) -> Result<(), WriteError> { 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); let _ = self.records.write().unwrap().remove(&id);
Ok(()) Ok(())
} }

View File

@ -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 { impl Recordable for TraxRecord {