Start setting up the day detail view model

I've created the view model and added a getter function for the weight.
I'm passing the view model now to the DayDetailView, DayDetail, and
DayEdit.

I'm starting to set up the Save function for the view model, draining
all of the updated records and saving them.

None of the components yet save any updates to the view model, so
updated_records is always going to be empty until I figure that out.
This commit is contained in:
Savanni D'Gerinel 2024-01-18 09:00:08 -05:00
parent 1c2c4982a1
commit 18e7e4fe2f
5 changed files with 134 additions and 90 deletions

View File

@ -16,7 +16,9 @@ You should have received a copy of the GNU General Public License along with Fit
use crate::{ use crate::{
app::App, app::App,
views::{DayDetailView, HistoricalView, PlaceholderView, View, WelcomeView}, views::{
DayDetailView, DayDetailViewModel, HistoricalView, PlaceholderView, View, WelcomeView,
},
}; };
use adw::prelude::*; use adw::prelude::*;
use chrono::{Duration, Local}; use chrono::{Duration, Local};
@ -137,7 +139,12 @@ impl AppWindow {
Rc::new(move |date, records| { Rc::new(move |date, records| {
let layout = gtk::Box::new(gtk::Orientation::Vertical, 0); let layout = gtk::Box::new(gtk::Orientation::Vertical, 0);
layout.append(&adw::HeaderBar::new()); 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() let page = &adw::NavigationPage::builder()
.title(date.format("%Y-%m-%d").to_string()) .title(date.format("%Y-%m-%d").to_string())
.child(&layout) .child(&layout)

View File

@ -16,7 +16,10 @@ You should have received a copy of the GNU General Public License along with Fit
// use chrono::NaiveDate; // use chrono::NaiveDate;
// use ft_core::TraxRecord; // use ft_core::TraxRecord;
use crate::components::{ActionGroup, TimeDistanceView, Weight}; use crate::{
components::{ActionGroup, TimeDistanceView, Weight},
views::DayDetailViewModel,
};
use emseries::Record; use emseries::Record;
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
@ -102,27 +105,14 @@ impl DaySummary {
} }
} }
pub struct DayDetailPrivate { #[derive(Default)]
date: gtk::Label, pub struct DayDetailPrivate {}
weight: RefCell<Option<gtk::Label>>,
}
#[glib::object_subclass] #[glib::object_subclass]
impl ObjectSubclass for DayDetailPrivate { impl ObjectSubclass for DayDetailPrivate {
const NAME: &'static str = "DayDetail"; const NAME: &'static str = "DayDetail";
type Type = DayDetail; type Type = DayDetail;
type ParentType = gtk::Box; 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 {} impl ObjectImpl for DayDetailPrivate {}
@ -134,11 +124,7 @@ glib::wrapper! {
} }
impl DayDetail { impl DayDetail {
pub fn new<OnEdit>( pub fn new<OnEdit>(view_model: DayDetailViewModel, on_edit: OnEdit) -> Self
date: chrono::NaiveDate,
records: Vec<Record<ft_core::TraxRecord>>,
on_edit: OnEdit,
) -> Self
where where
OnEdit: Fn() + 'static, OnEdit: Fn() + 'static,
{ {
@ -148,7 +134,7 @@ impl DayDetail {
s.append( s.append(
&ActionGroup::builder() &ActionGroup::builder()
.primary_action("Edit", Box::new(on_edit)) .primary_action("Edit", Box::new(move || on_edit()))
.build(), .build(),
); );
@ -167,6 +153,7 @@ impl DayDetail {
s.add_controller(click_controller); s.add_controller(click_controller);
*/ */
/*
let weight_record = records.iter().find_map(|record| match record { let weight_record = records.iter().find_map(|record| match record {
Record { Record {
id, id,
@ -174,13 +161,12 @@ impl DayDetail {
} => Some((id.clone(), record.clone())), } => Some((id.clone(), record.clone())),
_ => None, _ => None,
}); });
*/
let weight_view = match weight_record { let weight_view = Weight::new(view_model.weight());
Some((id, data)) => Weight::new(Some(data.clone())),
None => Weight::new(None),
};
s.append(&weight_view.widget()); s.append(&weight_view.widget());
/*
records.into_iter().for_each(|record| { records.into_iter().for_each(|record| {
let record_view = match record { let record_view = match record {
Record { Record {
@ -224,21 +210,20 @@ impl DayDetail {
s.append(&record_view); s.append(&record_view);
} }
}); });
*/
s s
} }
} }
pub struct DayEditPrivate { pub struct DayEditPrivate {
date: gtk::Label, on_finished: RefCell<Box<dyn Fn()>>,
weight: Rc<WeightEdit>,
} }
impl Default for DayEditPrivate { impl Default for DayEditPrivate {
fn default() -> Self { fn default() -> Self {
Self { Self {
date: gtk::Label::new(None), on_finished: RefCell::new(Box::new(|| {})),
weight: Rc::new(WeightEdit::new(None)),
} }
} }
} }
@ -259,25 +244,20 @@ glib::wrapper! {
} }
impl DayEdit { impl DayEdit {
pub fn new<PutRecordFn, UpdateRecordFn, CancelFn>( pub fn new<OnFinished>(view_model: DayDetailViewModel, on_finished: OnFinished) -> Self
date: chrono::NaiveDate,
records: Vec<Record<ft_core::TraxRecord>>,
on_put_record: PutRecordFn,
on_update_record: UpdateRecordFn,
on_cancel: CancelFn,
) -> Self
where where
PutRecordFn: Fn(ft_core::TraxRecord) + 'static, OnFinished: Fn() + 'static,
UpdateRecordFn: Fn(Record<ft_core::TraxRecord>) + 'static,
CancelFn: Fn() + 'static,
{ {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
s.set_orientation(gtk::Orientation::Vertical); s.set_orientation(gtk::Orientation::Vertical);
s.set_hexpand(true); s.set_hexpand(true);
*s.imp().on_finished.borrow_mut() = Box::new(on_finished);
s.append( s.append(
&ActionGroup::builder() &ActionGroup::builder()
.primary_action("Save", { .primary_action("Save", {
/*
let s = s.clone(); let s = s.clone();
let records = records.clone(); let records = records.clone();
move || { move || {
@ -309,11 +289,20 @@ impl DayEdit {
} }
}; };
} }
*/
let s = s.clone();
move || {
s.finish();
}
})
.secondary_action("Cancel", {
let s = s.clone();
move || s.finish()
}) })
.secondary_action("Cancel", on_cancel)
.build(), .build(),
); );
/*
let weight_record = records.iter().find_map(|record| match record { let weight_record = records.iter().find_map(|record| match record {
Record { Record {
id, id,
@ -327,7 +316,13 @@ impl DayEdit {
None => s.imp().weight.set_value(None), None => s.imp().weight.set_value(None),
}; };
s.append(&s.imp().weight.widget()); s.append(&s.imp().weight.widget());
*/
s.append(&WeightEdit::new(view_model.weight()).widget());
s s
} }
fn finish(&self) {
(self.imp().on_finished.borrow())()
}
} }

View File

@ -29,14 +29,14 @@ pub struct Weight {
} }
impl Weight { impl Weight {
pub fn new(weight: Option<ft_core::Weight>) -> Self { pub fn new(weight: Option<si::Kilogram<f64>>) -> Self {
let label = gtk::Label::builder() let label = gtk::Label::builder()
.css_classes(["card", "weight-view"]) .css_classes(["card", "weight-view"])
.can_focus(true) .can_focus(true)
.build(); .build();
match weight { match weight {
Some(w) => label.set_text(&format!("{:?}", w.weight)), Some(w) => label.set_text(&format!("{:?}", w)),
None => label.set_text("No weight recorded"), None => label.set_text("No weight recorded"),
} }
@ -54,11 +54,11 @@ pub struct WeightEdit {
} }
impl WeightEdit { impl WeightEdit {
pub fn new(weight: Option<ft_core::Weight>) -> Self { pub fn new(weight: Option<si::Kilogram<f64>>) -> Self {
Self { Self {
entry: TextEntry::new( entry: TextEntry::new(
"0 kg", "0 kg",
weight.map(|w| w.weight), weight,
|val: &si::Kilogram<f64>| val.to_string(), |val: &si::Kilogram<f64>| val.to_string(),
|v: &str| v.parse::<f64>().map(|w| w * si::KG).map_err(|_| ParseError), |v: &str| v.parse::<f64>().map(|w| w * si::KG).map_err(|_| ParseError),
), ),

View File

@ -18,18 +18,85 @@ use crate::{
app::App, app::App,
components::{DayDetail, DayEdit, Singleton, SingletonImpl}, components::{DayDetail, DayEdit, Singleton, SingletonImpl},
}; };
use emseries::Record; use dimensioned::si;
use emseries::{Record, RecordId};
use ft_core::TraxRecord; use ft_core::TraxRecord;
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use std::cell::RefCell; use std::{
cell::RefCell,
rc::Rc,
sync::{Arc, RwLock},
};
#[derive(Default)]
struct DayDetailViewModelInner {
records: Vec<Record<TraxRecord>>,
updated_records: Vec<Record<TraxRecord>>,
new_records: Vec<TraxRecord>,
deleted_records: Vec<RecordId>,
}
#[derive(Clone, Default)]
pub struct DayDetailViewModel {
app: Option<App>,
pub date: chrono::NaiveDate,
inner: Arc<RwLock<DayDetailViewModelInner>>,
}
impl DayDetailViewModel {
pub fn new(date: chrono::NaiveDate, records: Vec<Record<TraxRecord>>, app: App) -> Self {
Self {
app: Some(app),
date,
inner: Arc::new(RwLock::new(DayDetailViewModelInner {
records,
updated_records: vec![],
new_records: vec![],
deleted_records: vec![],
})),
}
}
pub fn weight(&self) -> Option<si::Kilogram<f64>> {
self.inner
.read()
.unwrap()
.records
.iter()
.find_map(|record| match record {
Record {
data: ft_core::TraxRecord::Weight(record),
..
} => Some(record.weight.clone()),
_ => None,
})
}
pub fn save(&self) {
glib::spawn_future({
let s = self.clone();
async move {
if let Some(app) = s.app {
let updated_records = {
let mut data = s.inner.write().unwrap();
data.updated_records
.drain(..)
.collect::<Vec<Record<TraxRecord>>>()
};
for record in updated_records.into_iter() {
let _ = app.update_record(record.clone()).await;
}
}
}
});
}
}
#[derive(Default)] #[derive(Default)]
pub struct DayDetailViewPrivate { pub struct DayDetailViewPrivate {
app: RefCell<Option<App>>,
container: Singleton, container: Singleton,
date: RefCell<chrono::NaiveDate>, view_model: RefCell<DayDetailViewModel>,
records: RefCell<Vec<Record<TraxRecord>>>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -49,62 +116,36 @@ glib::wrapper! {
} }
impl DayDetailView { impl DayDetailView {
pub fn new(date: chrono::NaiveDate, records: Vec<Record<TraxRecord>>, app: App) -> Self { pub fn new(view_model: DayDetailViewModel) -> Self {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
*s.imp().view_model.borrow_mut() = view_model;
*s.imp().date.borrow_mut() = date;
*s.imp().records.borrow_mut() = records;
*s.imp().app.borrow_mut() = Some(app);
s.append(&s.imp().container); 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.view();
s s
} }
fn view(&self) { fn view(&self) {
self.imp().container.swap(&DayDetail::new( self.imp()
self.imp().date.borrow().clone(), .container
self.imp().records.borrow().clone(), .swap(&DayDetail::new(self.imp().view_model.borrow().clone(), {
{
let s = self.clone(); let s = self.clone();
move || s.edit() move || s.edit()
}, }));
));
} }
fn edit(&self) { fn edit(&self) {
self.imp().container.swap(&DayEdit::new( self.imp()
self.imp().date.borrow().clone(), .container
self.imp().records.borrow().clone(), .swap(&DayEdit::new(self.imp().view_model.borrow().clone(), {
self.on_put_record(),
self.on_update_record(),
{
let s = self.clone(); let s = self.clone();
move || s.view() move || s.view()
}, }));
));
} }
/*
fn on_put_record(&self) -> Box<dyn Fn(ft_core::TraxRecord)> { fn on_put_record(&self) -> Box<dyn Fn(ft_core::TraxRecord)> {
let s = self.clone(); let s = self.clone();
let app = self.imp().app.clone(); let app = self.imp().app.clone();
@ -161,4 +202,5 @@ impl DayDetailView {
}); });
}) })
} }
*/
} }

View File

@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with Fit
use gtk::prelude::*; use gtk::prelude::*;
mod day_detail_view; mod day_detail_view;
pub use day_detail_view::DayDetailView; pub use day_detail_view::{DayDetailView, DayDetailViewModel};
mod historical_view; mod historical_view;
pub use historical_view::HistoricalView; pub use historical_view::HistoricalView;