Compare commits

..

2 Commits

Author SHA1 Message Date
Savanni D'Gerinel 39acfe7950 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.
2024-01-27 10:37:18 -05:00
Savanni D'Gerinel d0cce4ee58 Make emseries::Record copyable 2024-01-27 10:36:11 -05:00
5 changed files with 140 additions and 35 deletions

View File

@ -120,7 +120,7 @@ pub trait Recordable {
/// Uniquely identifies a record. /// Uniquely identifies a record.
/// ///
/// This is a wrapper around a basic uuid with some extra convenience methods. /// This is a wrapper around a basic uuid with some extra convenience methods.
#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
pub struct RecordId(Uuid); pub struct RecordId(Uuid);
impl Default for RecordId { impl Default for RecordId {

View File

@ -23,13 +23,13 @@ use crate::{
}, },
view_models::DayDetailViewModel, view_models::DayDetailViewModel,
}; };
use dimensioned::si; use emseries::Record;
use ft_core::{RecordType, TraxRecord}; use ft_core::{RecordType, 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,
@ -198,12 +198,19 @@ impl DayDetail {
pub struct DayEditPrivate { pub struct DayEditPrivate {
on_finished: RefCell<Box<dyn Fn()>>, on_finished: RefCell<Box<dyn Fn()>>,
workout_rows: RefCell<gtk::Box>,
} }
impl Default for DayEditPrivate { impl Default for DayEditPrivate {
fn default() -> Self { fn default() -> Self {
Self { Self {
on_finished: RefCell::new(Box::new(|| {})), 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.set_hexpand(true);
*s.imp().on_finished.borrow_mut() = Box::new(on_finished); *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(&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
} }
@ -243,6 +256,24 @@ 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();
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 { fn control_buttons(s: &DayEdit, view_model: &DayDetailViewModel) -> ActionGroup {
@ -291,24 +322,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_record(RecordType::Walk);
&add_row(workout);
}
});
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_record(RecordType::Walk);
add_row(workout);
}
});
let layout = gtk::Box::builder() let layout = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical) .orientation(gtk::Orientation::Vertical)
@ -316,7 +360,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

@ -132,6 +132,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
@ -139,12 +140,17 @@ impl Default for TimeDistanceEdit {
} }
impl TimeDistanceEdit { impl TimeDistanceEdit {
fn empty<OnUpdate>(on_update: OnUpdate) -> Self pub fn new<OnUpdate>(type_: RecordType, record: TimeDistance, on_update: OnUpdate) -> Self
where where
OnUpdate: Fn(&ft_core::RecordType, &ft_core::TimeDistance), OnUpdate: Fn(&ft_core::RecordType, &ft_core::TimeDistance),
{ {
println!("new TimeDistanceEdit");
let s = Self::default(); let s = Self::default();
s.append(&gtk::Label::new(Some(
record.datetime.format("%H:%M").to_string().as_ref(),
)));
s s
} }

View File

@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with Fit
use crate::app::App; use crate::app::App;
use dimensioned::si; use dimensioned::si;
use emseries::{Record, RecordId, Recordable}; use emseries::{Record, RecordId, Recordable};
use ft_core::{TimeDistance, TraxRecord}; use ft_core::{RecordType, TimeDistance, TraxRecord};
use std::{ use std::{
collections::HashMap, collections::HashMap,
ops::Deref, ops::Deref,
@ -27,7 +27,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>),
#[allow(unused)] #[allow(unused)]
Deleted(Record<T>), Deleted(Record<T>),
@ -53,13 +53,21 @@ impl<T: Clone + emseries::Recordable> RecordState<T> {
} }
} }
fn with_value(self, value: T) -> RecordState<T> { fn set_value(&mut self, value: T) {
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(value), RecordState::New(_) => RecordState::New(Record {
RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..r }), id: RecordId::default(),
RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..r }), 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<T> {
self.set_value(value);
self
} }
#[allow(unused)] #[allow(unused)]
@ -78,7 +86,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,
} }
@ -126,9 +134,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 {
id: RecordId::default(),
data: ft_core::Weight {
date: self.date, date: self.date,
weight: new_weight, weight: new_weight,
},
}), }),
}; };
*record = Some(new_record); *record = Some(new_record);
@ -145,9 +156,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 {
id: RecordId::default(),
data: ft_core::Steps {
date: self.date, date: self.date,
count: new_count, count: new_count,
},
}), }),
}; };
*record = Some(new_record); *record = Some(new_record);
@ -177,6 +191,25 @@ impl DayDetailViewModel {
) )
} }
pub fn new_record(&self, type_: RecordType) -> Record<TraxRecord> {
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<TraxRecord>) {
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<Record<TraxRecord>> { pub fn records(&self) -> Vec<Record<TraxRecord>> {
let read_lock = self.records.read().unwrap(); let read_lock = self.records.read().unwrap();
read_lock read_lock
@ -194,8 +227,8 @@ impl DayDetailViewModel {
if let Some(app) = s.app { if let Some(app) = s.app {
let weight_record = s.weight.read().unwrap().clone(); let weight_record = s.weight.read().unwrap().clone();
match weight_record { match weight_record {
Some(RecordState::New(weight)) => { Some(RecordState::New(Record { data, .. })) => {
let _ = app.put_record(TraxRecord::Weight(weight)).await; let _ = app.put_record(TraxRecord::Weight(data)).await;
} }
Some(RecordState::Original(_)) => {} Some(RecordState::Original(_)) => {}
Some(RecordState::Updated(weight)) => { Some(RecordState::Updated(weight)) => {
@ -212,8 +245,8 @@ impl DayDetailViewModel {
let steps_record = s.steps.read().unwrap().clone(); let steps_record = s.steps.read().unwrap().clone();
match steps_record { match steps_record {
Some(RecordState::New(steps)) => { Some(RecordState::New(Record { data, .. })) => {
let _ = app.put_record(TraxRecord::Steps(steps)).await; let _ = app.put_record(TraxRecord::Steps(data)).await;
} }
Some(RecordState::Original(_)) => {} Some(RecordState::Original(_)) => {}
Some(RecordState::Updated(steps)) => { Some(RecordState::Updated(steps)) => {
@ -238,7 +271,7 @@ impl DayDetailViewModel {
for record in records { for record in records {
match record { match record {
RecordState::New(data) => { RecordState::New(Record { data, .. }) => {
let _ = app.put_record(data).await; let _ = app.put_record(data).await;
} }
RecordState::Original(_) => {} RecordState::Original(_) => {}

View File

@ -57,6 +57,17 @@ pub struct TimeDistance {
pub comments: Option<String>, pub comments: Option<String>,
} }
impl TimeDistance {
pub fn new(time: DateTime<FixedOffset>) -> Self {
Self {
datetime: time,
distance: None,
duration: None,
comments: None,
}
}
}
/// A singular daily weight measurement. Weight changes slowly enough that it seems unlikely to /// 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. /// need to track more than a single weight in a day.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -99,6 +110,18 @@ pub enum TraxRecord {
} }
impl TraxRecord { impl TraxRecord {
pub fn new(type_: RecordType, time: DateTime<FixedOffset>) -> 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 { pub fn workout_type(&self) -> RecordType {
match self { match self {
TraxRecord::BikeRide(_) => RecordType::BikeRide, TraxRecord::BikeRide(_) => RecordType::BikeRide,