diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4d9636b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.showUnlinkedFileNotification": false +} \ No newline at end of file diff --git a/emseries/src/series.rs b/emseries/src/series.rs index ee964e7..310b3f7 100644 --- a/emseries/src/series.rs +++ b/emseries/src/series.rs @@ -108,7 +108,7 @@ where Ok(line_) => { match serde_json::from_str::>(line_.as_ref()) .map_err(EmseriesReadError::JSONParseError) - .and_then(|record| Record::try_from(record)) + .and_then(Record::try_from) { Ok(record) => records.insert(record.id.clone(), record.clone()), Err(EmseriesReadError::RecordDeleted(id)) => records.remove(&id), diff --git a/emseries/src/types.rs b/emseries/src/types.rs index 018a99b..3d4eb2e 100644 --- a/emseries/src/types.rs +++ b/emseries/src/types.rs @@ -11,9 +11,6 @@ You should have received a copy of the GNU General Public License along with Lum */ use chrono::{DateTime, FixedOffset, NaiveDate}; -use chrono_tz::UTC; -use serde::de::DeserializeOwned; -use serde::ser::Serialize; use std::{cmp::Ordering, fmt, io, str}; use thiserror::Error; use uuid::Uuid; @@ -93,33 +90,9 @@ impl str::FromStr for Timestamp { } } -/* -impl PartialEq for Timestamp { - fn eq(&self, other: &Timestamp) -> bool { - match (self, other) { - (Timestamp::DateTime(dt1), Timestamp::DateTime(dt2)) => { - dt1.with_timezone(&UTC) == dt2.with_timezone(&UTC) - } - // It's not clear to me what would make sense when I'm comparing a date and a - // timestamp. I'm going with a naive date comparison on the idea that what I'm wanting - // here is human scale, again. - (Timestamp::DateTime(dt1), Timestamp::Date(dt2)) => dt1.date_naive() == *dt2, - (Timestamp::Date(dt1), Timestamp::DateTime(dt2)) => *dt1 == dt2.date_naive(), - (Timestamp::Date(dt1), Timestamp::Date(dt2)) => *dt1 == *dt2, - } - } -} -*/ - impl PartialOrd for Timestamp { fn partial_cmp(&self, other: &Timestamp) -> Option { - // Some(self.cmp(other)) - match (self, other) { - (Timestamp::DateTime(dt1), Timestamp::DateTime(dt2)) => dt1.partial_cmp(dt2), - (Timestamp::DateTime(dt1), Timestamp::Date(dt2)) => dt1.date_naive().partial_cmp(dt2), - (Timestamp::Date(dt1), Timestamp::DateTime(dt2)) => dt1.partial_cmp(&dt2.date_naive()), - (Timestamp::Date(dt1), Timestamp::Date(dt2)) => dt1.partial_cmp(dt2), - } + Some(self.cmp(other)) } } diff --git a/fitnesstrax/app/src/app.rs b/fitnesstrax/app/src/app.rs index 94a6e00..8dfb882 100644 --- a/fitnesstrax/app/src/app.rs +++ b/fitnesstrax/app/src/app.rs @@ -14,7 +14,6 @@ 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::DayInterval; use chrono::NaiveDate; use emseries::{time_range, Record, RecordId, Series, Timestamp}; use ft_core::TraxRecord; @@ -52,12 +51,10 @@ impl App { .unwrap(), ); - let s = Self { + Self { runtime, database: Arc::new(RwLock::new(database)), - }; - - s + } } pub async fn records( @@ -76,7 +73,7 @@ impl App { Timestamp::Date(end), true, )) - .map(|record| record.clone()) + .cloned() .collect::>>(); Ok(records) } else { diff --git a/fitnesstrax/app/src/app_window.rs b/fitnesstrax/app/src/app_window.rs index 5fc3213..f5f2d46 100644 --- a/fitnesstrax/app/src/app_window.rs +++ b/fitnesstrax/app/src/app_window.rs @@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License along with Fit use crate::{ app::App, + view_models::DayDetailViewModel, views::{DayDetailView, HistoricalView, PlaceholderView, View, WelcomeView}, }; use adw::prelude::*; @@ -80,7 +81,7 @@ impl AppWindow { .orientation(gtk::Orientation::Vertical) .build(); - let initial_view = View::Placeholder(PlaceholderView::new().upcast()); + let initial_view = View::Placeholder(PlaceholderView::default().upcast()); layout.append(&initial_view.widget()); @@ -114,9 +115,10 @@ impl AppWindow { s.navigation.connect_popped({ let s = s.clone(); - move |_, _| match *s.current_view.borrow() { - View::Historical(_) => s.load_records(), - _ => {} + move |_, _| { + if let View::Historical(_) = *s.current_view.borrow() { + s.load_records(); + } } }); @@ -137,7 +139,12 @@ impl AppWindow { Rc::new(move |date, records| { let layout = gtk::Box::new(gtk::Orientation::Vertical, 0); layout.append(&adw::HeaderBar::new()); - layout.append(&DayDetailView::new(date, records, s.app.clone())); + // layout.append(&DayDetailView::new(date, records, s.app.clone())); + layout.append(&DayDetailView::new(DayDetailViewModel::new( + date, + records, + s.app.clone(), + ))); let page = &adw::NavigationPage::builder() .title(date.format("%Y-%m-%d").to_string()) .child(&layout) @@ -185,22 +192,4 @@ impl AppWindow { } }); } - - fn on_put_record(&self, record: TraxRecord) { - glib::spawn_future_local({ - let s = self.clone(); - async move { - s.app.put_record(record).await; - } - }); - } - - fn on_update_record(&self, record: Record) { - glib::spawn_future_local({ - let s = self.clone(); - async move { - s.app.update_record(record).await; - } - }); - } } diff --git a/fitnesstrax/app/src/components/action_group.rs b/fitnesstrax/app/src/components/action_group.rs index 4197edd..914adb6 100644 --- a/fitnesstrax/app/src/components/action_group.rs +++ b/fitnesstrax/app/src/components/action_group.rs @@ -14,6 +14,8 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ +//! ActionGroup and related structures + use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 1652d6c..631600e 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -16,11 +16,14 @@ 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, TimeDistanceView, Weight}; +use crate::{ + components::{ActionGroup, Weight}, + view_models::DayDetailViewModel, +}; use emseries::Record; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use std::{cell::RefCell, rc::Rc}; +use std::cell::RefCell; use super::weight::WeightEdit; @@ -57,8 +60,8 @@ glib::wrapper! { pub struct DaySummary(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; } -impl DaySummary { - pub fn new() -> Self { +impl Default for DaySummary { + fn default() -> Self { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); s.set_css_classes(&["day-summary"]); @@ -67,6 +70,12 @@ impl DaySummary { s } +} + +impl DaySummary { + pub fn new() -> Self { + Self::default() + } pub fn set_data(&self, date: chrono::NaiveDate, records: Vec>) { self.imp() @@ -80,11 +89,11 @@ impl DaySummary { if let Some(Record { data: ft_core::TraxRecord::Weight(weight_record), .. - }) = records.iter().filter(|f| f.data.is_weight()).next() + }) = records.iter().find(|f| f.data.is_weight()) { let label = gtk::Label::builder() .halign(gtk::Align::Start) - .label(&format!("{}", weight_record.weight)) + .label(weight_record.weight.to_string()) .css_classes(["day-summary__weight"]) .build(); self.append(&label); @@ -102,27 +111,14 @@ impl DaySummary { } } -pub struct DayDetailPrivate { - date: gtk::Label, - weight: RefCell>, -} +#[derive(Default)] +pub struct DayDetailPrivate {} #[glib::object_subclass] impl ObjectSubclass for DayDetailPrivate { const NAME: &'static str = "DayDetail"; type Type = DayDetail; type ParentType = gtk::Box; - - fn new() -> Self { - let date = gtk::Label::builder() - .css_classes(["daysummary-date"]) - .halign(gtk::Align::Start) - .build(); - Self { - date, - weight: RefCell::new(None), - } - } } impl ObjectImpl for DayDetailPrivate {} @@ -134,11 +130,7 @@ glib::wrapper! { } impl DayDetail { - pub fn new( - date: chrono::NaiveDate, - records: Vec>, - on_edit: OnEdit, - ) -> Self + pub fn new(view_model: DayDetailViewModel, on_edit: OnEdit) -> Self where OnEdit: Fn() + 'static, { @@ -167,6 +159,7 @@ impl DayDetail { s.add_controller(click_controller); */ + /* let weight_record = records.iter().find_map(|record| match record { Record { id, @@ -174,13 +167,12 @@ impl DayDetail { } => Some((id.clone(), record.clone())), _ => None, }); + */ - let weight_view = match weight_record { - Some((id, data)) => Weight::new(Some(data.clone())), - None => Weight::new(None), - }; + let weight_view = Weight::new(view_model.weight()); s.append(&weight_view.widget()); + /* records.into_iter().for_each(|record| { let record_view = match record { Record { @@ -224,21 +216,20 @@ impl DayDetail { s.append(&record_view); } }); + */ s } } pub struct DayEditPrivate { - date: gtk::Label, - weight: Rc, + on_finished: RefCell>, } impl Default for DayEditPrivate { fn default() -> Self { Self { - date: gtk::Label::new(None), - weight: Rc::new(WeightEdit::new(None)), + on_finished: RefCell::new(Box::new(|| {})), } } } @@ -259,75 +250,51 @@ glib::wrapper! { } impl DayEdit { - pub fn new( - date: chrono::NaiveDate, - records: Vec>, - on_put_record: PutRecordFn, - on_update_record: UpdateRecordFn, - on_cancel: CancelFn, - ) -> Self + pub fn new(view_model: DayDetailViewModel, on_finished: OnFinished) -> Self where - PutRecordFn: Fn(ft_core::TraxRecord) + 'static, - UpdateRecordFn: Fn(Record) + 'static, - CancelFn: Fn() + 'static, + OnFinished: Fn() + 'static, { 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.append( &ActionGroup::builder() .primary_action("Save", { let s = s.clone(); - let records = records.clone(); + let view_model = view_model.clone(); move || { - let weight_record = records.iter().find_map(|record| match record { - Record { - id, - data: ft_core::TraxRecord::Weight(w), - } => Some((id, w)), - _ => None, - }); - - let weight = s.imp().weight.value(); - - if let Some(weight) = weight { - match weight_record { - Some((id, _)) => on_update_record(Record { - id: id.clone(), - data: ft_core::TraxRecord::Weight(ft_core::Weight { - date, - weight, - }), - }), - None => { - on_put_record(ft_core::TraxRecord::Weight(ft_core::Weight { - date, - weight, - })) - } - } - }; + view_model.save(); + s.finish(); + } + }) + .secondary_action("Cancel", { + let s = s.clone(); + let view_model = view_model.clone(); + move || { + view_model.revert(); + s.finish(); } }) - .secondary_action("Cancel", on_cancel) .build(), ); - let weight_record = records.iter().find_map(|record| match record { - Record { - id, - data: ft_core::TraxRecord::Weight(record), - } => Some((id.clone(), record.clone())), - _ => None, - }); - - match weight_record { - Some((_id, data)) => s.imp().weight.set_value(Some(data.weight)), - None => s.imp().weight.set_value(None), - }; - s.append(&s.imp().weight.widget()); + s.append( + &WeightEdit::new(view_model.weight(), { + let view_model = view_model.clone(); + move |w| { + view_model.set_weight(w); + } + }) + .widget(), + ); s } + + fn finish(&self) { + (self.imp().on_finished.borrow())() + } } diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index 75ecdd2..78e9b9b 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -20,9 +20,6 @@ pub use action_group::ActionGroup; mod day; pub use day::{DayDetail, DayEdit, DaySummary}; -mod edit_view; -pub use edit_view::EditView; - mod singleton; pub use singleton::{Singleton, SingletonImpl}; diff --git a/fitnesstrax/app/src/components/text_entry.rs b/fitnesstrax/app/src/components/text_entry.rs index 996f1ed..12c0a6c 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -17,14 +17,19 @@ You should have received a copy of the GNU General Public License along with Fit use gtk::prelude::*; use std::{cell::RefCell, rc::Rc}; +#[derive(Clone, Debug)] pub struct ParseError; +type Renderer = dyn Fn(&T) -> String; +type Parser = dyn Fn(&str) -> Result; + #[derive(Clone)] pub struct TextEntry { value: Rc>>, widget: gtk::Entry, - renderer: Rc String>>, - parser: Rc Result>>, + #[allow(unused)] + renderer: Rc>, + parser: Rc>, } impl std::fmt::Debug for TextEntry { @@ -45,16 +50,15 @@ impl TextEntry { V: Fn(&str) -> Result + 'static, { let widget = gtk::Entry::builder().placeholder_text(placeholder).build(); - match value { - Some(ref v) => widget.set_text(&renderer(&v)), - None => {} + if let Some(ref v) = value { + widget.set_text(&renderer(v)) } let s = Self { value: Rc::new(RefCell::new(value)), widget, - renderer: Rc::new(Box::new(renderer)), - parser: Rc::new(Box::new(parser)), + renderer: Rc::new(renderer), + parser: Rc::new(parser), }; s.widget.buffer().connect_text_notify({ @@ -83,19 +87,20 @@ impl TextEntry { } } + #[allow(unused)] pub fn value(&self) -> Option { let v = self.value.borrow().clone(); self.value.borrow().clone() } pub fn set_value(&self, value: Option) { - match value { - Some(ref v) => self.widget.set_text(&(self.renderer)(&v)), - None => {} + if let Some(ref v) = value { + self.widget.set_text(&(self.renderer)(v)) } *self.value.borrow_mut() = value; } + #[allow(unused)] pub fn grab_focus(&self) { self.widget.grab_focus(); } diff --git a/fitnesstrax/app/src/components/time_distance.rs b/fitnesstrax/app/src/components/time_distance.rs index 8a20c42..ac1ad9c 100644 --- a/fitnesstrax/app/src/components/time_distance.rs +++ b/fitnesstrax/app/src/components/time_distance.rs @@ -24,6 +24,7 @@ use std::cell::RefCell; #[derive(Default)] pub struct TimeDistanceViewPrivate { + #[allow(unused)] record: RefCell>, } @@ -53,7 +54,7 @@ impl TimeDistanceView { first_row.append( >k::Label::builder() .halign(gtk::Align::Start) - .label(&record.datetime.format("%H:%M").to_string()) + .label(record.datetime.format("%H:%M").to_string()) .build(), ); @@ -96,7 +97,7 @@ impl TimeDistanceView { .label( record .comments - .map(|comments| format!("{}", comments)) + .map(|comments| comments.to_string()) .unwrap_or("".to_owned()), ) .build(), diff --git a/fitnesstrax/app/src/components/weight.rs b/fitnesstrax/app/src/components/weight.rs index 552b107..220b51e 100644 --- a/fitnesstrax/app/src/components/weight.rs +++ b/fitnesstrax/app/src/components/weight.rs @@ -14,12 +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::{EditView, ParseError, Singleton, TextEntry}; -use chrono::{Local, NaiveDate}; +use crate::components::{ParseError, TextEntry}; use dimensioned::si; -use glib::{object::ObjectRef, Object}; -use gtk::{prelude::*, subclass::prelude::*}; -use std::{borrow::Borrow, cell::RefCell}; +use gtk::prelude::*; #[derive(Default)] pub struct WeightViewPrivate {} @@ -29,14 +26,14 @@ pub struct Weight { } impl Weight { - pub fn new(weight: Option) -> Self { + pub fn new(weight: Option>) -> Self { let label = gtk::Label::builder() .css_classes(["card", "weight-view"]) .can_focus(true) .build(); match weight { - Some(w) => label.set_text(&format!("{:?}", w.weight)), + Some(w) => label.set_text(&format!("{:?}", w)), None => label.set_text("No weight recorded"), } @@ -54,17 +51,30 @@ pub struct WeightEdit { } impl WeightEdit { - pub fn new(weight: Option) -> Self { + pub fn new(weight: Option>, on_update: OnUpdate) -> Self + where + OnUpdate: Fn(si::Kilogram) + 'static, + { Self { entry: TextEntry::new( "0 kg", - weight.map(|w| w.weight), + weight, |val: &si::Kilogram| val.to_string(), - |v: &str| v.parse::().map(|w| w * si::KG).map_err(|_| ParseError), + 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); } diff --git a/fitnesstrax/app/src/main.rs b/fitnesstrax/app/src/main.rs index 0811941..1e86d60 100644 --- a/fitnesstrax/app/src/main.rs +++ b/fitnesstrax/app/src/main.rs @@ -18,12 +18,12 @@ mod app; mod app_window; mod components; mod types; +mod view_models; mod views; use adw::prelude::*; use app_window::AppWindow; use std::{env, path::PathBuf}; -use types::DayInterval; const APP_ID_DEV: &str = "com.luminescent-dreams.fitnesstrax.dev"; const APP_ID_PROD: &str = "com.luminescent-dreams.fitnesstrax"; diff --git a/fitnesstrax/app/src/types.rs b/fitnesstrax/app/src/types.rs index 319e845..799f2d8 100644 --- a/fitnesstrax/app/src/types.rs +++ b/fitnesstrax/app/src/types.rs @@ -21,8 +21,8 @@ impl Default for DayInterval { impl DayInterval { pub fn days(&self) -> impl Iterator { DayIterator { - current: self.start.clone(), - end: self.end.clone(), + current: self.start, + end: self.end, } } } @@ -37,7 +37,7 @@ impl Iterator for DayIterator { fn next(&mut self) -> Option { if self.current <= self.end { - let val = self.current.clone(); + let val = self.current; self.current += Duration::days(1); Some(val) } else { diff --git a/fitnesstrax/app/src/view_models/day_detail.rs b/fitnesstrax/app/src/view_models/day_detail.rs new file mode 100644 index 0000000..f823d6f --- /dev/null +++ b/fitnesstrax/app/src/view_models/day_detail.rs @@ -0,0 +1,217 @@ +/* +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::app::App; +use dimensioned::si; +use emseries::{Record, RecordId, Recordable}; +use ft_core::TraxRecord; +use std::{ + collections::HashMap, + ops::Deref, + sync::{Arc, RwLock}, +}; + +#[derive(Clone, Debug)] +enum RecordState { + Original(Record), + New(T), + Updated(Record), + #[allow(unused)] + Deleted(Record), +} + +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 with_value(self, value: T) -> RecordState { + match self { + RecordState::Original(r) => RecordState::Updated(Record { data: value, ..r }), + RecordState::New(_) => RecordState::New(value), + RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..r }), + RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..r }), + } + } + + #[allow(unused)] + fn with_delete(self) -> Option> { + match self { + RecordState::Original(r) => Some(RecordState::Deleted(r)), + RecordState::New(r) => None, + RecordState::Updated(r) => Some(RecordState::Deleted(r)), + RecordState::Deleted(r) => Some(RecordState::Deleted(r)), + } + } +} + +impl Deref for RecordState { + type Target = T; + fn deref(&self) -> &Self::Target { + match self { + RecordState::Original(ref r) => &r.data, + RecordState::New(ref r) => r, + RecordState::Updated(ref r) => &r.data, + RecordState::Deleted(ref r) => &r.data, + } + } +} + +#[derive(Default)] +struct DayDetailViewModelInner {} + +#[derive(Clone, Default)] +pub struct DayDetailViewModel { + app: Option, + pub date: chrono::NaiveDate, + weight: Arc>>>, + steps: Arc>>>, + records: Arc>>>, +} + +impl DayDetailViewModel { + pub fn new(date: chrono::NaiveDate, records: Vec>, app: App) -> Self { + 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 { + app: Some(app), + date, + weight: Arc::new(RwLock::new( + 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 })), + )), + steps: Arc::new(RwLock::new( + 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 })), + )), + + records: Arc::new(RwLock::new( + records + .into_iter() + .map(|r| (r.id.clone(), RecordState::Original(r))) + .collect::>>(), + )), + } + } + + pub fn weight(&self) -> Option> { + (*self.weight.read().unwrap()).as_ref().map(|w| w.weight) + } + + pub fn set_weight(&self, new_weight: si::Kilogram) { + let mut record = self.weight.write().unwrap(); + let new_record = match *record { + Some(ref rstate) => rstate.clone().with_value(ft_core::Weight { + date: self.date, + weight: new_weight, + }), + None => RecordState::New(ft_core::Weight { + date: self.date, + weight: new_weight, + }), + }; + *record = Some(new_record); + } + + pub fn steps(&self) -> Option { + (*self.steps.read().unwrap()).as_ref().map(|w| w.count) + } + + pub fn set_steps(&self, new_count: u32) { + let mut record = self.steps.write().unwrap(); + let new_record = match *record { + Some(ref rstate) => rstate.clone().with_value(ft_core::Steps { + date: self.date, + count: new_count, + }), + None => RecordState::New(ft_core::Steps { + date: self.date, + count: new_count, + }), + }; + *record = Some(new_record); + } + + pub fn save(&self) { + glib::spawn_future({ + let s = self.clone(); + async move { + if let Some(app) = s.app { + let weight_record = s.weight.read().unwrap().clone(); + match weight_record { + Some(RecordState::New(weight)) => { + let _ = app.put_record(TraxRecord::Weight(weight)).await; + } + Some(RecordState::Original(_)) => {} + Some(RecordState::Updated(weight)) => { + let _ = app + .update_record(Record { + id: weight.id, + data: TraxRecord::Weight(weight.data), + }) + .await; + } + Some(RecordState::Deleted(_)) => {} + None => {} + } + + let records = s + .records + .write() + .unwrap() + .drain() + .map(|(_, record)| record) + .collect::>>(); + + for record in records { + match record { + RecordState::New(data) => { + let _ = app.put_record(data).await; + } + RecordState::Original(_) => {} + RecordState::Updated(r) => { + let _ = app.update_record(r.clone()).await; + } + RecordState::Deleted(_) => unimplemented!(), + } + } + } + } + }); + } + + pub fn revert(&self) { + unimplemented!(); + } +} diff --git a/fitnesstrax/app/src/components/edit_view.rs b/fitnesstrax/app/src/view_models/mod.rs similarity index 79% rename from fitnesstrax/app/src/components/edit_view.rs rename to fitnesstrax/app/src/view_models/mod.rs index f6f96a9..4e41990 100644 --- a/fitnesstrax/app/src/components/edit_view.rs +++ b/fitnesstrax/app/src/view_models/mod.rs @@ -1,5 +1,5 @@ /* -Copyright 2023, Savanni D'Gerinel +Copyright 2024, Savanni D'Gerinel This file is part of FitnessTrax. @@ -14,9 +14,5 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ -#[derive(Clone)] -pub enum EditView { - Unconfigured, - View(View), - Edit(Edit), -} +mod day_detail; +pub use day_detail::DayDetailViewModel; diff --git a/fitnesstrax/app/src/views/day_detail_view.rs b/fitnesstrax/app/src/views/day_detail_view.rs index c73d1f0..ae213fe 100644 --- a/fitnesstrax/app/src/views/day_detail_view.rs +++ b/fitnesstrax/app/src/views/day_detail_view.rs @@ -15,21 +15,17 @@ You should have received a copy of the GNU General Public License along with Fit */ use crate::{ - app::App, components::{DayDetail, DayEdit, Singleton, SingletonImpl}, + view_models::DayDetailViewModel, }; -use emseries::Record; -use ft_core::TraxRecord; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::cell::RefCell; #[derive(Default)] pub struct DayDetailViewPrivate { - app: RefCell>, container: Singleton, - date: RefCell, - records: RefCell>>, + view_model: RefCell, } #[glib::object_subclass] @@ -49,116 +45,32 @@ glib::wrapper! { } impl DayDetailView { - pub fn new(date: chrono::NaiveDate, records: Vec>, app: App) -> Self { + pub fn new(view_model: DayDetailViewModel) -> Self { let s: Self = Object::builder().build(); - - *s.imp().date.borrow_mut() = date; - *s.imp().records.borrow_mut() = records; - *s.imp().app.borrow_mut() = Some(app); + *s.imp().view_model.borrow_mut() = view_model; s.append(&s.imp().container); - /* - s.imp() - .container - .swap(&DayDetail::new(date, records.clone(), { - let s = s.clone(); - let records = records.clone(); - move || { - s.imp().container.swap(&DayEdit::new( - date, - records, - s.on_put_record(), - // s.on_update_record(), - |_| {}, - )) - } - })); - */ - s.view(); s } fn view(&self) { - self.imp().container.swap(&DayDetail::new( - self.imp().date.borrow().clone(), - self.imp().records.borrow().clone(), - { + self.imp() + .container + .swap(&DayDetail::new(self.imp().view_model.borrow().clone(), { let s = self.clone(); move || s.edit() - }, - )); + })); } fn edit(&self) { - self.imp().container.swap(&DayEdit::new( - self.imp().date.borrow().clone(), - self.imp().records.borrow().clone(), - self.on_put_record(), - self.on_update_record(), - { + self.imp() + .container + .swap(&DayEdit::new(self.imp().view_model.borrow().clone(), { let s = self.clone(); move || s.view() - }, - )); - } - - fn on_put_record(&self) -> Box { - let s = self.clone(); - let app = self.imp().app.clone(); - Box::new(move |record| { - let s = s.clone(); - let app = app.clone(); - glib::spawn_future_local({ - async move { - match &*app.borrow() { - Some(app) => { - let id = app - .put_record(record.clone()) - .await - .expect("successful write"); - s.imp() - .records - .borrow_mut() - .push(Record { id, data: record }); - } - None => {} - } - s.view(); - } - }); - }) - } - - fn on_update_record(&self) -> Box)> { - let s = self.clone(); - let app = self.imp().app.clone(); - Box::new(move |updated_record| { - let app = app.clone(); - - let mut records = s.imp().records.borrow_mut(); - let idx = records.iter().position(|r| r.id == updated_record.id); - match idx { - Some(i) => records[i] = updated_record.clone(), - None => records.push(updated_record.clone()), - } - - glib::spawn_future_local({ - let s = s.clone(); - async move { - match &*app.borrow() { - Some(app) => { - let _ = app.update_record(updated_record).await; - } - None => { - println!("no app!"); - } - } - s.view(); - } - }); - }) + })); } } diff --git a/fitnesstrax/app/src/views/historical_view.rs b/fitnesstrax/app/src/views/historical_view.rs index 20e074d..23bccad 100644 --- a/fitnesstrax/app/src/views/historical_view.rs +++ b/fitnesstrax/app/src/views/historical_view.rs @@ -15,7 +15,7 @@ You should have received a copy of the GNU General Public License along with Fit */ use crate::{components::DaySummary, types::DayInterval}; -use chrono::{Duration, Local, NaiveDate}; +use chrono::NaiveDate; use emseries::Record; use ft_core::TraxRecord; use glib::Object; @@ -161,7 +161,7 @@ impl DayRecords { } pub fn date(&self) -> chrono::NaiveDate { - self.imp().date.borrow().clone() + *self.imp().date.borrow() } pub fn records(&self) -> Vec> { @@ -204,11 +204,11 @@ impl GroupedRecords { self } - fn items<'a>(&'a self) -> impl Iterator + 'a { + fn items(&self) -> impl Iterator + '_ { self.interval.days().map(|date| { self.data .get(&date) - .map(|rec| rec.clone()) + .cloned() .unwrap_or(DayRecords::new(date, vec![])) }) } @@ -217,6 +217,7 @@ impl GroupedRecords { #[cfg(test)] mod test { use super::GroupedRecords; + use crate::types::DayInterval; use chrono::{FixedOffset, NaiveDate, TimeZone}; use dimensioned::si::{KG, M, S}; use emseries::{Record, RecordId}; @@ -272,7 +273,12 @@ mod test { }, ]; - let groups = GroupedRecords::from(records).0; + let groups = GroupedRecords::new(DayInterval { + start: NaiveDate::from_ymd_opt(2023, 10, 14).unwrap(), + end: NaiveDate::from_ymd_opt(2023, 10, 14).unwrap(), + }) + .with_data(records) + .data; assert_eq!(groups.len(), 3); } } diff --git a/fitnesstrax/app/src/views/placeholder_view.rs b/fitnesstrax/app/src/views/placeholder_view.rs index c3f68eb..94e8e50 100644 --- a/fitnesstrax/app/src/views/placeholder_view.rs +++ b/fitnesstrax/app/src/views/placeholder_view.rs @@ -38,8 +38,8 @@ glib::wrapper! { pub struct PlaceholderView(ObjectSubclass) @extends gtk::Box, gtk::Widget; } -impl PlaceholderView { - pub fn new() -> Self { +impl Default for PlaceholderView { + fn default() -> Self { let s: Self = Object::builder().build(); s } diff --git a/fitnesstrax/app/src/views/welcome_view.rs b/fitnesstrax/app/src/views/welcome_view.rs index 1f391a3..78a21a0 100644 --- a/fitnesstrax/app/src/views/welcome_view.rs +++ b/fitnesstrax/app/src/views/welcome_view.rs @@ -14,7 +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::{app::App, components::FileChooserRow}; +use crate::components::FileChooserRow; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::path::PathBuf; diff --git a/fitnesstrax/core/src/legacy.rs b/fitnesstrax/core/src/legacy.rs index 0a79713..9ab1ba3 100644 --- a/fitnesstrax/core/src/legacy.rs +++ b/fitnesstrax/core/src/legacy.rs @@ -1,23 +1,25 @@ -use crate::types; - #[cfg(test)] mod test { #[test] + #[ignore] fn read_a_legacy_set_rep_record() { unimplemented!() } #[test] + #[ignore] fn read_a_legacy_steps_record() { unimplemented!() } #[test] + #[ignore] fn read_a_legacy_time_distance_record() { unimplemented!() } #[test] + #[ignore] fn read_a_legacy_weight_record() { unimplemented!() } diff --git a/fitnesstrax/core/src/types.rs b/fitnesstrax/core/src/types.rs index 401f4a8..b0959f7 100644 --- a/fitnesstrax/core/src/types.rs +++ b/fitnesstrax/core/src/types.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; /// SetRep represents workouts like pushups or situps, which involve doing a "set" of a number of /// actions, resting, and then doing another set. +#[allow(dead_code)] pub struct SetRep { /// I assume that a set/rep workout is only done once in a day. date: NaiveDate, @@ -22,6 +23,16 @@ pub struct Steps { pub count: u32, } +impl Recordable for Steps { + fn timestamp(&self) -> Timestamp { + Timestamp::Date(self.date) + } + + fn tags(&self) -> Vec { + vec![] + } +} + /// TimeDistance represents workouts characterized by a duration and a distance travelled. These /// sorts of workouts can occur many times a day, depending on how one records things. I might /// record a single 30-km workout if I go on a long-distanec ride. Or I might record multiple 5km @@ -54,6 +65,16 @@ pub struct Weight { pub weight: si::Kilogram, } +impl Recordable for Weight { + fn timestamp(&self) -> Timestamp { + Timestamp::Date(self.date) + } + + fn tags(&self) -> Vec { + vec![] + } +} + #[derive(Clone, Debug, PartialEq)] pub enum RecordType { BikeRide, @@ -91,23 +112,24 @@ impl TraxRecord { } pub fn is_weight(&self) -> bool { - match self { - TraxRecord::Weight(_) => true, - _ => false, - } + matches!(self, TraxRecord::Weight(_)) + } + + pub fn is_steps(&self) -> bool { + matches!(self, TraxRecord::Steps(_)) } } impl Recordable for TraxRecord { fn timestamp(&self) -> Timestamp { match self { - TraxRecord::BikeRide(rec) => Timestamp::DateTime(rec.datetime.clone()), - TraxRecord::Row(rec) => Timestamp::DateTime(rec.datetime.clone()), - TraxRecord::Run(rec) => Timestamp::DateTime(rec.datetime.clone()), - TraxRecord::Steps(rec) => Timestamp::Date(rec.date), - TraxRecord::Swim(rec) => Timestamp::DateTime(rec.datetime.clone()), - TraxRecord::Walk(rec) => Timestamp::DateTime(rec.datetime.clone()), - TraxRecord::Weight(rec) => Timestamp::Date(rec.date), + TraxRecord::BikeRide(rec) => Timestamp::DateTime(rec.datetime), + TraxRecord::Row(rec) => Timestamp::DateTime(rec.datetime), + TraxRecord::Run(rec) => Timestamp::DateTime(rec.datetime), + TraxRecord::Steps(rec) => rec.timestamp(), + TraxRecord::Swim(rec) => Timestamp::DateTime(rec.datetime), + TraxRecord::Walk(rec) => Timestamp::DateTime(rec.datetime), + TraxRecord::Weight(rec) => rec.timestamp(), } } @@ -135,6 +157,6 @@ mod test { let id = series.put(record.clone()).unwrap(); let record_ = series.get(&id).unwrap(); - assert_eq!(record_, record); + assert_eq!(record_.data, record); } }