diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index b16980d..8735086 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -16,14 +16,12 @@ You should have received a copy of the GNU General Public License along with Fit // use chrono::NaiveDate; // use ft_core::TraxRecord; -use crate::components::{EditView, ParseError, TextEntry}; -use chrono::{Local, NaiveDate}; -use dimensioned::si; +use crate::components::{TimeDistanceView, WeightView}; use emseries::Record; -use ft_core::{RecordType, TimeDistance, TraxRecord, Weight}; +use ft_core::{RecordType, TraxRecord, Weight}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use std::{cell::RefCell, rc::Rc}; +use std::cell::RefCell; pub struct DaySummaryPrivate { date: gtk::Label, @@ -220,250 +218,3 @@ impl DayDetail { s } } - -pub struct WeightViewPrivate { - date: RefCell, - record: RefCell>, - - widget: RefCell>>>, - - on_edit_finished: RefCell)>>, -} - -impl Default for WeightViewPrivate { - fn default() -> Self { - Self { - date: RefCell::new(Local::now().date_naive()), - record: RefCell::new(None), - widget: RefCell::new(EditView::Unconfigured), - on_edit_finished: RefCell::new(Box::new(|_| {})), - } - } -} - -#[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( - date: NaiveDate, - weight: Option, - on_edit_finished: OnEditFinished, - ) -> Self - where - OnEditFinished: Fn(si::Kilogram) + 'static, - { - let s: Self = Object::builder().build(); - - *s.imp().on_edit_finished.borrow_mut() = Box::new(on_edit_finished); - *s.imp().date.borrow_mut() = date; - - *s.imp().record.borrow_mut() = weight; - s.view(); - - s - } - - fn view(&self) { - let view = gtk::Label::builder() - .css_classes(["card", "weight-view"]) - .halign(gtk::Align::Start) - .can_focus(true) - .build(); - - let view_click_controller = gtk::GestureClick::new(); - view_click_controller.connect_released({ - let s = self.clone(); - move |_, _, _, _| { - s.edit(); - } - }); - - view.add_controller(view_click_controller); - - 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(EditView::View(view)); - } - - fn edit(&self) { - let edit = TextEntry::>::new( - "weight", - None, - |val: &si::Kilogram| val.to_string(), - |v: &str| v.parse::().map(|w| w * si::KG).map_err(|_| ParseError), - ); - - match *self.imp().record.borrow() { - Some(ref record) => edit.set_value(Some(record.weight)), - None => edit.set_value(None), - } - - self.swap(EditView::Edit(edit.clone())); - edit.grab_focus(); - } - - fn swap(&self, new_view: EditView>>) { - let mut widget = self.imp().widget.borrow_mut(); - match *widget { - EditView::Unconfigured => {} - EditView::View(ref view) => self.remove(view), - EditView::Edit(ref editor) => self.remove(&editor.widget()), - } - - match new_view { - EditView::Unconfigured => {} - EditView::View(ref view) => self.append(view), - EditView::Edit(ref editor) => self.append(&editor.widget()), - } - *widget = new_view; - } - - fn blur(&self) { - match *self.imp().widget.borrow() { - EditView::Unconfigured => {} - EditView::View(_) => {} - EditView::Edit(ref editor) => { - let weight = editor.value(); - // This has really turned into rubbish - // on_edit_finished needs to accept a full record now. - // needs to be possible to delete a record if the value is None - // it's hard to be sure whether I need the full record object or if I need to update - // it. I probably don't. I think I need to borrow it and call on_edit_finished with an - // updated version of it. - // on_edit_finished still doesn't have a way to support a delete operation - let record = match (self.imp().record.borrow().clone(), weight) { - // update an existing record - (Some(record), Some(weight)) => Some(Weight { - date: record.date, - weight, - }), - - // create a new record - (None, Some(weight)) => Some(Weight { - date: self.imp().date.borrow().clone(), - weight, - }), - - // do nothing or delete an existing record - (_, None) => None, - }; - - match record { - Some(record) => { - self.imp().on_edit_finished.borrow()(record.weight); - *self.imp().record.borrow_mut() = Some(record); - } - None => {} - } - } - } - - self.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/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index 30e7908..19ee548 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -23,6 +23,12 @@ pub use edit_view::EditView; mod text_entry; pub use text_entry::{ParseError, TextEntry}; +mod time_distance; +pub use time_distance::TimeDistanceView; + +mod weight; +pub use weight::WeightView; + use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::{cell::RefCell, path::PathBuf, rc::Rc}; diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs new file mode 100644 index 0000000..8a20c42 --- /dev/null +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -0,0 +1,107 @@ +/* +Copyright 2023, Savanni D'Gerinel + +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 . +*/ + +// use crate::components::{EditView, ParseError, TextEntry}; +// use chrono::{Local, NaiveDate}; +// use dimensioned::si; +use ft_core::{RecordType, TimeDistance}; +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; +use std::cell::RefCell; + +#[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/app/src/components/weight.rs b/fitnesstrax/app/src/components/weight.rs new file mode 100644 index 0000000..f32d11e --- /dev/null +++ b/fitnesstrax/app/src/components/weight.rs @@ -0,0 +1,186 @@ +/* +Copyright 2023, Savanni D'Gerinel + +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 . +*/ + +use crate::components::{EditView, ParseError, TextEntry}; +use chrono::{Local, NaiveDate}; +use dimensioned::si; +use ft_core::Weight; +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; +use std::cell::RefCell; + +pub struct WeightViewPrivate { + date: RefCell, + record: RefCell>, + + widget: RefCell>>>, + + on_edit_finished: RefCell)>>, +} + +impl Default for WeightViewPrivate { + fn default() -> Self { + Self { + date: RefCell::new(Local::now().date_naive()), + record: RefCell::new(None), + widget: RefCell::new(EditView::Unconfigured), + on_edit_finished: RefCell::new(Box::new(|_| {})), + } + } +} + +#[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( + date: NaiveDate, + weight: Option, + on_edit_finished: OnEditFinished, + ) -> Self + where + OnEditFinished: Fn(si::Kilogram) + 'static, + { + let s: Self = Object::builder().build(); + + *s.imp().on_edit_finished.borrow_mut() = Box::new(on_edit_finished); + *s.imp().date.borrow_mut() = date; + + *s.imp().record.borrow_mut() = weight; + s.view(); + + s + } + + fn view(&self) { + let view = gtk::Label::builder() + .css_classes(["card", "weight-view"]) + .halign(gtk::Align::Start) + .can_focus(true) + .build(); + + let view_click_controller = gtk::GestureClick::new(); + view_click_controller.connect_released({ + let s = self.clone(); + move |_, _, _, _| { + s.edit(); + } + }); + + view.add_controller(view_click_controller); + + 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(EditView::View(view)); + } + + fn edit(&self) { + let edit = TextEntry::>::new( + "weight", + None, + |val: &si::Kilogram| val.to_string(), + |v: &str| v.parse::().map(|w| w * si::KG).map_err(|_| ParseError), + ); + + match *self.imp().record.borrow() { + Some(ref record) => edit.set_value(Some(record.weight)), + None => edit.set_value(None), + } + + self.swap(EditView::Edit(edit.clone())); + edit.grab_focus(); + } + + fn swap(&self, new_view: EditView>>) { + let mut widget = self.imp().widget.borrow_mut(); + match *widget { + EditView::Unconfigured => {} + EditView::View(ref view) => self.remove(view), + EditView::Edit(ref editor) => self.remove(&editor.widget()), + } + + match new_view { + EditView::Unconfigured => {} + EditView::View(ref view) => self.append(view), + EditView::Edit(ref editor) => self.append(&editor.widget()), + } + *widget = new_view; + } + + pub fn blur(&self) { + match *self.imp().widget.borrow() { + EditView::Unconfigured => {} + EditView::View(_) => {} + EditView::Edit(ref editor) => { + let weight = editor.value(); + // This has really turned into rubbish + // on_edit_finished needs to accept a full record now. + // needs to be possible to delete a record if the value is None + // it's hard to be sure whether I need the full record object or if I need to update + // it. I probably don't. I think I need to borrow it and call on_edit_finished with an + // updated version of it. + // on_edit_finished still doesn't have a way to support a delete operation + let record = match (self.imp().record.borrow().clone(), weight) { + // update an existing record + (Some(record), Some(weight)) => Some(Weight { + date: record.date, + weight, + }), + + // create a new record + (None, Some(weight)) => Some(Weight { + date: self.imp().date.borrow().clone(), + weight, + }), + + // do nothing or delete an existing record + (_, None) => None, + }; + + match record { + Some(record) => { + self.imp().on_edit_finished.borrow()(record.weight); + *self.imp().record.borrow_mut() = Some(record); + } + None => {} + } + } + } + + self.view(); + } +}