diff --git a/fitnesstrax/app/resources/style.css b/fitnesstrax/app/resources/style.css index 4028c61..2814994 100644 --- a/fitnesstrax/app/resources/style.css +++ b/fitnesstrax/app/resources/style.css @@ -32,3 +32,8 @@ margin: 4px; } +.weight-view { + padding: 8px; + margin: 8px; +} + diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 4c1c3db..23da6c2 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -16,7 +16,8 @@ You should have received a copy of the GNU General Public License along with Fit // use chrono::NaiveDate; // use ft_core::TraxRecord; -use ft_core::TraxRecord; +use dimensioned::si; +use ft_core::{RecordType, TimeDistance, TraxRecord, Weight}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::cell::RefCell; @@ -131,23 +132,234 @@ impl DayDetail { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); + let weight_record = records.iter().find_map(|record| match record { + TraxRecord::Weight(record) => Some(record.clone()), + _ => None, + }); + + let weight_view = WeightView::new(weight_record, |weight| { + println!("on_blur on the weight view: {:?}", weight) + }); + s.append(&weight_view); + records.into_iter().for_each(|record| { let record_view = match record { - TraxRecord::BikeRide(_) => gtk::Label::new(Some("BikeRide")), - TraxRecord::Row(_) => gtk::Label::new(Some("Row")), - TraxRecord::Run(_) => gtk::Label::new(Some("Run")), - TraxRecord::Steps(_) => gtk::Label::new(Some("Steps")), - TraxRecord::Swim(_) => gtk::Label::new(Some("Swim")), - TraxRecord::Walk(_) => gtk::Label::new(Some("Walk")), - TraxRecord::Weight(_) => gtk::Label::new(Some("Weight")), + TraxRecord::BikeRide(record) => Some( + TimeDistanceView::new(RecordType::BikeRide, record).upcast::(), + ), + TraxRecord::Row(record) => { + Some(TimeDistanceView::new(RecordType::Row, record).upcast::()) + } + TraxRecord::Run(record) => { + Some(TimeDistanceView::new(RecordType::Row, record).upcast::()) + } + TraxRecord::Swim(record) => { + Some(TimeDistanceView::new(RecordType::Row, record).upcast::()) + } + TraxRecord::Walk(record) => { + Some(TimeDistanceView::new(RecordType::Row, record).upcast::()) + } + _ => None, }; - record_view.add_css_class("day-detail"); - record_view.set_halign(gtk::Align::Start); + if let Some(record_view) = record_view { + record_view.add_css_class("day-detail"); + record_view.set_halign(gtk::Align::Start); - s.append(&record_view); + s.append(&record_view); + } }); s } } + +pub struct WeightViewPrivate { + record: RefCell>, + view: RefCell, + edit: RefCell, + current: RefCell, +} + +impl Default for WeightViewPrivate { + fn default() -> Self { + let view = gtk::Label::builder() + .css_classes(["card", "weight-view"]) + .halign(gtk::Align::Start) + .can_focus(true) + .build(); + let edit = gtk::Entry::builder().halign(gtk::Align::Start).build(); + + let current = view.clone(); + + Self { + record: RefCell::new(None), + view: RefCell::new(view), + edit: RefCell::new(edit), + current: RefCell::new(current.upcast()), + } + } +} + +#[glib::object_subclass] +impl ObjectSubclass for WeightViewPrivate { + const NAME: &'static str = "WeightView"; + type Type = WeightView; + type ParentType = gtk::Box; +} + +impl ObjectImpl for WeightViewPrivate {} +impl WidgetImpl for WeightViewPrivate {} +impl BoxImpl for WeightViewPrivate {} + +glib::wrapper! { + pub struct WeightView(ObjectSubclass) @extends gtk::Box, gtk::Widget; +} + +impl WeightView { + pub fn new(weight: Option, on_blur: OnBlur) -> Self + where + OnBlur: Fn(si::Kilogram), + { + let s: Self = Object::builder().build(); + + *s.imp().record.borrow_mut() = weight; + s.view(); + + let view_click_controller = gtk::GestureClick::new(); + view_click_controller.connect_released({ + let s = s.clone(); + move |_, _, _, _| { + s.edit(); + } + }); + + let edit_click_controller = gtk::GestureClick::new(); + edit_click_controller.connect_released({ + let s = s.clone(); + move |_, _, _, _| { + s.view(); + } + }); + + s.imp().view.borrow().add_controller(view_click_controller); + s.imp().edit.borrow().add_controller(edit_click_controller); + s + } + + fn view(&self) { + let view = self.imp().view.borrow(); + match *self.imp().record.borrow() { + Some(ref record) => { + view.remove_css_class("dim_label"); + view.set_label(&format!("{:?}", record.weight)); + } + None => { + view.add_css_class("dim_label"); + view.set_label("No weight recorded"); + } + } + self.swap(view.clone().upcast()); + } + + fn edit(&self) { + let edit = self.imp().edit.borrow(); + match *self.imp().record.borrow() { + Some(ref record) => edit.buffer().set_text(&format!("{:?}", record.weight)), + None => edit.buffer().set_text(""), + } + self.swap(edit.clone().upcast()); + edit.grab_focus(); + } + + fn swap(&self, new_view: gtk::Widget) { + let mut current = self.imp().current.borrow_mut(); + self.remove(&*current); + self.append(&new_view); + *current = new_view; + } +} + +#[derive(Default)] +pub struct TimeDistanceViewPrivate { + record: RefCell>, +} + +#[glib::object_subclass] +impl ObjectSubclass for TimeDistanceViewPrivate { + const NAME: &'static str = "TimeDistanceView"; + type Type = TimeDistanceView; + type ParentType = gtk::Box; +} + +impl ObjectImpl for TimeDistanceViewPrivate {} +impl WidgetImpl for TimeDistanceViewPrivate {} +impl BoxImpl for TimeDistanceViewPrivate {} + +glib::wrapper! { + pub struct TimeDistanceView(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; +} + +impl TimeDistanceView { + pub fn new(type_: RecordType, record: TimeDistance) -> Self { + let s: Self = Object::builder().build(); + s.set_orientation(gtk::Orientation::Vertical); + s.set_hexpand(true); + + let first_row = gtk::Box::builder().homogeneous(true).build(); + + first_row.append( + >k::Label::builder() + .halign(gtk::Align::Start) + .label(&record.datetime.format("%H:%M").to_string()) + .build(), + ); + + first_row.append( + >k::Label::builder() + .halign(gtk::Align::Start) + .label(format!("{:?}", type_)) + .build(), + ); + + first_row.append( + >k::Label::builder() + .halign(gtk::Align::Start) + .label( + record + .distance + .map(|dist| format!("{}", dist)) + .unwrap_or("".to_owned()), + ) + .build(), + ); + + first_row.append( + >k::Label::builder() + .halign(gtk::Align::Start) + .label( + record + .duration + .map(|duration| format!("{}", duration)) + .unwrap_or("".to_owned()), + ) + .build(), + ); + + s.append(&first_row); + + s.append( + >k::Label::builder() + .halign(gtk::Align::Start) + .label( + record + .comments + .map(|comments| format!("{}", comments)) + .unwrap_or("".to_owned()), + ) + .build(), + ); + + s + } +} diff --git a/fitnesstrax/core/src/lib.rs b/fitnesstrax/core/src/lib.rs index 4a3791c..b206602 100644 --- a/fitnesstrax/core/src/lib.rs +++ b/fitnesstrax/core/src/lib.rs @@ -1,3 +1,3 @@ mod legacy; mod types; -pub use types::{Steps, TimeDistance, TraxRecord, Weight}; +pub use types::{RecordType, Steps, TimeDistance, TraxRecord, Weight}; diff --git a/fitnesstrax/core/src/types.rs b/fitnesstrax/core/src/types.rs index 7026030..401f4a8 100644 --- a/fitnesstrax/core/src/types.rs +++ b/fitnesstrax/core/src/types.rs @@ -54,6 +54,17 @@ pub struct Weight { pub weight: si::Kilogram, } +#[derive(Clone, Debug, PartialEq)] +pub enum RecordType { + BikeRide, + Row, + Run, + Steps, + Swim, + Walk, + Weight, +} + /// The unified data structure for all records that are part of the app. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum TraxRecord { @@ -67,6 +78,18 @@ pub enum TraxRecord { } impl TraxRecord { + pub fn workout_type(&self) -> RecordType { + match self { + TraxRecord::BikeRide(_) => RecordType::BikeRide, + TraxRecord::Row(_) => RecordType::Row, + TraxRecord::Run(_) => RecordType::Run, + TraxRecord::Steps(_) => RecordType::Steps, + TraxRecord::Swim(_) => RecordType::Swim, + TraxRecord::Walk(_) => RecordType::Walk, + TraxRecord::Weight(_) => RecordType::Weight, + } + } + pub fn is_weight(&self) -> bool { match self { TraxRecord::Weight(_) => true,