Add a step count editor field #162

Merged
savanni merged 3 commits from fitnesstrax/step-counting into main 2024-01-20 22:08:31 +00:00
8 changed files with 201 additions and 108 deletions

View File

@ -11,8 +11,7 @@
padding: 8px; padding: 8px;
} }
.welcome__footer { .welcome__footer {}
}
.historical { .historical {
margin: 32px; margin: 32px;
@ -37,3 +36,7 @@
margin: 8px; margin: 8px;
} }
.step-view {
padding: 8px;
margin: 8px;
}

View File

@ -134,7 +134,7 @@ impl AppWindow {
} }
fn show_historical_view(&self, records: Vec<Record<TraxRecord>>) { fn show_historical_view(&self, records: Vec<Record<TraxRecord>>) {
let view = View::Historical(HistoricalView::new(records, { let view = View::Historical(HistoricalView::new(self.app.clone(), records, {
let s = self.clone(); let s = self.clone();
Rc::new(move |date, records| { Rc::new(move |date, records| {
let layout = gtk::Box::new(gtk::Orientation::Vertical, 0); let layout = gtk::Box::new(gtk::Orientation::Vertical, 0);

View File

@ -17,19 +17,15 @@ You should have received a copy of the GNU General Public License along with Fit
// use chrono::NaiveDate; // use chrono::NaiveDate;
// use ft_core::TraxRecord; // use ft_core::TraxRecord;
use crate::{ use crate::{
components::{ActionGroup, Weight}, components::{steps_editor, weight_editor, ActionGroup, Steps, Weight},
view_models::DayDetailViewModel, view_models::DayDetailViewModel,
}; };
use emseries::Record;
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use std::cell::RefCell; use std::cell::RefCell;
use super::weight::WeightEdit;
pub struct DaySummaryPrivate { pub struct DaySummaryPrivate {
date: gtk::Label, date: gtk::Label,
weight: RefCell<Option<gtk::Label>>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -43,10 +39,7 @@ impl ObjectSubclass for DaySummaryPrivate {
.css_classes(["day-summary__date"]) .css_classes(["day-summary__date"])
.halign(gtk::Align::Start) .halign(gtk::Align::Start)
.build(); .build();
Self { Self { date }
date,
weight: RefCell::new(None),
}
} }
} }
@ -77,37 +70,34 @@ impl DaySummary {
Self::default() Self::default()
} }
pub fn set_data(&self, date: chrono::NaiveDate, records: Vec<Record<ft_core::TraxRecord>>) { pub fn set_data(&self, view_model: DayDetailViewModel) {
self.imp() self.imp()
.date .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() { let row = gtk::Box::builder().build();
self.remove(weight_label);
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 { self.append(&label);
data: ft_core::TraxRecord::Weight(weight_record),
.. let label = gtk::Label::builder()
}) = records.iter().find(|f| f.data.is_weight()) .halign(gtk::Align::Start)
{ .css_classes(["day-summary__weight"])
let label = gtk::Label::builder() .build();
.halign(gtk::Align::Start) if let Some(s) = view_model.steps() {
.label(weight_record.weight.to_string()) label.set_label(&format!("{} steps", s.to_string()));
.css_classes(["day-summary__weight"])
.build();
self.append(&label);
*self.imp().weight.borrow_mut() = Some(label);
} }
row.append(&label);
/* self.append(&row);
self.append(
&gtk::Label::builder()
.halign(gtk::Align::Start)
.label("15km of biking in 60 minutes")
.build(),
);
*/
} }
} }
@ -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()); 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| { records.into_iter().for_each(|record| {
@ -281,8 +279,11 @@ impl DayEdit {
.build(), .build(),
); );
s.append( let top_row = gtk::Box::builder()
&WeightEdit::new(view_model.weight(), { .orientation(gtk::Orientation::Horizontal)
.build();
top_row.append(
&weight_editor(view_model.weight(), {
let view_model = view_model.clone(); let view_model = view_model.clone();
move |w| { move |w| {
view_model.set_weight(w); view_model.set_weight(w);
@ -291,6 +292,15 @@ impl DayEdit {
.widget(), .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 s
} }

View File

@ -23,6 +23,9 @@ pub use day::{DayDetail, DayEdit, DaySummary};
mod singleton; mod singleton;
pub use singleton::{Singleton, SingletonImpl}; pub use singleton::{Singleton, SingletonImpl};
mod steps;
pub use steps::{steps_editor, Steps};
mod text_entry; mod text_entry;
pub use text_entry::{ParseError, TextEntry}; pub use text_entry::{ParseError, TextEntry};
@ -30,7 +33,7 @@ mod time_distance;
pub use time_distance::TimeDistanceView; pub use time_distance::TimeDistanceView;
mod weight; mod weight;
pub use weight::Weight; pub use weight::{weight_editor, Weight};
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};

View File

@ -0,0 +1,61 @@
/*
Copyright 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 crate::components::{ParseError, TextEntry};
use gtk::prelude::*;
#[derive(Default)]
pub struct Steps {
label: gtk::Label,
}
impl Steps {
pub fn new(steps: Option<u32>) -> 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<OnUpdate>(value: Option<u32>, on_update: OnUpdate) -> TextEntry<u32>
where
OnUpdate: Fn(u32) + 'static,
{
TextEntry::new(
"0",
value,
|v| format!("{}", v),
move |v| match v.parse::<u32>() {
Ok(val) => {
on_update(val);
Ok(val)
}
Err(_) => Err(ParseError),
},
)
}

View File

@ -18,9 +18,6 @@ use crate::components::{ParseError, TextEntry};
use dimensioned::si; use dimensioned::si;
use gtk::prelude::*; use gtk::prelude::*;
#[derive(Default)]
pub struct WeightViewPrivate {}
pub struct Weight { pub struct Weight {
label: gtk::Label, label: gtk::Label,
} }
@ -45,45 +42,26 @@ impl Weight {
} }
} }
#[derive(Debug)] pub fn weight_editor<OnUpdate>(
pub struct WeightEdit { weight: Option<si::Kilogram<f64>>,
entry: TextEntry<si::Kilogram<f64>>, on_update: OnUpdate,
} ) -> TextEntry<si::Kilogram<f64>>
where
impl WeightEdit { OnUpdate: Fn(si::Kilogram<f64>) + 'static,
pub fn new<OnUpdate>(weight: Option<si::Kilogram<f64>>, on_update: OnUpdate) -> Self {
where TextEntry::new(
OnUpdate: Fn(si::Kilogram<f64>) + 'static, "0 kg",
{ weight,
Self { |val: &si::Kilogram<f64>| val.to_string(),
entry: TextEntry::new( move |v: &str| {
"0 kg", let new_weight = v.parse::<f64>().map(|w| w * si::KG).map_err(|_| ParseError);
weight, match new_weight {
|val: &si::Kilogram<f64>| val.to_string(), Ok(w) => {
move |v: &str| { on_update(w);
let new_weight = v.parse::<f64>().map(|w| w * si::KG).map_err(|_| ParseError); Ok(w)
match new_weight { }
Ok(w) => { Err(err) => Err(err),
on_update(w); }
Ok(w) },
} )
Err(err) => Err(err),
}
},
),
}
}
#[allow(unused)]
pub fn set_value(&self, value: Option<si::Kilogram<f64>>) {
self.entry.set_value(value);
}
pub fn value(&self) -> Option<si::Kilogram<f64>> {
self.entry.value()
}
pub fn widget(&self) -> gtk::Widget {
self.entry.widget()
}
} }

View File

@ -186,6 +186,24 @@ impl DayDetailViewModel {
None => {} 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 let records = s
.records .records
.write() .write()

View File

@ -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 <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::DaySummary, types::DayInterval}; use crate::{
app::App, components::DaySummary, types::DayInterval, view_models::DayDetailViewModel,
};
use chrono::NaiveDate; use chrono::NaiveDate;
use emseries::Record; use emseries::Record;
use ft_core::TraxRecord; 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 /// daily summaries, daily details, and will provide all functions the user may need for editing
/// records. /// records.
pub struct HistoricalViewPrivate { pub struct HistoricalViewPrivate {
time_window: RefCell<DayInterval>, app: Rc<RefCell<Option<App>>>,
time_window: Rc<RefCell<DayInterval>>,
list_view: gtk::ListView, list_view: gtk::ListView,
} }
@ -45,31 +48,42 @@ impl ObjectSubclass for HistoricalViewPrivate {
.set_child(Some(&DaySummary::new())); .set_child(Some(&DaySummary::new()));
}); });
factory.connect_bind(move |_, list_item| { let s = Self {
let records = list_item app: Rc::new(RefCell::new(None)),
.downcast_ref::<gtk::ListItem>() time_window: Rc::new(RefCell::new(DayInterval::default())),
.expect("should be a ListItem")
.item()
.and_downcast::<DayRecords>()
.expect("should be a DaySummary");
let summary = list_item
.downcast_ref::<gtk::ListItem>()
.expect("should be a ListItem")
.child()
.and_downcast::<DaySummary>()
.expect("should be a DaySummary");
summary.set_data(records.date(), records.records());
});
Self {
time_window: RefCell::new(DayInterval::default()),
list_view: gtk::ListView::builder() list_view: gtk::ListView::builder()
.factory(&factory) .factory(&factory)
.single_click_activate(true) .single_click_activate(true)
.build(), .build(),
} };
factory.connect_bind({
let app = s.app.clone();
move |_, list_item| {
let records = list_item
.downcast_ref::<gtk::ListItem>()
.expect("should be a ListItem")
.item()
.and_downcast::<DayRecords>()
.expect("should be a DaySummary");
let summary = list_item
.downcast_ref::<gtk::ListItem>()
.expect("should be a ListItem")
.child()
.and_downcast::<DaySummary>()
.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 { impl HistoricalView {
pub fn new<SelectFn>(records: Vec<Record<TraxRecord>>, on_select_day: Rc<SelectFn>) -> Self pub fn new<SelectFn>(
app: App,
records: Vec<Record<TraxRecord>>,
on_select_day: Rc<SelectFn>,
) -> Self
where where
SelectFn: Fn(chrono::NaiveDate, Vec<Record<TraxRecord>>) + 'static, SelectFn: Fn(chrono::NaiveDate, Vec<Record<TraxRecord>>) + 'static,
{ {
@ -90,6 +108,8 @@ impl HistoricalView {
s.set_orientation(gtk::Orientation::Vertical); s.set_orientation(gtk::Orientation::Vertical);
s.set_css_classes(&["historical"]); s.set_css_classes(&["historical"]);
*s.imp().app.borrow_mut() = Some(app);
let grouped_records = let grouped_records =
GroupedRecords::new((*s.imp().time_window.borrow()).clone()).with_data(records); GroupedRecords::new((*s.imp().time_window.borrow()).clone()).with_data(records);