diff --git a/fitnesstrax/app/resources/style.css b/fitnesstrax/app/resources/style.css index 2814994..b375633 100644 --- a/fitnesstrax/app/resources/style.css +++ b/fitnesstrax/app/resources/style.css @@ -11,8 +11,7 @@ padding: 8px; } -.welcome__footer { -} +.welcome__footer {} .historical { margin: 32px; @@ -37,3 +36,7 @@ margin: 8px; } +.step-view { + padding: 8px; + margin: 8px; +} \ No newline at end of file diff --git a/fitnesstrax/app/src/app_window.rs b/fitnesstrax/app/src/app_window.rs index f5f2d46..75f6dbb 100644 --- a/fitnesstrax/app/src/app_window.rs +++ b/fitnesstrax/app/src/app_window.rs @@ -134,7 +134,7 @@ impl AppWindow { } fn show_historical_view(&self, records: Vec>) { - let view = View::Historical(HistoricalView::new(records, { + let view = View::Historical(HistoricalView::new(self.app.clone(), records, { let s = self.clone(); Rc::new(move |date, records| { let layout = gtk::Box::new(gtk::Orientation::Vertical, 0); diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 631600e..b4b130d 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -17,19 +17,15 @@ 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::{ActionGroup, Weight}, + components::{steps_editor, weight_editor, ActionGroup, Steps, Weight}, view_models::DayDetailViewModel, }; -use emseries::Record; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::cell::RefCell; -use super::weight::WeightEdit; - pub struct DaySummaryPrivate { date: gtk::Label, - weight: RefCell>, } #[glib::object_subclass] @@ -43,10 +39,7 @@ impl ObjectSubclass for DaySummaryPrivate { .css_classes(["day-summary__date"]) .halign(gtk::Align::Start) .build(); - Self { - date, - weight: RefCell::new(None), - } + Self { date } } } @@ -77,37 +70,34 @@ impl DaySummary { Self::default() } - pub fn set_data(&self, date: chrono::NaiveDate, records: Vec>) { + pub fn set_data(&self, view_model: DayDetailViewModel) { self.imp() .date - .set_text(&date.format("%Y-%m-%d").to_string()); + .set_text(&view_model.date.format("%Y-%m-%d").to_string()); - if let Some(ref weight_label) = *self.imp().weight.borrow() { - self.remove(weight_label); + let row = gtk::Box::builder().build(); + + let label = gtk::Label::builder() + .halign(gtk::Align::Start) + .css_classes(["day-summary__weight"]) + .build(); + if let Some(w) = view_model.weight() { + label.set_label(&w.to_string()) } + row.append(&label); - if let Some(Record { - data: ft_core::TraxRecord::Weight(weight_record), - .. - }) = records.iter().find(|f| f.data.is_weight()) - { - let label = gtk::Label::builder() - .halign(gtk::Align::Start) - .label(weight_record.weight.to_string()) - .css_classes(["day-summary__weight"]) - .build(); - self.append(&label); - *self.imp().weight.borrow_mut() = Some(label); + self.append(&label); + + let label = gtk::Label::builder() + .halign(gtk::Align::Start) + .css_classes(["day-summary__weight"]) + .build(); + if let Some(s) = view_model.steps() { + label.set_label(&format!("{} steps", s.to_string())); } + row.append(&label); - /* - self.append( - >k::Label::builder() - .halign(gtk::Align::Start) - .label("15km of biking in 60 minutes") - .build(), - ); - */ + self.append(&row); } } @@ -169,8 +159,16 @@ impl DayDetail { }); */ + let top_row = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .build(); let weight_view = Weight::new(view_model.weight()); - s.append(&weight_view.widget()); + top_row.append(&weight_view.widget()); + + let steps_view = Steps::new(view_model.steps()); + top_row.append(&steps_view.widget()); + + s.append(&top_row); /* records.into_iter().for_each(|record| { @@ -281,8 +279,11 @@ impl DayEdit { .build(), ); - s.append( - &WeightEdit::new(view_model.weight(), { + let top_row = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .build(); + top_row.append( + &weight_editor(view_model.weight(), { let view_model = view_model.clone(); move |w| { view_model.set_weight(w); @@ -291,6 +292,15 @@ impl DayEdit { .widget(), ); + top_row.append( + &steps_editor(view_model.steps(), { + let view_model = view_model.clone(); + move |s| view_model.set_steps(s) + }) + .widget(), + ); + s.append(&top_row); + s } diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index 78e9b9b..880664e 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -23,6 +23,9 @@ pub use day::{DayDetail, DayEdit, DaySummary}; mod singleton; pub use singleton::{Singleton, SingletonImpl}; +mod steps; +pub use steps::{steps_editor, Steps}; + mod text_entry; pub use text_entry::{ParseError, TextEntry}; @@ -30,7 +33,7 @@ mod time_distance; pub use time_distance::TimeDistanceView; mod weight; -pub use weight::Weight; +pub use weight::{weight_editor, Weight}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; diff --git a/fitnesstrax/app/src/components/steps.rs b/fitnesstrax/app/src/components/steps.rs new file mode 100644 index 0000000..e81c840 --- /dev/null +++ b/fitnesstrax/app/src/components/steps.rs @@ -0,0 +1,61 @@ +/* +Copyright 2024, 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::{ParseError, TextEntry}; +use gtk::prelude::*; + +#[derive(Default)] +pub struct Steps { + label: gtk::Label, +} + +impl Steps { + pub fn new(steps: Option) -> Self { + let label = gtk::Label::builder() + .css_classes(["card", "step-view"]) + .can_focus(true) + .build(); + + match steps { + Some(s) => label.set_text(&format!("{}", s)), + None => label.set_text("No steps recorded"), + } + + Self { label } + } + + pub fn widget(&self) -> gtk::Widget { + self.label.clone().upcast() + } +} + +pub fn steps_editor(value: Option, on_update: OnUpdate) -> TextEntry +where + OnUpdate: Fn(u32) + 'static, +{ + TextEntry::new( + "0", + value, + |v| format!("{}", v), + move |v| match v.parse::() { + Ok(val) => { + on_update(val); + Ok(val) + } + Err(_) => Err(ParseError), + }, + ) +} diff --git a/fitnesstrax/app/src/components/weight.rs b/fitnesstrax/app/src/components/weight.rs index 220b51e..059e903 100644 --- a/fitnesstrax/app/src/components/weight.rs +++ b/fitnesstrax/app/src/components/weight.rs @@ -18,9 +18,6 @@ use crate::components::{ParseError, TextEntry}; use dimensioned::si; use gtk::prelude::*; -#[derive(Default)] -pub struct WeightViewPrivate {} - pub struct Weight { label: gtk::Label, } @@ -45,45 +42,26 @@ impl Weight { } } -#[derive(Debug)] -pub struct WeightEdit { - entry: TextEntry>, -} - -impl WeightEdit { - pub fn new(weight: Option>, on_update: OnUpdate) -> Self - where - OnUpdate: Fn(si::Kilogram) + 'static, - { - Self { - entry: TextEntry::new( - "0 kg", - weight, - |val: &si::Kilogram| val.to_string(), - move |v: &str| { - let new_weight = v.parse::().map(|w| w * si::KG).map_err(|_| ParseError); - match new_weight { - Ok(w) => { - on_update(w); - Ok(w) - } - Err(err) => Err(err), - } - }, - ), - } - } - - #[allow(unused)] - pub fn set_value(&self, value: Option>) { - self.entry.set_value(value); - } - - pub fn value(&self) -> Option> { - self.entry.value() - } - - pub fn widget(&self) -> gtk::Widget { - self.entry.widget() - } +pub fn weight_editor( + weight: Option>, + on_update: OnUpdate, +) -> TextEntry> +where + OnUpdate: Fn(si::Kilogram) + 'static, +{ + TextEntry::new( + "0 kg", + weight, + |val: &si::Kilogram| val.to_string(), + move |v: &str| { + let new_weight = v.parse::().map(|w| w * si::KG).map_err(|_| ParseError); + match new_weight { + Ok(w) => { + on_update(w); + Ok(w) + } + Err(err) => Err(err), + } + }, + ) } diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs index f823d6f..f32e75f 100644 --- a/fitnesstrax/app/src/view_models/day_detail.rs +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -186,6 +186,24 @@ impl DayDetailViewModel { None => {} } + let steps_record = s.steps.read().unwrap().clone(); + match steps_record { + Some(RecordState::New(steps)) => { + let _ = app.put_record(TraxRecord::Steps(steps)).await; + } + Some(RecordState::Original(_)) => {} + Some(RecordState::Updated(steps)) => { + let _ = app + .update_record(Record { + id: steps.id, + data: TraxRecord::Steps(steps.data), + }) + .await; + } + Some(RecordState::Deleted(_)) => {} + None => {} + } + let records = s .records .write() diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index 23bccad..2e80e40 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -14,7 +14,9 @@ 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::DaySummary, types::DayInterval}; +use crate::{ + app::App, components::DaySummary, types::DayInterval, view_models::DayDetailViewModel, +}; use chrono::NaiveDate; use emseries::Record; use ft_core::TraxRecord; @@ -26,7 +28,8 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc}; /// daily summaries, daily details, and will provide all functions the user may need for editing /// records. pub struct HistoricalViewPrivate { - time_window: RefCell, + app: Rc>>, + time_window: Rc>, list_view: gtk::ListView, } @@ -45,31 +48,42 @@ impl ObjectSubclass for HistoricalViewPrivate { .set_child(Some(&DaySummary::new())); }); - factory.connect_bind(move |_, list_item| { - let records = list_item - .downcast_ref::() - .expect("should be a ListItem") - .item() - .and_downcast::() - .expect("should be a DaySummary"); - - let summary = list_item - .downcast_ref::() - .expect("should be a ListItem") - .child() - .and_downcast::() - .expect("should be a DaySummary"); - - summary.set_data(records.date(), records.records()); - }); - - Self { - time_window: RefCell::new(DayInterval::default()), + let s = Self { + app: Rc::new(RefCell::new(None)), + time_window: Rc::new(RefCell::new(DayInterval::default())), list_view: gtk::ListView::builder() .factory(&factory) .single_click_activate(true) .build(), - } + }; + factory.connect_bind({ + let app = s.app.clone(); + move |_, list_item| { + let records = list_item + .downcast_ref::() + .expect("should be a ListItem") + .item() + .and_downcast::() + .expect("should be a DaySummary"); + + let summary = list_item + .downcast_ref::() + .expect("should be a ListItem") + .child() + .and_downcast::() + .expect("should be a DaySummary"); + + if let Some(app) = app.borrow().clone() { + summary.set_data(DayDetailViewModel::new( + records.date(), + records.records(), + app.clone(), + )); + } + } + }); + + s } } @@ -82,7 +96,11 @@ glib::wrapper! { } impl HistoricalView { - pub fn new(records: Vec>, on_select_day: Rc) -> Self + pub fn new( + app: App, + records: Vec>, + on_select_day: Rc, + ) -> Self where SelectFn: Fn(chrono::NaiveDate, Vec>) + 'static, { @@ -90,6 +108,8 @@ impl HistoricalView { s.set_orientation(gtk::Orientation::Vertical); s.set_css_classes(&["historical"]); + *s.imp().app.borrow_mut() = Some(app); + let grouped_records = GroupedRecords::new((*s.imp().time_window.borrow()).clone()).with_data(records);