Add the ability to edit the time of a workout and the associated activity. #183
|
@ -24,7 +24,7 @@ use crate::{
|
||||||
view_models::DayDetailViewModel,
|
view_models::DayDetailViewModel,
|
||||||
};
|
};
|
||||||
use emseries::{Record, RecordId};
|
use emseries::{Record, RecordId};
|
||||||
use ft_core::{TimeDistanceActivity, TraxRecord};
|
use ft_core::{TimeDistanceActivity, TraxRecord, TIME_DISTANCE_ACTIVITIES};
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
@ -105,12 +105,15 @@ impl DaySummary {
|
||||||
row.append(&label);
|
row.append(&label);
|
||||||
self.append(&row);
|
self.append(&row);
|
||||||
|
|
||||||
let biking_summary = view_model.time_distance_summary(TimeDistanceActivity::BikeRide);
|
for activity in TIME_DISTANCE_ACTIVITIES {
|
||||||
if let Some(label) = time_distance_summary(
|
let summary = view_model.time_distance_summary(activity);
|
||||||
DistanceFormatter::from(biking_summary.0),
|
if let Some(label) = time_distance_summary(
|
||||||
DurationFormatter::from(biking_summary.1),
|
activity,
|
||||||
) {
|
DistanceFormatter::from(summary.0),
|
||||||
self.append(&label);
|
DurationFormatter::from(summary.1),
|
||||||
|
) {
|
||||||
|
self.append(&label);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -383,7 +386,7 @@ where
|
||||||
biking_button.connect_clicked({
|
biking_button.connect_clicked({
|
||||||
let view_model = view_model.clone();
|
let view_model = view_model.clone();
|
||||||
move |_| {
|
move |_| {
|
||||||
let workout = view_model.new_time_distance(TimeDistanceActivity::BikeRide);
|
let workout = view_model.new_time_distance(TimeDistanceActivity::Biking);
|
||||||
add_row(workout.map(TraxRecord::TimeDistance));
|
add_row(workout.map(TraxRecord::TimeDistance));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,31 +14,38 @@ General Public License for more details.
|
||||||
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// use crate::components::{EditView, ParseError, TextEntry};
|
|
||||||
// use chrono::{Local, NaiveDate};
|
|
||||||
// use dimensioned::si;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{distance_field, duration_field, time_field},
|
components::{distance_field, duration_field, time_field},
|
||||||
types::{DistanceFormatter, DurationFormatter, FormatOption, TimeFormatter},
|
types::{DistanceFormatter, DurationFormatter, FormatOption, TimeFormatter},
|
||||||
};
|
};
|
||||||
use dimensioned::si;
|
use dimensioned::si;
|
||||||
use ft_core::{TimeDistance, TimeDistanceActivity};
|
use ft_core::{TimeDistance, TimeDistanceActivity, TIME_DISTANCE_ACTIVITIES};
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
use std::{rc::Rc, cell::RefCell};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
pub fn time_distance_summary(
|
pub fn time_distance_summary(
|
||||||
|
activity: TimeDistanceActivity,
|
||||||
distance: DistanceFormatter,
|
distance: DistanceFormatter,
|
||||||
duration: DurationFormatter,
|
duration: DurationFormatter,
|
||||||
) -> Option<gtk::Label> {
|
) -> Option<gtk::Label> {
|
||||||
let text = match (*distance > si::M, *duration > si::S) {
|
let text = match (*distance > si::M, *duration > si::S) {
|
||||||
(true, true) => Some(format!(
|
(true, true) => Some(format!(
|
||||||
"{} of biking in {}",
|
"{} of {:?} in {}",
|
||||||
distance.format(FormatOption::Full),
|
distance.format(FormatOption::Full),
|
||||||
|
activity,
|
||||||
duration.format(FormatOption::Full)
|
duration.format(FormatOption::Full)
|
||||||
)),
|
)),
|
||||||
(true, false) => Some(format!("{} of biking", distance.format(FormatOption::Full))),
|
(true, false) => Some(format!(
|
||||||
(false, true) => Some(format!("{} of biking", duration.format(FormatOption::Full))),
|
"{} of {:?}",
|
||||||
|
distance.format(FormatOption::Full),
|
||||||
|
activity
|
||||||
|
)),
|
||||||
|
(false, true) => Some(format!(
|
||||||
|
"{} of {:?}",
|
||||||
|
duration.format(FormatOption::Full),
|
||||||
|
activity
|
||||||
|
)),
|
||||||
(false, false) => None,
|
(false, false) => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -122,7 +129,7 @@ impl Default for TimeDistanceEditPrivate {
|
||||||
Self {
|
Self {
|
||||||
workout: RefCell::new(TimeDistance {
|
workout: RefCell::new(TimeDistance {
|
||||||
datetime: chrono::Utc::now().into(),
|
datetime: chrono::Utc::now().into(),
|
||||||
activity: TimeDistanceActivity::BikeRide,
|
activity: TimeDistanceActivity::Biking,
|
||||||
duration: None,
|
duration: None,
|
||||||
distance: None,
|
distance: None,
|
||||||
comments: None,
|
comments: None,
|
||||||
|
@ -182,6 +189,7 @@ impl TimeDistanceEdit {
|
||||||
)
|
)
|
||||||
.widget(),
|
.widget(),
|
||||||
);
|
);
|
||||||
|
details_row.append(&s.activity_menu(workout.activity));
|
||||||
details_row.append(
|
details_row.append(
|
||||||
&distance_field(workout.distance.map(DistanceFormatter::from), {
|
&distance_field(workout.distance.map(DistanceFormatter::from), {
|
||||||
let s = s.clone();
|
let s = s.clone();
|
||||||
|
@ -202,8 +210,26 @@ impl TimeDistanceEdit {
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_time(&self, _time: Option<TimeFormatter>) {
|
fn update_time(&self, time: Option<TimeFormatter>) {
|
||||||
unimplemented!()
|
if let Some(time_formatter) = time {
|
||||||
|
let mut workout = self.imp().workout.borrow_mut();
|
||||||
|
let tz = workout.datetime.timezone();
|
||||||
|
let new_time = workout
|
||||||
|
.datetime
|
||||||
|
.date_naive()
|
||||||
|
.and_time(*time_formatter)
|
||||||
|
.and_local_timezone(tz)
|
||||||
|
.unwrap()
|
||||||
|
.fixed_offset();
|
||||||
|
workout.datetime = new_time;
|
||||||
|
(self.imp().on_update.borrow())(workout.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_workout_type(&self, type_: TimeDistanceActivity) {
|
||||||
|
let mut workout = self.imp().workout.borrow_mut();
|
||||||
|
workout.activity = type_;
|
||||||
|
(self.imp().on_update.borrow())(workout.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_distance(&self, distance: Option<DistanceFormatter>) {
|
fn update_distance(&self, distance: Option<DistanceFormatter>) {
|
||||||
|
@ -217,4 +243,29 @@ impl TimeDistanceEdit {
|
||||||
workout.duration = duration.map(|d| *d);
|
workout.duration = duration.map(|d| *d);
|
||||||
(self.imp().on_update.borrow())(workout.clone());
|
(self.imp().on_update.borrow())(workout.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn activity_menu(&self, selected: TimeDistanceActivity) -> gtk::DropDown {
|
||||||
|
let options = TIME_DISTANCE_ACTIVITIES
|
||||||
|
.iter()
|
||||||
|
.map(|item| format!("{:?}", item))
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
let options = options.iter().map(|o| o.as_ref()).collect::<Vec<&str>>();
|
||||||
|
|
||||||
|
let selected_idx = TIME_DISTANCE_ACTIVITIES
|
||||||
|
.iter()
|
||||||
|
.position(|&v| v == selected)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let menu = gtk::DropDown::from_strings(&options);
|
||||||
|
menu.set_selected(selected_idx as u32);
|
||||||
|
menu.connect_selected_item_notify({
|
||||||
|
let s = self.clone();
|
||||||
|
move |menu| {
|
||||||
|
let new_item = TIME_DISTANCE_ACTIVITIES[menu.selected() as usize];
|
||||||
|
s.update_workout_type(new_item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
menu
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,9 +176,20 @@ impl DayDetailViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_time_distance(&self, activity: TimeDistanceActivity) -> Record<TimeDistance> {
|
pub fn new_time_distance(&self, activity: TimeDistanceActivity) -> Record<TimeDistance> {
|
||||||
|
let now = chrono::Local::now();
|
||||||
|
let base_time = now.time();
|
||||||
|
let tz = now.timezone();
|
||||||
|
let datetime = self
|
||||||
|
.date
|
||||||
|
.clone()
|
||||||
|
.and_time(base_time)
|
||||||
|
.and_local_timezone(tz)
|
||||||
|
.unwrap()
|
||||||
|
.into();
|
||||||
|
|
||||||
let id = RecordId::default();
|
let id = RecordId::default();
|
||||||
let workout = TimeDistance {
|
let workout = TimeDistance {
|
||||||
datetime: chrono::Local::now().into(),
|
datetime,
|
||||||
activity,
|
activity,
|
||||||
distance: None,
|
distance: None,
|
||||||
duration: None,
|
duration: None,
|
||||||
|
@ -499,7 +510,7 @@ mod test {
|
||||||
id: RecordId::default(),
|
id: RecordId::default(),
|
||||||
data: TraxRecord::TimeDistance(ft_core::TimeDistance {
|
data: TraxRecord::TimeDistance(ft_core::TimeDistance {
|
||||||
datetime: oct_13_am.clone(),
|
datetime: oct_13_am.clone(),
|
||||||
activity: TimeDistanceActivity::BikeRide,
|
activity: TimeDistanceActivity::Biking,
|
||||||
distance: Some(15000. * si::M),
|
distance: Some(15000. * si::M),
|
||||||
duration: Some(3600. * si::S),
|
duration: Some(3600. * si::S),
|
||||||
comments: Some("somecomments present".to_owned()),
|
comments: Some("somecomments present".to_owned()),
|
||||||
|
@ -549,11 +560,11 @@ mod test {
|
||||||
async fn it_can_construct_new_records() {
|
async fn it_can_construct_new_records() {
|
||||||
let (view_model, provider) = create_empty_view_model().await;
|
let (view_model, provider) = create_empty_view_model().await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
|
view_model.time_distance_summary(TimeDistanceActivity::Biking),
|
||||||
(0. * si::M, 0. * si::S)
|
(0. * si::M, 0. * si::S)
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide);
|
let mut record = view_model.new_time_distance(TimeDistanceActivity::Biking);
|
||||||
record.data.duration = Some(60. * si::S);
|
record.data.duration = Some(60. * si::S);
|
||||||
view_model.async_save().await;
|
view_model.async_save().await;
|
||||||
|
|
||||||
|
@ -566,17 +577,17 @@ mod test {
|
||||||
async fn it_can_update_a_new_record_before_saving() {
|
async fn it_can_update_a_new_record_before_saving() {
|
||||||
let (view_model, provider) = create_empty_view_model().await;
|
let (view_model, provider) = create_empty_view_model().await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
|
view_model.time_distance_summary(TimeDistanceActivity::Biking),
|
||||||
(0. * si::M, 0. * si::S)
|
(0. * si::M, 0. * si::S)
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide);
|
let mut record = view_model.new_time_distance(TimeDistanceActivity::Biking);
|
||||||
record.data.duration = Some(60. * si::S);
|
record.data.duration = Some(60. * si::S);
|
||||||
let record = record.map(TraxRecord::TimeDistance);
|
let record = record.map(TraxRecord::TimeDistance);
|
||||||
view_model.update_record(record.clone());
|
view_model.update_record(record.clone());
|
||||||
assert_eq!(view_model.get_record(&record.id), Some(record));
|
assert_eq!(view_model.get_record(&record.id), Some(record));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
|
view_model.time_distance_summary(TimeDistanceActivity::Biking),
|
||||||
(0. * si::M, 60. * si::S)
|
(0. * si::M, 60. * si::S)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -599,7 +610,7 @@ mod test {
|
||||||
view_model.update_record(workout.map(TraxRecord::TimeDistance));
|
view_model.update_record(workout.map(TraxRecord::TimeDistance));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
|
view_model.time_distance_summary(TimeDistanceActivity::Biking),
|
||||||
(15000. * si::M, 1800. * si::S)
|
(15000. * si::M, 1800. * si::S)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -614,11 +625,11 @@ mod test {
|
||||||
async fn it_can_remove_a_new_record() {
|
async fn it_can_remove_a_new_record() {
|
||||||
let (view_model, provider) = create_empty_view_model().await;
|
let (view_model, provider) = create_empty_view_model().await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
|
view_model.time_distance_summary(TimeDistanceActivity::Biking),
|
||||||
(0. * si::M, 0. * si::S)
|
(0. * si::M, 0. * si::S)
|
||||||
);
|
);
|
||||||
|
|
||||||
let record = view_model.new_time_distance(TimeDistanceActivity::BikeRide);
|
let record = view_model.new_time_distance(TimeDistanceActivity::Biking);
|
||||||
view_model.remove_record(record.id);
|
view_model.remove_record(record.id);
|
||||||
view_model.save();
|
view_model.save();
|
||||||
|
|
||||||
|
@ -634,7 +645,7 @@ mod test {
|
||||||
|
|
||||||
view_model.remove_record(workout.id);
|
view_model.remove_record(workout.id);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view_model.time_distance_summary(TimeDistanceActivity::BikeRide),
|
view_model.time_distance_summary(TimeDistanceActivity::Biking),
|
||||||
(0. * si::M, 0. * si::S)
|
(0. * si::M, 0. * si::S)
|
||||||
);
|
);
|
||||||
view_model.async_save().await;
|
view_model.async_save().await;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
mod legacy;
|
mod legacy;
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
pub use types::{Steps, TimeDistance, TimeDistanceActivity, TraxRecord, Weight};
|
pub use types::{
|
||||||
|
Steps, TimeDistance, TimeDistanceActivity, TraxRecord, Weight, TIME_DISTANCE_ACTIVITIES,
|
||||||
|
};
|
||||||
|
|
|
@ -1,3 +1,19 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023-2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||||
|
|
||||||
|
This file is part of FitnessTrax.
|
||||||
|
|
||||||
|
FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
||||||
|
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
use chrono::{DateTime, FixedOffset, NaiveDate};
|
use chrono::{DateTime, FixedOffset, NaiveDate};
|
||||||
use dimensioned::si;
|
use dimensioned::si;
|
||||||
use emseries::{Recordable, Timestamp};
|
use emseries::{Recordable, Timestamp};
|
||||||
|
@ -35,13 +51,21 @@ impl Recordable for Steps {
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum TimeDistanceActivity {
|
pub enum TimeDistanceActivity {
|
||||||
BikeRide,
|
Biking,
|
||||||
Running,
|
Running,
|
||||||
Rowing,
|
Rowing,
|
||||||
Swimming,
|
Swimming,
|
||||||
Walking,
|
Walking,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const TIME_DISTANCE_ACTIVITIES: [TimeDistanceActivity; 5] = [
|
||||||
|
TimeDistanceActivity::Biking,
|
||||||
|
TimeDistanceActivity::Rowing,
|
||||||
|
TimeDistanceActivity::Running,
|
||||||
|
TimeDistanceActivity::Swimming,
|
||||||
|
TimeDistanceActivity::Walking,
|
||||||
|
];
|
||||||
|
|
||||||
/// TimeDistance represents workouts characterized by a duration and a distance travelled. These
|
/// TimeDistance represents workouts characterized by a duration and a distance travelled. These
|
||||||
/// sorts of workouts can occur many times a day, depending on how one records things. I might
|
/// sorts of workouts can occur many times a day, depending on how one records things. I might
|
||||||
/// record a single 30-km workout if I go on a long-distanec ride. Or I might record multiple 5km
|
/// record a single 30-km workout if I go on a long-distanec ride. Or I might record multiple 5km
|
||||||
|
@ -117,7 +141,7 @@ impl TraxRecord {
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
self,
|
||||||
TraxRecord::TimeDistance(TimeDistance {
|
TraxRecord::TimeDistance(TimeDistance {
|
||||||
activity: TimeDistanceActivity::BikeRide,
|
activity: TimeDistanceActivity::Biking,
|
||||||
..
|
..
|
||||||
}) | TraxRecord::TimeDistance(TimeDistance {
|
}) | TraxRecord::TimeDistance(TimeDistance {
|
||||||
activity: TimeDistanceActivity::Running,
|
activity: TimeDistanceActivity::Running,
|
||||||
|
|
Loading…
Reference in New Issue