From ecdd38ebbc4b5be0d37d45c9d70760c27a72bd42 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 21 Jan 2024 10:50:18 -0500 Subject: [PATCH 01/13] Show a summary of the day's biking stats when there is one --- fitnesstrax/app/src/components/day.rs | 11 ++++++++-- fitnesstrax/app/src/components/mod.rs | 2 +- .../app/src/components/time_distance.rs | 22 +++++++++++++++++++ fitnesstrax/app/src/view_models/day_detail.rs | 20 ++++++++--------- 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 0b8791f..dbc115e 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -17,10 +17,13 @@ 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::{steps_editor, weight_field, ActionGroup, Steps, WeightLabel}, + components::{ + steps_editor, time_distance_summary, weight_field, ActionGroup, Steps, WeightLabel, + }, types::WeightFormatter, view_models::DayDetailViewModel, }; +use ft_core::TimeDistanceActivity; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::cell::RefCell; @@ -97,8 +100,12 @@ impl DaySummary { label.set_label(&format!("{} steps", s)); } row.append(&label); - self.append(&row); + + let biking_summary = view_model.time_distance_summary(TimeDistanceActivity::BikeRide); + if let Some(label) = time_distance_summary(biking_summary.0, biking_summary.1) { + self.append(&label); + } } } diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index b6c0fe3..cdaa1b6 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -30,7 +30,7 @@ mod text_entry; pub use text_entry::{weight_field, TextEntry}; mod time_distance; -pub use time_distance::TimeDistanceView; +pub use time_distance::{time_distance_summary, TimeDistanceView}; mod weight; pub use weight::WeightLabel; diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs index 2721002..ae91b5b 100644 --- a/fitnesstrax/app/src/components/time_distance.rs +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -17,11 +17,33 @@ You should have received a copy of the GNU General Public License along with Fit // use crate::components::{EditView, ParseError, TextEntry}; // use chrono::{Local, NaiveDate}; // use dimensioned::si; +use dimensioned::si; use ft_core::TimeDistance; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::cell::RefCell; +pub fn time_distance_summary( + distance: si::Meter, + duration: si::Second, +) -> Option { + let text = match (distance > si::M, duration > si::S) { + (true, true) => Some(format!( + "{} kilometers of biking in {} minutes", + distance.value_unsafe / 1000., + duration.value_unsafe / 60. + )), + (true, false) => Some(format!( + "{} kilometers of biking", + distance.value_unsafe / 1000. + )), + (false, true) => Some(format!("{} seconds of biking", duration.value_unsafe / 60.)), + (false, false) => None, + }; + + text.map(|text| gtk::Label::new(Some(&text))) +} + #[derive(Default)] pub struct TimeDistanceViewPrivate { #[allow(unused)] diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs index 0f651bd..f0975d7 100644 --- a/fitnesstrax/app/src/view_models/day_detail.rs +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -36,16 +36,6 @@ enum RecordState { } impl RecordState { - #[allow(unused)] - fn id(&self) -> Option<&RecordId> { - match self { - RecordState::Original(ref r) => Some(&r.id), - RecordState::New(ref r) => None, - RecordState::Updated(ref r) => Some(&r.id), - RecordState::Deleted(ref r) => Some(&r.id), - } - } - fn exists(&self) -> bool { match self { RecordState::Original(_) => true, @@ -55,6 +45,16 @@ impl RecordState { } } + #[allow(unused)] + fn data(&self) -> Option<&Record> { + match self { + RecordState::Original(ref r) => Some(r), + RecordState::New(ref r) => None, + RecordState::Updated(ref r) => Some(r), + RecordState::Deleted(ref r) => Some(r), + } + } + fn set_value(&mut self, value: T) { *self = match self { RecordState::Original(r) => RecordState::Updated(Record { -- 2.44.1 From 96c42016805fe90c0b4863ec006246845bef87b6 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 23 Jan 2024 08:28:39 -0500 Subject: [PATCH 02/13] Render time distance details in the day detail view --- fitnesstrax/app/src/components/day.rs | 51 +----- fitnesstrax/app/src/components/mod.rs | 2 +- .../app/src/components/time_distance.rs | 149 +++++++----------- 3 files changed, 67 insertions(+), 135 deletions(-) diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index dbc115e..09d03cf 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -28,6 +28,8 @@ use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::cell::RefCell; +use super::time_distance_detail; + pub struct DaySummaryPrivate { date: gtk::Label, } @@ -178,51 +180,10 @@ impl DayDetail { s.append(&top_row); - /* - records.into_iter().for_each(|record| { - let record_view = match record { - Record { - data: ft_core::TraxRecord::BikeRide(record), - .. - } => Some( - TimeDistanceView::new(ft_core::RecordType::BikeRide, record) - .upcast::(), - ), - Record { - data: ft_core::TraxRecord::Row(record), - .. - } => Some( - TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::(), - ), - Record { - data: ft_core::TraxRecord::Run(record), - .. - } => Some( - TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::(), - ), - Record { - data: ft_core::TraxRecord::Swim(record), - .. - } => Some( - TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::(), - ), - Record { - data: ft_core::TraxRecord::Walk(record), - .. - } => Some( - TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::(), - ), - _ => None, - }; - - 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); - } - }); - */ + let records = view_model.time_distance_records(); + for emseries::Record { data, .. } in records { + s.append(&time_distance_detail(data)); + } s } diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index cdaa1b6..792db09 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -30,7 +30,7 @@ mod text_entry; pub use text_entry::{weight_field, TextEntry}; mod time_distance; -pub use time_distance::{time_distance_summary, TimeDistanceView}; +pub use time_distance::{time_distance_detail, time_distance_summary}; mod weight; pub use weight::WeightLabel; diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs index ae91b5b..1ff3ba2 100644 --- a/fitnesstrax/app/src/components/time_distance.rs +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -18,10 +18,7 @@ You should have received a copy of the GNU General Public License along with Fit // use chrono::{Local, NaiveDate}; // use dimensioned::si; use dimensioned::si; -use ft_core::TimeDistance; -use glib::Object; -use gtk::{prelude::*, subclass::prelude::*}; -use std::cell::RefCell; +use gtk::prelude::*; pub fn time_distance_summary( distance: si::Meter, @@ -44,89 +41,63 @@ pub fn time_distance_summary( text.map(|text| gtk::Label::new(Some(&text))) } -#[derive(Default)] -pub struct TimeDistanceViewPrivate { - #[allow(unused)] - 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(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| comments.to_string()) - .unwrap_or("".to_owned()), - ) - .build(), - ); - - s - } +pub fn time_distance_detail(record: ft_core::TimeDistance) -> gtk::Box { + let layout = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .hexpand(true) + .build(); + 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!("{:?}", record.activity)) + .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(), + ); + + layout.append(&first_row); + + layout.append( + >k::Label::builder() + .halign(gtk::Align::Start) + .label( + record + .comments + .map(|comments| comments.to_string()) + .unwrap_or("".to_owned()), + ) + .build(), + ); + layout } -- 2.44.1 From 74df2880bb736ba8629a207a96bf934a568e6457 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 23 Jan 2024 08:46:35 -0500 Subject: [PATCH 03/13] Implement the Edit Cancel button --- fitnesstrax/app/src/components/day.rs | 102 ++++++++++-------- fitnesstrax/app/src/view_models/day_detail.rs | 38 ++++++- 2 files changed, 90 insertions(+), 50 deletions(-) diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 09d03cf..59b601e 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -238,52 +238,8 @@ impl DayEdit { *s.imp().on_finished.borrow_mut() = Box::new(on_finished); *s.imp().view_model.borrow_mut() = Some(view_model.clone()); - s.append( - &ActionGroup::builder() - .primary_action("Save", { - let s = s.clone(); - let view_model = view_model.clone(); - move || { - view_model.save(); - s.finish(); - } - }) - .secondary_action("Cancel", { - let s = s.clone(); - let view_model = view_model.clone(); - move || { - view_model.revert(); - s.finish(); - } - }) - .build(), - ); - - let top_row = gtk::Box::builder() - .orientation(gtk::Orientation::Horizontal) - .build(); - top_row.append( - &weight_field(view_model.weight().map(WeightFormatter::from), { - let view_model = view_model.clone(); - move |w| match w { - Some(w) => view_model.set_weight(*w), - None => eprintln!("have not implemented record delete"), - } - }) - .widget(), - ); - - top_row.append( - &steps_editor(view_model.steps(), { - let view_model = view_model.clone(); - move |s| match s { - Some(s) => view_model.set_steps(s), - None => eprintln!("have not implemented record delete"), - } - }) - .widget(), - ); - s.append(&top_row); + s.append(&control_buttons(&s, &view_model)); + s.append(&weight_and_steps_row(&view_model)); s } @@ -292,3 +248,57 @@ impl DayEdit { (self.imp().on_finished.borrow())() } } + +fn control_buttons(s: &DayEdit, view_model: &DayDetailViewModel) -> ActionGroup { + ActionGroup::builder() + .primary_action("Save", { + let s = s.clone(); + let view_model = view_model.clone(); + move || { + view_model.save(); + s.finish(); + } + }) + .secondary_action("Cancel", { + let s = s.clone(); + let view_model = view_model.clone(); + move || { + let s = s.clone(); + let view_model = view_model.clone(); + glib::spawn_future_local(async move { + view_model.revert().await; + s.finish(); + }); + } + }) + .build() +} + +fn weight_and_steps_row(view_model: &DayDetailViewModel) -> gtk::Box { + let row = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .build(); + row.append( + &weight_field(view_model.weight().map(WeightFormatter::from), { + let view_model = view_model.clone(); + move |w| match w { + Some(w) => view_model.set_weight(*w), + None => eprintln!("have not implemented record delete"), + } + }) + .widget(), + ); + + row.append( + &steps_editor(view_model.steps(), { + let view_model = view_model.clone(); + move |s| match s { + Some(s) => view_model.set_steps(s), + None => eprintln!("have not implemented record delete"), + } + }) + .widget(), + ); + + row +} diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs index f0975d7..2751abe 100644 --- a/fitnesstrax/app/src/view_models/day_detail.rs +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -374,8 +374,38 @@ impl DayDetailViewModel { } } - pub fn revert(&self) { - unimplemented!(); + pub async fn revert(&self) { + self.populate_records().await; + } + + async fn populate_records(&self) { + let records = self.provider.records(self.date, self.date).await.unwrap(); + + let (weight_records, records): (Vec>, Vec>) = + records.into_iter().partition(|r| r.data.is_weight()); + let (step_records, records): (Vec>, Vec>) = + records.into_iter().partition(|r| r.data.is_steps()); + + *self.weight.write().unwrap() = weight_records + .first() + .and_then(|r| match r.data { + TraxRecord::Weight(ref w) => Some((r.id.clone(), w.clone())), + _ => None, + }) + .map(|(id, w)| RecordState::Original(Record { id, data: w })); + + *self.steps.write().unwrap() = step_records + .first() + .and_then(|r| match r.data { + TraxRecord::Steps(ref w) => Some((r.id.clone(), w.clone())), + _ => None, + }) + .map(|(id, w)| RecordState::Original(Record { id, data: w })); + + *self.records.write().unwrap() = records + .into_iter() + .map(|r| (r.id.clone(), RecordState::Original(r))) + .collect::>>(); } } @@ -383,7 +413,7 @@ impl DayDetailViewModel { mod test { use super::*; use async_trait::async_trait; - use chrono::{DateTime, FixedOffset, TimeZone}; + use chrono::{DateTime, FixedOffset}; use dimensioned::si; use emseries::Record; @@ -640,7 +670,7 @@ mod test { #[tokio::test] async fn it_can_delete_an_existing_record() { let (view_model, provider) = create_view_model().await; - let mut workout = view_model.time_distance_records().first().cloned().unwrap(); + let workout = view_model.time_distance_records().first().cloned().unwrap(); view_model.remove_record(workout.id); assert_eq!( -- 2.44.1 From 8016188b29c37b7a5b1bbfe1e4e2e168f028fa74 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Thu, 25 Jan 2024 09:03:17 -0500 Subject: [PATCH 04/13] Add a test program for gnome icons --- Cargo.lock | 11 +++++++++++ Cargo.toml | 1 + flake.nix | 1 + icon-test/Cargo.toml | 12 ++++++++++++ icon-test/src/main.rs | 38 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+) create mode 100644 icon-test/Cargo.toml create mode 100644 icon-test/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 3aa632b..fc0a3d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -945,6 +945,7 @@ dependencies = [ "dimensioned 0.8.0", "emseries", "ft-core", + "gdk4", "gio", "glib", "glib-build-tools 0.18.0", @@ -1806,6 +1807,16 @@ dependencies = [ "cc", ] +[[package]] +name = "icon-test" +version = "0.1.0" +dependencies = [ + "gio", + "glib", + "gtk4", + "libadwaita", +] + [[package]] name = "idna" version = "0.1.5" diff --git a/Cargo.toml b/Cargo.toml index bf860b2..1812447 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "geo-types", "gm-control-panel", "hex-grid", + "icon-test", "ifc", "kifu/core", "kifu/gtk", diff --git a/flake.nix b/flake.nix index 91bc76d..70bc31b 100644 --- a/flake.nix +++ b/flake.nix @@ -32,6 +32,7 @@ pkgs.gst_all_1.gstreamer pkgs.gtk4 pkgs.libadwaita + pkgs.librsvg pkgs.nodejs pkgs.openssl pkgs.pipewire diff --git a/icon-test/Cargo.toml b/icon-test/Cargo.toml new file mode 100644 index 0000000..27b7f1c --- /dev/null +++ b/icon-test/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "icon-test" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +adw = { version = "0.5", package = "libadwaita", features = [ "v1_4" ] } +gio = { version = "0.18" } +glib = { version = "0.18" } +gtk = { version = "0.7", package = "gtk4", features = [ "v4_10" ] } diff --git a/icon-test/src/main.rs b/icon-test/src/main.rs new file mode 100644 index 0000000..7349f55 --- /dev/null +++ b/icon-test/src/main.rs @@ -0,0 +1,38 @@ +use adw::prelude::*; + +fn main() { + let adw_app = adw::Application::builder().build(); + + adw_app.connect_activate(move |adw_app| { + let window = gtk::ApplicationWindow::builder() + .application(adw_app) + .width_request(400) + .height_request(400) + .build(); + + let sunrise_button = gtk::Button::builder() + .icon_name("daytime-sunrise-symbolic") + .width_request(64) + .height_request(64) + .build(); + + let walking_button = gtk::Button::builder() + .icon_name("walking2-symbolic") + .width_request(64) + .height_request(64) + .build(); + + let layout = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .valign(gtk::Align::Start) + .build(); + layout.append(&sunrise_button); + layout.append(&walking_button); + + window.set_child(Some(&layout)); + window.present(); + }); + + let args: Vec = std::env::args().collect(); + ApplicationExtManual::run_with_args(&adw_app, &args); +} -- 2.44.1 From 792e20d44b7b201e22f051ee52fe23cf899f8ac7 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Thu, 25 Jan 2024 09:03:55 -0500 Subject: [PATCH 05/13] Add buttons with icons to represent workouts --- fitnesstrax/app/Cargo.toml | 1 + fitnesstrax/app/gresources.xml | 7 ++++ .../app/resources/resources.gresource.xml | 6 ++++ .../app/resources/running-symbolic.svg | 2 ++ .../app/resources/walking2-symbolic.svg | 2 ++ fitnesstrax/app/src/app_window.rs | 2 +- fitnesstrax/app/src/components/day.rs | 35 ++++++++++++++++++- fitnesstrax/app/src/main.rs | 3 ++ 8 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 fitnesstrax/app/resources/resources.gresource.xml create mode 100644 fitnesstrax/app/resources/running-symbolic.svg create mode 100644 fitnesstrax/app/resources/walking2-symbolic.svg diff --git a/fitnesstrax/app/Cargo.toml b/fitnesstrax/app/Cargo.toml index 3966c0c..2e7a012 100644 --- a/fitnesstrax/app/Cargo.toml +++ b/fitnesstrax/app/Cargo.toml @@ -16,6 +16,7 @@ emseries = { path = "../../emseries" } ft-core = { path = "../core" } gio = { version = "0.18" } glib = { version = "0.18" } +gdk = { version = "0.7", package = "gdk4" } gtk = { version = "0.7", package = "gtk4", features = [ "v4_10" ] } thiserror = { version = "1.0" } tokio = { version = "1.34", features = [ "full" ] } diff --git a/fitnesstrax/app/gresources.xml b/fitnesstrax/app/gresources.xml index a597670..4eafed7 100644 --- a/fitnesstrax/app/gresources.xml +++ b/fitnesstrax/app/gresources.xml @@ -3,4 +3,11 @@ style.css + + walking2-symbolic.svg + + + + running-symbolic.svg + diff --git a/fitnesstrax/app/resources/resources.gresource.xml b/fitnesstrax/app/resources/resources.gresource.xml new file mode 100644 index 0000000..67e604b --- /dev/null +++ b/fitnesstrax/app/resources/resources.gresource.xml @@ -0,0 +1,6 @@ + + + + start-here-symbolic.svg + + \ No newline at end of file diff --git a/fitnesstrax/app/resources/running-symbolic.svg b/fitnesstrax/app/resources/running-symbolic.svg new file mode 100644 index 0000000..89eba11 --- /dev/null +++ b/fitnesstrax/app/resources/running-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/fitnesstrax/app/resources/walking2-symbolic.svg b/fitnesstrax/app/resources/walking2-symbolic.svg new file mode 100644 index 0000000..c43dc8b --- /dev/null +++ b/fitnesstrax/app/resources/walking2-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/fitnesstrax/app/src/app_window.rs b/fitnesstrax/app/src/app_window.rs index 24f0484..d372119 100644 --- a/fitnesstrax/app/src/app_window.rs +++ b/fitnesstrax/app/src/app_window.rs @@ -54,7 +54,7 @@ impl AppWindow { let window = adw::ApplicationWindow::builder() .application(adw_app) .width_request(800) - .height_request(600) + .height_request(746) .build(); let stylesheet = String::from_utf8( diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 59b601e..0edcd5d 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -234,12 +234,12 @@ impl DayEdit { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); s.set_hexpand(true); - *s.imp().on_finished.borrow_mut() = Box::new(on_finished); *s.imp().view_model.borrow_mut() = Some(view_model.clone()); s.append(&control_buttons(&s, &view_model)); s.append(&weight_and_steps_row(&view_model)); + s.append(&workout_buttons()); s } @@ -302,3 +302,36 @@ fn weight_and_steps_row(view_model: &DayDetailViewModel) -> gtk::Box { row } + +fn workout_buttons() -> gtk::Box { + let sunrise_button = gtk::Button::builder() + .icon_name("daytime-sunrise-symbolic") + .width_request(64) + .height_request(64) + .build(); + + let walking_button = gtk::Button::builder() + .icon_name("walking2-symbolic") + .width_request(64) + .height_request(64) + .build(); + + let running_button = gtk::Button::builder() + .icon_name("running-symbolic") + .width_request(64) + .height_request(64) + .build(); + + let layout = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + let row = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .build(); + row.append(&sunrise_button); + row.append(&walking_button); + row.append(&running_button); + layout.append(&row); + + layout +} diff --git a/fitnesstrax/app/src/main.rs b/fitnesstrax/app/src/main.rs index 316bfa6..bbad804 100644 --- a/fitnesstrax/app/src/main.rs +++ b/fitnesstrax/app/src/main.rs @@ -62,6 +62,9 @@ fn main() { .build(); adw_app.connect_activate(move |adw_app| { + let icon_theme = gtk::IconTheme::for_display(&gdk::Display::default().unwrap()); + icon_theme.add_resource_path(&(RESOURCE_BASE_PATH.to_owned() + "/icons/scalable/actions")); + AppWindow::new(app_id, RESOURCE_BASE_PATH, adw_app, ft_app.clone()); }); -- 2.44.1 From af1422d523c95e16b7972adca813a0cfee6d1eba Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 26 Jan 2024 09:53:42 -0500 Subject: [PATCH 06/13] Build some convenienc functions for measurement entry fields Move the weight field into text_entry --- fitnesstrax/app/src/components/mod.rs | 2 +- .../app/src/components/time_distance.rs | 52 ++++++++++++++++++- fitnesstrax/app/src/components/weight.rs | 4 +- fitnesstrax/app/src/views/historical_view.rs | 5 +- 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index 792db09..59f7347 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -27,7 +27,7 @@ mod steps; pub use steps::{steps_editor, Steps}; mod text_entry; -pub use text_entry::{weight_field, TextEntry}; +pub use text_entry::{distance_field, duration_field, time_field, weight_field, TextEntry}; mod time_distance; pub use time_distance::{time_distance_detail, time_distance_summary}; diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs index 1ff3ba2..3771556 100644 --- a/fitnesstrax/app/src/components/time_distance.rs +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -18,7 +18,9 @@ You should have received a copy of the GNU General Public License along with Fit // use chrono::{Local, NaiveDate}; // use dimensioned::si; use dimensioned::si; -use gtk::prelude::*; +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; +use std::cell::RefCell; pub fn time_distance_summary( distance: si::Meter, @@ -99,5 +101,53 @@ pub fn time_distance_detail(record: ft_core::TimeDistance) -> gtk::Box { ) .build(), ); + layout } + +#[derive(Default)] +pub struct TimeDistanceEditPrivate { + #[allow(unused)] + record: RefCell>, +} + +#[glib::object_subclass] +impl ObjectSubclass for TimeDistanceEditPrivate { + const NAME: &'static str = "TimeDistanceEdit"; + type Type = TimeDistanceEdit; + type ParentType = gtk::Box; +} + +impl ObjectImpl for TimeDistanceEditPrivate {} +impl WidgetImpl for TimeDistanceEditPrivate {} +impl BoxImpl for TimeDistanceEditPrivate {} + +glib::wrapper! { + pub struct TimeDistanceEdit(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; +} + +impl Default for TimeDistanceEdit { + fn default() -> Self { + let s: Self = Object::builder().build(); + s.set_orientation(gtk::Orientation::Horizontal); + s.set_css_classes(&["time-distance-edit"]); + + s + } +} + +impl TimeDistanceEdit { + #[allow(unused)] + fn empty(_on_update: OnUpdate) -> Self + where + OnUpdate: Fn(&ft_core::TimeDistance), + { + Self::default() + } + + /* + fn with_record(type_: ft_core::RecordType, record: ft_core::TimeDistance, on_update: OnUpdate) -> Self + where OnUpdate: Fn(&ft_core::RecordType, &ft_core::TimeDistance) { + } + */ +} diff --git a/fitnesstrax/app/src/components/weight.rs b/fitnesstrax/app/src/components/weight.rs index 615f3f2..2f17c82 100644 --- a/fitnesstrax/app/src/components/weight.rs +++ b/fitnesstrax/app/src/components/weight.rs @@ -14,9 +14,7 @@ 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::{ - types::{FormatOption, WeightFormatter}, -}; +use crate::types::{FormatOption, WeightFormatter}; use gtk::prelude::*; pub struct WeightLabel { diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index acbf44b..46fbab6 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -56,6 +56,7 @@ impl ObjectSubclass for HistoricalViewPrivate { .single_click_activate(true) .build(), }; + factory.connect_bind({ let app = s.app.clone(); move |_, list_item| { @@ -119,9 +120,7 @@ impl HistoricalView { s.imp().list_view.connect_activate({ let on_select_day = on_select_day.clone(); move |s, idx| { - // This gets triggered whenever the user clicks on an item on the list. What we - // actually want to do here is to open a modal dialog that shows all of the details of - // the day and which allows the user to edit items within that dialog. + // This gets triggered whenever the user clicks on an item on the list. let item = s.model().unwrap().item(idx).unwrap(); let date = item.downcast_ref::().unwrap(); on_select_day(date.date()); -- 2.44.1 From afe693fe10af9c168ff2c2316f494e114567d4bf Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sat, 27 Jan 2024 10:36:11 -0500 Subject: [PATCH 07/13] Make emseries::Record copyable --- emseries/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/emseries/src/types.rs b/emseries/src/types.rs index 3d4eb2e..a094830 100644 --- a/emseries/src/types.rs +++ b/emseries/src/types.rs @@ -120,7 +120,7 @@ pub trait Recordable { /// Uniquely identifies a record. /// /// This is a wrapper around a basic uuid with some extra convenience methods. -#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)] pub struct RecordId(Uuid); impl Default for RecordId { -- 2.44.1 From 2c42c35dfe1f8e3cd63941a10aa50e68ed41da53 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sat, 27 Jan 2024 10:37:18 -0500 Subject: [PATCH 08/13] Build the facilities to add a new time/distance workout This adds the code to show the new records in the UI, plus it adds them to the view model. Some of the representation changed in order to facilitate linking UI elements to particular records. There are now some buttons to create workouts of various types, clicking on a button adds a new row to the UI, and it also adds a new record to the view model. Saving the view model writes the records to the database. --- emseries/src/series.rs | 15 ++- emseries/src/types.rs | 11 +++ fitnesstrax/app/src/components/day.rs | 54 ++++++++--- .../app/src/components/time_distance.rs | 14 ++- fitnesstrax/app/src/view_models/day_detail.rs | 91 +++++++++---------- fitnesstrax/core/src/types.rs | 12 --- 6 files changed, 114 insertions(+), 83 deletions(-) diff --git a/emseries/src/series.rs b/emseries/src/series.rs index 310b3f7..0e6441b 100644 --- a/emseries/src/series.rs +++ b/emseries/src/series.rs @@ -110,7 +110,7 @@ where .map_err(EmseriesReadError::JSONParseError) .and_then(Record::try_from) { - Ok(record) => records.insert(record.id.clone(), record.clone()), + Ok(record) => records.insert(record.id, record.clone()), Err(EmseriesReadError::RecordDeleted(id)) => records.remove(&id), Err(err) => return Err(err), }; @@ -124,19 +124,16 @@ where /// Put a new record into the database. A unique id will be assigned to the record and /// returned. pub fn put(&mut self, entry: T) -> Result { - let uuid = RecordId::default(); - let record = Record { - id: uuid.clone(), - data: entry, - }; + let id = RecordId::default(); + let record = Record { id, data: entry }; self.update(record)?; - Ok(uuid) + Ok(id) } /// Update an existing record. The [RecordId] of the record passed into this function must match /// the [RecordId] of a record already in the database. pub fn update(&mut self, record: Record) -> Result<(), EmseriesWriteError> { - self.records.insert(record.id.clone(), record.clone()); + self.records.insert(record.id, record.clone()); let write_res = match serde_json::to_string(&RecordOnDisk { id: record.id, data: Some(record.data), @@ -166,7 +163,7 @@ where self.records.remove(uuid); let rec: RecordOnDisk = RecordOnDisk { - id: uuid.clone(), + id: *uuid, data: None, }; match serde_json::to_string(&rec) { diff --git a/emseries/src/types.rs b/emseries/src/types.rs index a094830..e10298c 100644 --- a/emseries/src/types.rs +++ b/emseries/src/types.rs @@ -166,6 +166,17 @@ impl Record { pub fn timestamp(&self) -> Timestamp { self.data.timestamp() } + + pub fn map(self, map: Map) -> Record + where + Map: Fn(T) -> U, + U: Clone + Recordable, + { + Record { + id: self.id, + data: map(self.data), + } + } } #[cfg(test)] diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 0edcd5d..b024912 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -23,12 +23,13 @@ use crate::{ types::WeightFormatter, view_models::DayDetailViewModel, }; -use ft_core::TimeDistanceActivity; +use emseries::Record; +use ft_core::{TimeDistanceActivity, TraxRecord}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use std::cell::RefCell; +use std::{cell::RefCell, rc::Rc}; -use super::time_distance_detail; +use super::{time_distance::TimeDistanceEdit, time_distance_detail}; pub struct DaySummaryPrivate { date: gtk::Label, @@ -237,9 +238,15 @@ impl DayEdit { *s.imp().on_finished.borrow_mut() = Box::new(on_finished); *s.imp().view_model.borrow_mut() = Some(view_model.clone()); + let workout_buttons = workout_buttons(view_model.clone(), { + let s = s.clone(); + move |workout| s.add_row(workout) + }); + s.append(&control_buttons(&s, &view_model)); s.append(&weight_and_steps_row(&view_model)); - s.append(&workout_buttons()); + s.append(&*s.imp().workout_rows.borrow()); + s.append(&workout_buttons); s } @@ -247,6 +254,17 @@ impl DayEdit { fn finish(&self) { (self.imp().on_finished.borrow())() } + + fn add_row(&self, workout: Record) { + println!("add_row: {:?}", workout); + let workout_rows = self.imp().workout_rows.borrow(); + + #[allow(clippy::single_match)] + match workout.data { + TraxRecord::TimeDistance(r) => workout_rows.append(&TimeDistanceEdit::new(r, |_| {})), + _ => {} + } + } } fn control_buttons(s: &DayEdit, view_model: &DayDetailViewModel) -> ActionGroup { @@ -303,24 +321,37 @@ fn weight_and_steps_row(view_model: &DayDetailViewModel) -> gtk::Box { row } -fn workout_buttons() -> gtk::Box { - let sunrise_button = gtk::Button::builder() - .icon_name("daytime-sunrise-symbolic") - .width_request(64) - .height_request(64) - .build(); - +fn workout_buttons(view_model: DayDetailViewModel, add_row: AddRow) -> gtk::Box +where + AddRow: Fn(Record) + 'static, +{ + let add_row = Rc::new(add_row); let walking_button = gtk::Button::builder() .icon_name("walking2-symbolic") .width_request(64) .height_request(64) .build(); + walking_button.connect_clicked({ + let view_model = view_model.clone(); + let add_row = add_row.clone(); + move |_| { + let workout = view_model.new_time_distance(TimeDistanceActivity::Walking); + add_row(workout.map(TraxRecord::TimeDistance)); + } + }); let running_button = gtk::Button::builder() .icon_name("running-symbolic") .width_request(64) .height_request(64) .build(); + running_button.connect_clicked({ + let view_model = view_model.clone(); + move |_| { + let workout = view_model.new_time_distance(TimeDistanceActivity::Running); + add_row(workout.map(TraxRecord::TimeDistance)); + } + }); let layout = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) @@ -328,7 +359,6 @@ fn workout_buttons() -> gtk::Box { let row = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .build(); - row.append(&sunrise_button); row.append(&walking_button); row.append(&running_button); layout.append(&row); diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs index 3771556..bda7248 100644 --- a/fitnesstrax/app/src/components/time_distance.rs +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with Fit // use chrono::{Local, NaiveDate}; // use dimensioned::si; use dimensioned::si; +use ft_core::TimeDistance; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::cell::RefCell; @@ -130,6 +131,7 @@ impl Default for TimeDistanceEdit { fn default() -> Self { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Horizontal); + s.set_hexpand(true); s.set_css_classes(&["time-distance-edit"]); s @@ -137,12 +139,18 @@ impl Default for TimeDistanceEdit { } impl TimeDistanceEdit { - #[allow(unused)] - fn empty(_on_update: OnUpdate) -> Self + pub fn new(record: TimeDistance, _on_update: OnUpdate) -> Self where OnUpdate: Fn(&ft_core::TimeDistance), { - Self::default() + println!("new TimeDistanceEdit"); + let s = Self::default(); + + s.append(>k::Label::new(Some( + record.datetime.format("%H:%M").to_string().as_ref(), + ))); + + s } /* diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs index 2751abe..7339168 100644 --- a/fitnesstrax/app/src/view_models/day_detail.rs +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -30,7 +30,7 @@ use std::{ #[derive(Clone, Debug)] enum RecordState { Original(Record), - New(T), + New(Record), Updated(Record), Deleted(Record), } @@ -57,19 +57,13 @@ impl RecordState { fn set_value(&mut self, value: T) { *self = match self { - RecordState::Original(r) => RecordState::Updated(Record { - id: r.id.clone(), - data: value, - }), - RecordState::New(_) => RecordState::New(value), - RecordState::Updated(r) => RecordState::Updated(Record { - id: r.id.clone(), - data: value, - }), - RecordState::Deleted(r) => RecordState::Updated(Record { - id: r.id.clone(), + RecordState::Original(r) => RecordState::Updated(Record { data: value, ..*r }), + RecordState::New(_) => RecordState::New(Record { + id: RecordId::default(), data: value, }), + RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..*r }), + RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..*r }), }; } @@ -94,7 +88,7 @@ impl Deref for RecordState { fn deref(&self) -> &Self::Target { match self { RecordState::Original(ref r) => &r.data, - RecordState::New(ref r) => r, + RecordState::New(ref r) => &r.data, RecordState::Updated(ref r) => &r.data, RecordState::Deleted(ref r) => &r.data, } @@ -105,7 +99,7 @@ impl std::ops::DerefMut for RecordState { fn deref_mut(&mut self) -> &mut Self::Target { match self { RecordState::Original(ref mut r) => &mut r.data, - RecordState::New(ref mut r) => r, + RecordState::New(ref mut r) => &mut r.data, RecordState::Updated(ref mut r) => &mut r.data, RecordState::Deleted(ref mut r) => &mut r.data, } @@ -149,7 +143,7 @@ impl DayDetailViewModel { weight_records .first() .and_then(|r| match r.data { - TraxRecord::Weight(ref w) => Some((r.id.clone(), w.clone())), + TraxRecord::Weight(ref w) => Some((r.id, w.clone())), _ => None, }) .map(|(id, w)| RecordState::Original(Record { id, data: w })), @@ -158,7 +152,7 @@ impl DayDetailViewModel { step_records .first() .and_then(|r| match r.data { - TraxRecord::Steps(ref w) => Some((r.id.clone(), w.clone())), + TraxRecord::Steps(ref w) => Some((r.id, w.clone())), _ => None, }) .map(|(id, w)| RecordState::Original(Record { id, data: w })), @@ -167,7 +161,7 @@ impl DayDetailViewModel { records: Arc::new(RwLock::new( records .into_iter() - .map(|r| (r.id.clone(), RecordState::Original(r))) + .map(|r| (r.id, RecordState::Original(r))) .collect::>>(), )), }) @@ -184,9 +178,12 @@ impl DayDetailViewModel { date: self.date, weight: new_weight, }), - None => RecordState::New(ft_core::Weight { - date: self.date, - weight: new_weight, + None => RecordState::New(Record { + id: RecordId::default(), + data: ft_core::Weight { + date: self.date, + weight: new_weight, + }, }), }; *record = Some(new_record); @@ -203,9 +200,12 @@ impl DayDetailViewModel { date: self.date, count: new_count, }), - None => RecordState::New(ft_core::Steps { - date: self.date, - count: new_count, + None => RecordState::New(Record { + id: RecordId::default(), + data: ft_core::Steps { + date: self.date, + count: new_count, + }, }), }; *record = Some(new_record); @@ -224,7 +224,7 @@ impl DayDetailViewModel { self.records .write() .unwrap() - .insert(id.clone(), RecordState::New(tr)); + .insert(id, RecordState::New(Record { id, data: tr })); println!( "records after new_time_distance: {:?}", self.records.read().unwrap() @@ -233,11 +233,10 @@ impl DayDetailViewModel { } pub fn update_time_distance(&self, workout: Record) { - let id = workout.id.clone(); let data = workout.data.clone(); let mut record_set = self.records.write().unwrap(); - record_set.entry(id).and_modify(|record_state| { + record_set.entry(workout.id).and_modify(|record_state| { record_state.set_value(TraxRecord::TimeDistance(data)); }); } @@ -250,7 +249,7 @@ impl DayDetailViewModel { .filter(|(_, record)| record.exists()) .filter_map(|(id, record_state)| match **record_state { TraxRecord::TimeDistance(ref workout) => Some(Record { - id: id.clone(), + id: *id, data: workout.clone(), }), _ => None, @@ -283,7 +282,7 @@ impl DayDetailViewModel { fn get_record(&self, id: &RecordId) -> Option> { let record_set = self.records.read().unwrap(); record_set.get(id).map(|record| Record { - id: id.clone(), + id: *id, data: (**record).clone(), }) } @@ -303,18 +302,19 @@ impl DayDetailViewModel { } pub fn save(&self) { - glib::spawn_future({ - let s = self.clone(); - async move { s.async_save().await } - }); + let s = self.clone(); + + glib::spawn_future(async move { s.async_save().await }); } pub async fn async_save(&self) { - println!("async_save"); let weight_record = self.weight.read().unwrap().clone(); match weight_record { Some(RecordState::New(data)) => { - let _ = self.provider.put_record(TraxRecord::Weight(data)).await; + let _ = self + .provider + .put_record(TraxRecord::Weight(data.data)) + .await; } Some(RecordState::Original(_)) => {} Some(RecordState::Updated(weight)) => { @@ -333,7 +333,7 @@ impl DayDetailViewModel { let steps_record = self.steps.read().unwrap().clone(); match steps_record { Some(RecordState::New(data)) => { - let _ = self.provider.put_record(TraxRecord::Steps(data)).await; + let _ = self.provider.put_record(TraxRecord::Steps(data.data)).await; } Some(RecordState::Original(_)) => {} Some(RecordState::Updated(steps)) => { @@ -361,7 +361,7 @@ impl DayDetailViewModel { println!("saving record: {:?}", record); match record { RecordState::New(data) => { - let _ = self.provider.put_record(data).await; + let _ = self.provider.put_record(data.data).await; } RecordState::Original(_) => {} RecordState::Updated(r) => { @@ -389,7 +389,7 @@ impl DayDetailViewModel { *self.weight.write().unwrap() = weight_records .first() .and_then(|r| match r.data { - TraxRecord::Weight(ref w) => Some((r.id.clone(), w.clone())), + TraxRecord::Weight(ref w) => Some((r.id, w.clone())), _ => None, }) .map(|(id, w)| RecordState::Original(Record { id, data: w })); @@ -397,14 +397,14 @@ impl DayDetailViewModel { *self.steps.write().unwrap() = step_records .first() .and_then(|r| match r.data { - TraxRecord::Steps(ref w) => Some((r.id.clone(), w.clone())), + TraxRecord::Steps(ref w) => Some((r.id, w.clone())), _ => None, }) .map(|(id, w)| RecordState::Original(Record { id, data: w })); *self.records.write().unwrap() = records .into_iter() - .map(|r| (r.id.clone(), RecordState::Original(r))) + .map(|r| (r.id, RecordState::Original(r))) .collect::>>(); } } @@ -430,7 +430,7 @@ mod test { fn new(records: Vec>) -> Self { let record_map = records .into_iter() - .map(|r| (r.id.clone(), r)) + .map(|r| (r.id, r)) .collect::>>(); Self { records: Arc::new(RwLock::new(record_map)), @@ -464,26 +464,23 @@ mod test { async fn put_record(&self, record: TraxRecord) -> Result { let id = RecordId::default(); let record = Record { - id: id.clone(), + id: id, data: record, }; self.put_records.write().unwrap().push(record.clone()); - self.records.write().unwrap().insert(id.clone(), record); + self.records.write().unwrap().insert(id, record); Ok(id) } async fn update_record(&self, record: Record) -> Result<(), WriteError> { println!("updated record: {:?}", record); self.updated_records.write().unwrap().push(record.clone()); - self.records - .write() - .unwrap() - .insert(record.id.clone(), record); + self.records.write().unwrap().insert(record.id, record); Ok(()) } async fn delete_record(&self, id: RecordId) -> Result<(), WriteError> { - self.deleted_records.write().unwrap().push(id.clone()); + self.deleted_records.write().unwrap().push(id); let _ = self.records.write().unwrap().remove(&id); Ok(()) } diff --git a/fitnesstrax/core/src/types.rs b/fitnesstrax/core/src/types.rs index c50d4eb..a48da32 100644 --- a/fitnesstrax/core/src/types.rs +++ b/fitnesstrax/core/src/types.rs @@ -134,18 +134,6 @@ impl TraxRecord { }) ) } - - /* - pub fn is_time_distance_type(&self, type_: TimeDistanceActivity) -> bool { - match type_ { - TimeDistanceWorkoutType::BikeRide => matches!(self, TraxRecord::BikeRide(_)), - TimeDistanceWorkoutType::Row => matches!(self, TraxRecord::Row(_)), - TimeDistanceWorkoutType::Run => matches!(self, TraxRecord::Run(_)), - TimeDistanceWorkoutType::Swim => matches!(self, TraxRecord::Swim(_)), - TimeDistanceWorkoutType::Walk => matches!(self, TraxRecord::Walk(_)), - } - } - */ } impl Recordable for TraxRecord { -- 2.44.1 From 73052a0694338c5ce7dcff53a91657c9169674aa Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 28 Jan 2024 14:00:09 -0500 Subject: [PATCH 09/13] Save new time/distance records This sets up a bunch of callbacks. We're starting to get into Callback Hell, where there are things that need knowledge that I really don't want them to have. However, edit fields for TimeDistanceEdit now propogate data back into the view model, which is then able to save the results. --- fitnesstrax/app/src/components/day.rs | 23 ++++- fitnesstrax/app/src/components/steps.rs | 2 +- fitnesstrax/app/src/components/text_entry.rs | 4 +- .../app/src/components/time_distance.rs | 90 +++++++++++++++---- fitnesstrax/app/src/view_models/day_detail.rs | 47 +++++----- 5 files changed, 120 insertions(+), 46 deletions(-) diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index b024912..ede6968 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -23,7 +23,7 @@ use crate::{ types::WeightFormatter, view_models::DayDetailViewModel, }; -use emseries::Record; +use emseries::{Record, RecordId}; use ft_core::{TimeDistanceActivity, TraxRecord}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; @@ -256,15 +256,32 @@ impl DayEdit { } fn add_row(&self, workout: Record) { - println!("add_row: {:?}", workout); + println!("adding a row for {:?}", workout); let workout_rows = self.imp().workout_rows.borrow(); #[allow(clippy::single_match)] match workout.data { - TraxRecord::TimeDistance(r) => workout_rows.append(&TimeDistanceEdit::new(r, |_| {})), + TraxRecord::TimeDistance(r) => workout_rows.append(&TimeDistanceEdit::new(r, { + let s = self.clone(); + move |data| { + println!("update workout callback on workout: {:?}", workout.id); + s.update_workout(workout.id, data) + } + })), + _ => {} } } + + fn update_workout(&self, id: RecordId, data: ft_core::TimeDistance) { + if let Some(ref view_model) = *self.imp().view_model.borrow() { + let record = Record { + id, + data: TraxRecord::TimeDistance(data), + }; + view_model.update_record(record); + } + } } fn control_buttons(s: &DayEdit, view_model: &DayDetailViewModel) -> ActionGroup { diff --git a/fitnesstrax/app/src/components/steps.rs b/fitnesstrax/app/src/components/steps.rs index 391b43a..11ba591 100644 --- a/fitnesstrax/app/src/components/steps.rs +++ b/fitnesstrax/app/src/components/steps.rs @@ -50,7 +50,7 @@ where "0", value, |v| format!("{}", v), - move |v| v.parse::().map_err(|_| ParseError), + |v| v.parse::().map_err(|_| ParseError), on_update, ) } diff --git a/fitnesstrax/app/src/components/text_entry.rs b/fitnesstrax/app/src/components/text_entry.rs index 9cf0542..a8f89cb 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -20,8 +20,8 @@ use crate::types::{ use gtk::prelude::*; use std::{cell::RefCell, rc::Rc}; -type Parser = dyn Fn(&str) -> Result; -type OnUpdate = dyn Fn(Option); +pub type Parser = dyn Fn(&str) -> Result; +pub type OnUpdate = dyn Fn(Option); #[derive(Clone)] pub struct TextEntry { diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs index bda7248..ee770fb 100644 --- a/fitnesstrax/app/src/components/time_distance.rs +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -17,11 +17,15 @@ You should have received a copy of the GNU General Public License along with Fit // use crate::components::{EditView, ParseError, TextEntry}; // use chrono::{Local, NaiveDate}; // use dimensioned::si; +use crate::{ + components::{distance_field, duration_field, time_field}, + types::{DistanceFormatter, DurationFormatter, TimeFormatter}, +}; use dimensioned::si; -use ft_core::TimeDistance; +use ft_core::{TimeDistance, TimeDistanceActivity}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use std::cell::RefCell; +use std::{cell::RefCell, rc::Rc}; pub fn time_distance_summary( distance: si::Meter, @@ -106,10 +110,27 @@ pub fn time_distance_detail(record: ft_core::TimeDistance) -> gtk::Box { layout } -#[derive(Default)] +type OnUpdate = Rc>>; + pub struct TimeDistanceEditPrivate { #[allow(unused)] - record: RefCell>, + workout: RefCell, + on_update: OnUpdate, +} + +impl Default for TimeDistanceEditPrivate { + fn default() -> Self { + Self { + workout: RefCell::new(TimeDistance { + datetime: chrono::Utc::now().into(), + activity: TimeDistanceActivity::BikeRide, + duration: None, + distance: None, + comments: None, + }), + on_update: Rc::new(RefCell::new(Box::new(|_| {}))), + } + } } #[glib::object_subclass] @@ -130,7 +151,7 @@ glib::wrapper! { impl Default for TimeDistanceEdit { fn default() -> Self { let s: Self = Object::builder().build(); - s.set_orientation(gtk::Orientation::Horizontal); + s.set_orientation(gtk::Orientation::Vertical); s.set_hexpand(true); s.set_css_classes(&["time-distance-edit"]); @@ -139,23 +160,62 @@ impl Default for TimeDistanceEdit { } impl TimeDistanceEdit { - pub fn new(record: TimeDistance, _on_update: OnUpdate) -> Self + pub fn new(workout: TimeDistance, on_update: OnUpdate) -> Self where - OnUpdate: Fn(&ft_core::TimeDistance), + OnUpdate: Fn(TimeDistance) + 'static, { - println!("new TimeDistanceEdit"); let s = Self::default(); - s.append(>k::Label::new(Some( - record.datetime.format("%H:%M").to_string().as_ref(), - ))); + *s.imp().workout.borrow_mut() = workout.clone(); + *s.imp().on_update.borrow_mut() = Box::new(on_update); + + let details_row = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .build(); + + details_row.append( + &time_field( + Some(TimeFormatter::from(workout.datetime.naive_local().time())), + { + let s = s.clone(); + move |t| s.update_time(t) + }, + ) + .widget(), + ); + details_row.append( + &distance_field(workout.distance.map(DistanceFormatter::from), { + let s = s.clone(); + move |d| s.update_distance(d) + }) + .widget(), + ); + details_row.append( + &duration_field(workout.duration.map(DurationFormatter::from), { + let s = s.clone(); + move |d| s.update_duration(d) + }) + .widget(), + ); + s.append(&details_row); + s.append(>k::Entry::new()); s } - /* - fn with_record(type_: ft_core::RecordType, record: ft_core::TimeDistance, on_update: OnUpdate) -> Self - where OnUpdate: Fn(&ft_core::RecordType, &ft_core::TimeDistance) { + fn update_time(&self, _time: Option) { + unimplemented!() + } + + fn update_distance(&self, distance: Option) { + let mut workout = self.imp().workout.borrow_mut(); + workout.distance = distance.map(|d| *d); + (self.imp().on_update.borrow())(workout.clone()); + } + + fn update_duration(&self, duration: Option) { + let mut workout = self.imp().workout.borrow_mut(); + workout.duration = duration.map(|d| *d); + (self.imp().on_update.borrow())(workout.clone()); } - */ } diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs index 7339168..730be85 100644 --- a/fitnesstrax/app/src/view_models/day_detail.rs +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -58,10 +58,7 @@ impl RecordState { fn set_value(&mut self, value: T) { *self = match self { RecordState::Original(r) => RecordState::Updated(Record { data: value, ..*r }), - RecordState::New(_) => RecordState::New(Record { - id: RecordId::default(), - data: value, - }), + RecordState::New(r) => RecordState::New(Record { data: value, ..*r }), RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..*r }), RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..*r }), }; @@ -225,22 +222,9 @@ impl DayDetailViewModel { .write() .unwrap() .insert(id, RecordState::New(Record { id, data: tr })); - println!( - "records after new_time_distance: {:?}", - self.records.read().unwrap() - ); Record { id, data: workout } } - pub fn update_time_distance(&self, workout: Record) { - let data = workout.data.clone(); - - let mut record_set = self.records.write().unwrap(); - record_set.entry(workout.id).and_modify(|record_state| { - record_state.set_value(TraxRecord::TimeDistance(data)); - }); - } - pub fn time_distance_records(&self) -> Vec> { self.records .read() @@ -278,6 +262,24 @@ impl DayDetailViewModel { ) } + pub fn update_record(&self, update: Record) { + println!("updating a record: {:?}", update); + let mut records = self.records.write().unwrap(); + records + .entry(update.id) + .and_modify(|record| record.set_value(update.data)); + println!("record updated: {:?}", records.get(&update.id)); + } + + pub fn records(&self) -> Vec> { + let read_lock = self.records.read().unwrap(); + read_lock + .iter() + .filter_map(|(_, record_state)| record_state.data()) + .cloned() + .collect::>>() + } + #[allow(unused)] fn get_record(&self, id: &RecordId) -> Option> { let record_set = self.records.read().unwrap(); @@ -604,11 +606,8 @@ mod test { let mut record = view_model.new_time_distance(TimeDistanceActivity::BikeRide); record.data.duration = Some(60. * si::S); - view_model.update_time_distance(record.clone()); - let record = Record { - id: record.id, - data: TraxRecord::TimeDistance(record.data), - }; + let record = record.map(TraxRecord::TimeDistance); + view_model.update_record(record.clone()); assert_eq!(view_model.get_record(&record.id), Some(record)); assert_eq!( view_model.time_distance_summary(TimeDistanceActivity::BikeRide), @@ -630,10 +629,8 @@ mod test { let (view_model, provider) = create_view_model().await; let mut workout = view_model.time_distance_records().first().cloned().unwrap(); - println!("found record: {:?}", workout); - workout.data.duration = Some(1800. * si::S); - view_model.update_time_distance(workout.clone()); + view_model.update_record(workout.map(TraxRecord::TimeDistance)); assert_eq!( view_model.time_distance_summary(TimeDistanceActivity::BikeRide), -- 2.44.1 From 76f4b314660e45fc5664da45c18c62ba9142398f Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 28 Jan 2024 16:28:27 -0500 Subject: [PATCH 10/13] Show existing time/distance workout rows in day detail and editor --- fitnesstrax/app/gresources.xml | 4 +++ .../app/resources/cycling-symbolic.svg | 2 ++ .../app/resources/resources.gresource.xml | 6 ---- fitnesstrax/app/src/components/day.rs | 35 +++++++++++++++++-- 4 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 fitnesstrax/app/resources/cycling-symbolic.svg delete mode 100644 fitnesstrax/app/resources/resources.gresource.xml diff --git a/fitnesstrax/app/gresources.xml b/fitnesstrax/app/gresources.xml index 4eafed7..b26eb63 100644 --- a/fitnesstrax/app/gresources.xml +++ b/fitnesstrax/app/gresources.xml @@ -10,4 +10,8 @@ running-symbolic.svg + + + cycling-symbolic.svg + diff --git a/fitnesstrax/app/resources/cycling-symbolic.svg b/fitnesstrax/app/resources/cycling-symbolic.svg new file mode 100644 index 0000000..d1a3f13 --- /dev/null +++ b/fitnesstrax/app/resources/cycling-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/fitnesstrax/app/resources/resources.gresource.xml b/fitnesstrax/app/resources/resources.gresource.xml deleted file mode 100644 index 67e604b..0000000 --- a/fitnesstrax/app/resources/resources.gresource.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - start-here-symbolic.svg - - \ No newline at end of file diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index ede6968..575e733 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -243,6 +243,23 @@ impl DayEdit { move |workout| s.add_row(workout) }); + view_model + .records() + .into_iter() + .filter_map({ + let s = s.clone(); + move |record| match record.data { + TraxRecord::TimeDistance(workout) => Some(TimeDistanceEdit::new(workout, { + let s = s.clone(); + move |data| { + s.update_workout(record.id, data); + } + })), + _ => None, + } + }) + .for_each(|row| s.imp().workout_rows.borrow().append(&row)); + s.append(&control_buttons(&s, &view_model)); s.append(&weight_and_steps_row(&view_model)); s.append(&*s.imp().workout_rows.borrow()); @@ -343,6 +360,7 @@ where AddRow: Fn(Record) + 'static, { let add_row = Rc::new(add_row); + /* let walking_button = gtk::Button::builder() .icon_name("walking2-symbolic") .width_request(64) @@ -369,6 +387,20 @@ where add_row(workout.map(TraxRecord::TimeDistance)); } }); + */ + + let biking_button = gtk::Button::builder() + .icon_name("cycling-symbolic") + .width_request(64) + .height_request(64) + .build(); + biking_button.connect_clicked({ + let view_model = view_model.clone(); + move |_| { + let workout = view_model.new_time_distance(TimeDistanceActivity::Walking); + add_row(workout.map(TraxRecord::TimeDistance)); + } + }); let layout = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) @@ -376,8 +408,7 @@ where let row = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .build(); - row.append(&walking_button); - row.append(&running_button); + row.append(&biking_button); layout.append(&row); layout -- 2.44.1 From 9fc9d2b758872782977fbb837accb31b9c3a5649 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 29 Jan 2024 08:26:41 -0500 Subject: [PATCH 11/13] Create Duration and Distance structures to handle rendering These structures handle parsing and rendering of a Duration and a Distance, allowing that knowledge to be centralized and reused. Then I'm using those structures in a variety of places in order to ensure that the information gets rendered consistently. --- fitnesstrax/app/src/components/day.rs | 9 ++++--- .../app/src/components/time_distance.rs | 27 +++++++++---------- fitnesstrax/app/src/view_models/day_detail.rs | 11 +++++--- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 575e733..bef67bf 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -20,7 +20,7 @@ use crate::{ components::{ steps_editor, time_distance_summary, weight_field, ActionGroup, Steps, WeightLabel, }, - types::WeightFormatter, + types::{DistanceFormatter, DurationFormatter, WeightFormatter}, view_models::DayDetailViewModel, }; use emseries::{Record, RecordId}; @@ -106,7 +106,10 @@ impl DaySummary { self.append(&row); let biking_summary = view_model.time_distance_summary(TimeDistanceActivity::BikeRide); - if let Some(label) = time_distance_summary(biking_summary.0, biking_summary.1) { + if let Some(label) = time_distance_summary( + DistanceFormatter::from(biking_summary.0), + DurationFormatter::from(biking_summary.1), + ) { self.append(&label); } } @@ -397,7 +400,7 @@ where biking_button.connect_clicked({ let view_model = view_model.clone(); move |_| { - let workout = view_model.new_time_distance(TimeDistanceActivity::Walking); + let workout = view_model.new_time_distance(TimeDistanceActivity::BikeRide); add_row(workout.map(TraxRecord::TimeDistance)); } }); diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs index ee770fb..02bfe27 100644 --- a/fitnesstrax/app/src/components/time_distance.rs +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -19,7 +19,7 @@ You should have received a copy of the GNU General Public License along with Fit // use dimensioned::si; use crate::{ components::{distance_field, duration_field, time_field}, - types::{DistanceFormatter, DurationFormatter, TimeFormatter}, + types::{DistanceFormatter, DurationFormatter, FormatOption, TimeFormatter}, }; use dimensioned::si; use ft_core::{TimeDistance, TimeDistanceActivity}; @@ -28,20 +28,17 @@ use gtk::{prelude::*, subclass::prelude::*}; use std::{cell::RefCell, rc::Rc}; pub fn time_distance_summary( - distance: si::Meter, - duration: si::Second, + distance: DistanceFormatter, + duration: DurationFormatter, ) -> Option { - let text = match (distance > si::M, duration > si::S) { + let text = match (*distance > si::M, *duration > si::S) { (true, true) => Some(format!( - "{} kilometers of biking in {} minutes", - distance.value_unsafe / 1000., - duration.value_unsafe / 60. + "{} of biking in {}", + distance.format(FormatOption::Full), + duration.format(FormatOption::Full) )), - (true, false) => Some(format!( - "{} kilometers of biking", - distance.value_unsafe / 1000. - )), - (false, true) => Some(format!("{} seconds of biking", duration.value_unsafe / 60.)), + (true, false) => Some(format!("{} of biking", distance.format(FormatOption::Full))), + (false, true) => Some(format!("{} of biking", duration.format(FormatOption::Full))), (false, false) => None, }; @@ -75,7 +72,7 @@ pub fn time_distance_detail(record: ft_core::TimeDistance) -> gtk::Box { .label( record .distance - .map(|dist| format!("{}", dist)) + .map(|dist| DistanceFormatter::from(dist).format(FormatOption::Abbreviated)) .unwrap_or("".to_owned()), ) .build(), @@ -87,7 +84,9 @@ pub fn time_distance_detail(record: ft_core::TimeDistance) -> gtk::Box { .label( record .duration - .map(|duration| format!("{}", duration)) + .map(|duration| { + DurationFormatter::from(duration).format(FormatOption::Abbreviated) + }) .unwrap_or("".to_owned()), ) .build(), diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs index 730be85..7e17388 100644 --- a/fitnesstrax/app/src/view_models/day_detail.rs +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -14,10 +14,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ -#[allow(unused_imports)] -use crate::app::{ReadError, RecordProvider, WriteError}; -#[allow(unused_imports)] -use chrono::NaiveDate; +use crate::app::{ReadError, RecordProvider}; use dimensioned::si; use emseries::{Record, RecordId, Recordable}; use ft_core::{TimeDistance, TimeDistanceActivity, TraxRecord}; @@ -27,6 +24,12 @@ use std::{ sync::{Arc, RwLock}, }; +// These are actually a used imports. Clippy isn't detecting their use, probably because of complexity around the async trait macros. +#[allow(unused_imports)] +use crate::app::WriteError; +#[allow(unused_imports)] +use chrono::NaiveDate; + #[derive(Clone, Debug)] enum RecordState { Original(Record), -- 2.44.1 From 1aff203afc01613f2a667c530214e9272dcfffd4 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 29 Jan 2024 10:22:21 -0500 Subject: [PATCH 12/13] Reload data when the user saves on the DayEdit panel This required some big overhauls. The view model no longer takes records. It only takes the date that it is responsible for, and it will ask the database for records pertaining to that date. This means that once the view model has saved all of its records, it can simply reload those records from the database. This has the effect that as soon as the user moves from DayEdit back to DayDetail, all of the interesting information has been repopulated. --- fitnesstrax/app/src/app.rs | 14 +++++ fitnesstrax/app/src/app_window.rs | 17 +++++-- fitnesstrax/app/src/components/day.rs | 46 ++++++----------- fitnesstrax/app/src/view_models/day_detail.rs | 51 +++---------------- fitnesstrax/app/src/views/historical_view.rs | 45 ++++++++-------- 5 files changed, 69 insertions(+), 104 deletions(-) diff --git a/fitnesstrax/app/src/app.rs b/fitnesstrax/app/src/app.rs index 2aafc94..94c83b8 100644 --- a/fitnesstrax/app/src/app.rs +++ b/fitnesstrax/app/src/app.rs @@ -95,6 +95,20 @@ impl App { .await .unwrap() } + + pub async fn get_record(&self, id: RecordId) -> Result>, AppError> { + let db = self.database.clone(); + self.runtime + .spawn_blocking(move || { + if let Some(ref db) = *db.read().unwrap() { + Ok(db.get(&id)) + } else { + Err(AppError::NoDatabase) + } + }) + .await + .unwrap() + } } #[async_trait] diff --git a/fitnesstrax/app/src/app_window.rs b/fitnesstrax/app/src/app_window.rs index d372119..238e2f3 100644 --- a/fitnesstrax/app/src/app_window.rs +++ b/fitnesstrax/app/src/app_window.rs @@ -135,14 +135,15 @@ impl AppWindow { } fn show_historical_view(&self, interval: DayInterval) { - let view = View::Historical(HistoricalView::new(self.app.clone(), interval, { + let on_select_day = { let s = self.clone(); - Rc::new(move |date| { + move |date| { let s = s.clone(); glib::spawn_future_local(async move { + let view_model = DayDetailViewModel::new(date, s.app.clone()).await.unwrap(); let layout = gtk::Box::new(gtk::Orientation::Vertical, 0); layout.append(&adw::HeaderBar::new()); - let view_model = DayDetailViewModel::new(date, s.app.clone()).await.unwrap(); + // layout.append(&DayDetailView::new(date, records, s.app.clone())); layout.append(&DayDetailView::new(view_model)); let page = &adw::NavigationPage::builder() .title(date.format("%Y-%m-%d").to_string()) @@ -150,8 +151,14 @@ impl AppWindow { .build(); s.navigation.push(page); }); - }) - })); + } + }; + + let view = View::Historical(HistoricalView::new( + self.app.clone(), + interval, + Rc::new(on_select_day), + )); self.swap_main(view); } diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index bef67bf..a2cfd1e 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -148,31 +148,6 @@ impl DayDetail { .build(), ); - /* - let click_controller = gtk::GestureClick::new(); - click_controller.connect_released({ - let s = s.clone(); - move |_, _, _, _| { - println!("clicked outside of focusable entity"); - if let Some(widget) = s.focus_child().and_downcast_ref::() { - println!("focused child is the weight view"); - widget.blur(); - } - } - }); - s.add_controller(click_controller); - */ - - /* - let weight_record = records.iter().find_map(|record| match record { - Record { - id, - data: ft_core::TraxRecord::Weight(record), - } => Some((id.clone(), record.clone())), - _ => None, - }); - */ - let top_row = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .build(); @@ -272,7 +247,20 @@ impl DayEdit { } fn finish(&self) { - (self.imp().on_finished.borrow())() + glib::spawn_future_local({ + let s = self.clone(); + async move { + let view_model = { + let view_model = s.imp().view_model.borrow(); + view_model + .as_ref() + .expect("DayEdit has not been initialized with the view model") + .clone() + }; + let _ = view_model.async_save().await; + (s.imp().on_finished.borrow())() + } + }); } fn add_row(&self, workout: Record) { @@ -308,11 +296,7 @@ fn control_buttons(s: &DayEdit, view_model: &DayDetailViewModel) -> ActionGroup ActionGroup::builder() .primary_action("Save", { let s = s.clone(); - let view_model = view_model.clone(); - move || { - view_model.save(); - s.finish(); - } + move || s.finish() }) .secondary_action("Cancel", { let s = s.clone(); diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs index 7e17388..21688c0 100644 --- a/fitnesstrax/app/src/view_models/day_detail.rs +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -120,51 +120,15 @@ impl DayDetailViewModel { date: chrono::NaiveDate, provider: impl RecordProvider + 'static, ) -> Result { - let (weight_records, records): (Vec>, Vec>) = - provider - .records(date, date) - .await? - .into_iter() - .partition(|r| r.data.is_weight()); - let (step_records, records): (Vec>, Vec>) = - records.into_iter().partition(|r| r.data.is_steps()); - - if weight_records.len() > 1 { - eprintln!("warning: multiple weight records found for {}. This is unsupported and the one presented is unpredictable.", date.format("%Y-%m-%d")); - } - if step_records.len() > 1 { - eprintln!("warning: multiple step records found for {}. This is unsupported and the one presented is unpredictable.", date.format("%Y-%m-%d")); - } - - Ok(Self { + let s = Self { provider: Arc::new(provider), date, - weight: Arc::new(RwLock::new( - weight_records - .first() - .and_then(|r| match r.data { - TraxRecord::Weight(ref w) => Some((r.id, w.clone())), - _ => None, - }) - .map(|(id, w)| RecordState::Original(Record { id, data: w })), - )), - steps: Arc::new(RwLock::new( - step_records - .first() - .and_then(|r| match r.data { - TraxRecord::Steps(ref w) => Some((r.id, w.clone())), - _ => None, - }) - .map(|(id, w)| RecordState::Original(Record { id, data: w })), - )), - - records: Arc::new(RwLock::new( - records - .into_iter() - .map(|r| (r.id, RecordState::Original(r))) - .collect::>>(), - )), - }) + weight: Arc::new(RwLock::new(None)), + steps: Arc::new(RwLock::new(None)), + records: Arc::new(RwLock::new(HashMap::new())), + }; + s.populate_records().await; + Ok(s) } pub fn weight(&self) -> Option> { @@ -377,6 +341,7 @@ impl DayDetailViewModel { } } } + self.populate_records().await; } pub async fn revert(&self) { diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index 46fbab6..ead6184 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -18,8 +18,6 @@ use crate::{ app::App, components::DaySummary, types::DayInterval, view_models::DayDetailViewModel, }; - - use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::{cell::RefCell, rc::Rc}; @@ -60,31 +58,28 @@ impl ObjectSubclass for HistoricalViewPrivate { factory.connect_bind({ let app = s.app.clone(); move |_, list_item| { - let app = app.clone(); - let list_item = list_item.clone(); - glib::spawn_future_local(async move { - let date = list_item - .downcast_ref::() - .expect("should be a ListItem") - .item() - .and_downcast::() - .expect("should be a DaySummary"); + let date = list_item + .downcast_ref::() + .expect("should be a ListItem") + .item() + .and_downcast::() + .expect("should be a Date"); - let summary = list_item - .downcast_ref::() - .expect("should be a ListItem") - .child() - .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() { - glib::spawn_future_local(async move { - let view_model = - DayDetailViewModel::new(date.date(), app).await.unwrap(); - summary.set_data(view_model); - }); - } - }); + if let Some(app) = app.borrow().clone() { + glib::spawn_future_local(async move { + let view_model = DayDetailViewModel::new(date.date(), app.clone()) + .await + .unwrap(); + summary.set_data(view_model); + }); + } } }); -- 2.44.1 From 4acf034b8d4da82b0cac3673202d2e248b4e46af Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 29 Jan 2024 23:42:14 -0500 Subject: [PATCH 13/13] Clean up warnings and remove printlns --- emseries/src/types.rs | 2 +- emseries/tests/test_io.rs | 11 +++++------ fitnesstrax/app/src/app_window.rs | 4 ---- fitnesstrax/app/src/components/day.rs | 1 - fitnesstrax/app/src/components/mod.rs | 3 ++- fitnesstrax/app/src/components/time_distance.rs | 2 +- fitnesstrax/app/src/types.rs | 8 -------- fitnesstrax/app/src/view_models/day_detail.rs | 2 -- 8 files changed, 9 insertions(+), 24 deletions(-) diff --git a/emseries/src/types.rs b/emseries/src/types.rs index e10298c..9f92395 100644 --- a/emseries/src/types.rs +++ b/emseries/src/types.rs @@ -201,7 +201,7 @@ mod test { impl Recordable for WeightRecord { fn timestamp(&self) -> Timestamp { - Timestamp::Date(self.date.clone()) + Timestamp::Date(self.date) } fn tags(&self) -> Vec { diff --git a/emseries/tests/test_io.rs b/emseries/tests/test_io.rs index 77142c4..330a875 100644 --- a/emseries/tests/test_io.rs +++ b/emseries/tests/test_io.rs @@ -20,7 +20,7 @@ extern crate emseries; #[cfg(test)] mod test { - use chrono::{format::Fixed, prelude::*}; + use chrono::{prelude::*}; use chrono_tz::Etc::UTC; use dimensioned::si::{Kilogram, Meter, Second, M, S}; @@ -42,7 +42,7 @@ mod test { impl Recordable for BikeTrip { fn timestamp(&self) -> Timestamp { - Timestamp::DateTime(self.datetime.clone()) + Timestamp::DateTime(self.datetime) } fn tags(&self) -> Vec { Vec::new() @@ -99,7 +99,7 @@ mod test { ] } - fn run_test(test: T) -> () + fn run_test(test: T) where T: FnOnce(tempfile::TempPath), { @@ -108,7 +108,7 @@ mod test { test(tmp_path); } - fn run(test: T) -> () + fn run(test: T) where T: FnOnce(Series), { @@ -280,8 +280,7 @@ mod test { UTC.with_ymd_and_hms(2011, 11, 04, 0, 0, 0) .unwrap() .with_timezone(&FixedOffset::east_opt(0).unwrap()), - ) - .into(), + ), true, ), |l, r| l.timestamp().cmp(&r.timestamp()), diff --git a/fitnesstrax/app/src/app_window.rs b/fitnesstrax/app/src/app_window.rs index 238e2f3..0a127c4 100644 --- a/fitnesstrax/app/src/app_window.rs +++ b/fitnesstrax/app/src/app_window.rs @@ -99,10 +99,6 @@ impl AppWindow { window.set_content(Some(&navigation)); window.present(); - let gesture = gtk::GestureClick::new(); - gesture.connect_released(|_, _, _, _| println!("detected gesture")); - layout.add_controller(gesture); - let s = Self { app: ft_app, layout, diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index a2cfd1e..289f1a5 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -264,7 +264,6 @@ impl DayEdit { } fn add_row(&self, workout: Record) { - println!("adding a row for {:?}", workout); let workout_rows = self.imp().workout_rows.borrow(); #[allow(clippy::single_match)] diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index 59f7347..c1da4fd 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -11,7 +11,8 @@ FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY W 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 . +You should have received a copy of the GNU General Public License along with FitnessTrax. If not, +see . */ mod action_group; diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs index 02bfe27..7b800c1 100644 --- a/fitnesstrax/app/src/components/time_distance.rs +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -25,7 +25,7 @@ use dimensioned::si; use ft_core::{TimeDistance, TimeDistanceActivity}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use std::{cell::RefCell, rc::Rc}; +use std::{rc::Rc, cell::RefCell}; pub fn time_distance_summary( distance: DistanceFormatter, diff --git a/fitnesstrax/app/src/types.rs b/fitnesstrax/app/src/types.rs index a0614c3..8ed75fd 100644 --- a/fitnesstrax/app/src/types.rs +++ b/fitnesstrax/app/src/types.rs @@ -256,14 +256,6 @@ impl From> for DurationFormatter { } } -/* -fn take_digits(s: String) -> String { - s.chars() - .take_while(|t| t.is_ascii_digit()) - .collect::() -} -*/ - #[cfg(test)] mod test { use super::*; diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs index 21688c0..a8f3c08 100644 --- a/fitnesstrax/app/src/view_models/day_detail.rs +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -230,12 +230,10 @@ impl DayDetailViewModel { } pub fn update_record(&self, update: Record) { - println!("updating a record: {:?}", update); let mut records = self.records.write().unwrap(); records .entry(update.id) .and_modify(|record| record.set_value(update.data)); - println!("record updated: {:?}", records.get(&update.id)); } pub fn records(&self) -> Vec> { -- 2.44.1