Compare commits

...

3 Commits

Author SHA1 Message Date
Savanni D'Gerinel 33c85ee7b3 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.
2024-01-29 10:22:21 -05:00
Savanni D'Gerinel 9874b6081b The view model can no longer be initialized without an app 2024-01-29 09:18:36 -05:00
Savanni D'Gerinel 7d5d639ed9 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.
2024-01-29 08:26:41 -05:00
11 changed files with 341 additions and 296 deletions

View File

@ -57,6 +57,20 @@ impl App {
} }
} }
pub async fn get_record(&self, id: RecordId) -> Result<Option<Record<TraxRecord>>, 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()
}
pub async fn records( pub async fn records(
&self, &self,
start: NaiveDate, start: NaiveDate,

View File

@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License along with Fit
use crate::{ use crate::{
app::App, app::App,
types::DayInterval,
view_models::DayDetailViewModel, view_models::DayDetailViewModel,
views::{DayDetailView, HistoricalView, PlaceholderView, View, WelcomeView}, views::{DayDetailView, HistoricalView, PlaceholderView, View, WelcomeView},
}; };
@ -133,25 +134,34 @@ impl AppWindow {
self.swap_main(view); self.swap_main(view);
} }
fn show_historical_view(&self, records: Vec<Record<TraxRecord>>) { fn show_historical_view(&self, start_date: chrono::NaiveDate, end_date: chrono::NaiveDate) {
let view = View::Historical(HistoricalView::new(self.app.clone(), records, { let on_select_day = {
let s = self.clone(); let s = self.clone();
Rc::new(move |date, records| { move |date, records| {
let s = s.clone();
glib::spawn_future_local(async move {
let view_model = DayDetailViewModel::new(date, s.app.clone()).await;
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( layout.append(&DayDetailView::new(view_model));
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)
.build(); .build();
s.navigation.push(page); s.navigation.push(page);
}) });
})); }
};
let view = View::Historical(HistoricalView::new(
self.app.clone(),
DayInterval {
start: start_date,
end: end_date,
},
Rc::new(on_select_day),
));
self.swap_main(view); self.swap_main(view);
} }
@ -162,7 +172,7 @@ impl AppWindow {
let end = Local::now().date_naive(); let end = Local::now().date_naive();
let start = end - Duration::days(7); let start = end - Duration::days(7);
match s.app.records(start, end).await { match s.app.records(start, end).await {
Ok(records) => s.show_historical_view(records), Ok(records) => s.show_historical_view(start, end),
Err(_) => s.show_welcome_view(), Err(_) => s.show_welcome_view(),
} }
} }

View File

@ -17,10 +17,7 @@ You should have received a copy of the GNU General Public License along with Fit
// use chrono::NaiveDate; // use chrono::NaiveDate;
// use ft_core::TraxRecord; // use ft_core::TraxRecord;
use crate::{ use crate::{
components::{ components::{steps_editor, time_distance_summary, weight_field, ActionGroup, Steps, Weight},
steps_editor, text_entry::distance_field, time_distance_summary, weight_field, ActionGroup,
Steps, Weight,
},
view_models::DayDetailViewModel, view_models::DayDetailViewModel,
}; };
use emseries::{Record, RecordId}; use emseries::{Record, RecordId};
@ -143,31 +140,6 @@ impl DayDetail {
.build(), .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::<WeightView>() {
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() let top_row = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal) .orientation(gtk::Orientation::Horizontal)
.build(); .build();
@ -200,7 +172,7 @@ impl DayDetail {
pub struct DayEditPrivate { pub struct DayEditPrivate {
on_finished: RefCell<Box<dyn Fn()>>, on_finished: RefCell<Box<dyn Fn()>>,
workout_rows: RefCell<gtk::Box>, workout_rows: RefCell<gtk::Box>,
view_model: RefCell<DayDetailViewModel>, view_model: RefCell<Option<DayDetailViewModel>>,
} }
impl Default for DayEditPrivate { impl Default for DayEditPrivate {
@ -213,7 +185,7 @@ impl Default for DayEditPrivate {
.hexpand(true) .hexpand(true)
.build(), .build(),
), ),
view_model: RefCell::new(DayDetailViewModel::default()), view_model: RefCell::new(None),
} }
} }
} }
@ -242,7 +214,7 @@ impl DayEdit {
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.imp().on_finished.borrow_mut() = Box::new(on_finished);
*s.imp().view_model.borrow_mut() = view_model.clone(); *s.imp().view_model.borrow_mut() = Some(view_model.clone());
let workout_buttons = workout_buttons(view_model.clone(), { let workout_buttons = workout_buttons(view_model.clone(), {
let s = s.clone(); let s = s.clone();
@ -284,7 +256,17 @@ impl DayEdit {
} }
fn finish(&self) { fn finish(&self) {
(self.imp().on_finished.borrow())() glib::spawn_future_local({
let s = self.clone();
async move {
let view_model = s.imp().view_model.borrow();
let view_model = view_model
.as_ref()
.expect("DayEdit has not been initialized with the view model");
let _ = view_model.save().await;
(s.imp().on_finished.borrow())()
}
});
} }
fn add_row(&self, workout: Record<TraxRecord>) { fn add_row(&self, workout: Record<TraxRecord>) {
@ -323,7 +305,11 @@ impl DayEdit {
_ => panic!("Record type {:?} is not a Time/Distance record", type_), _ => panic!("Record type {:?} is not a Time/Distance record", type_),
}; };
let record = Record { id, data }; let record = Record { id, data };
self.imp().view_model.borrow().update_record(record); let view_model = self.imp().view_model.borrow();
let view_model = view_model
.as_ref()
.expect("DayEdit has not been initialized with a view model");
view_model.update_record(record);
} }
} }
@ -332,10 +318,7 @@ fn control_buttons(s: &DayEdit, view_model: &DayDetailViewModel) -> ActionGroup
.primary_action("Save", { .primary_action("Save", {
let s = s.clone(); let s = s.clone();
let view_model = view_model.clone(); let view_model = view_model.clone();
move || { move || s.finish()
view_model.save();
s.finish();
}
}) })
.secondary_action("Cancel", { .secondary_action("Cancel", {
let s = s.clone(); let s = s.clone();
@ -419,7 +402,7 @@ where
biking_button.connect_clicked({ biking_button.connect_clicked({
let view_model = view_model.clone(); let view_model = view_model.clone();
move |_| { move |_| {
let workout = view_model.new_record(RecordType::Walk); let workout = view_model.new_record(RecordType::BikeRide);
add_row(workout); add_row(workout);
} }
}); });

View File

@ -27,9 +27,7 @@ mod steps;
pub use steps::{steps_editor, Steps}; pub use steps::{steps_editor, Steps};
mod text_entry; mod text_entry;
pub use text_entry::{ pub use text_entry::{distance_field, duration_field, time_field, weight_field, TextEntry};
distance_field, duration_field, time_field, weight_field, ParseError, TextEntry,
};
mod time_distance; mod time_distance;
pub use time_distance::{time_distance_detail, time_distance_summary}; pub use time_distance::{time_distance_detail, time_distance_summary};

View File

@ -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 <https://www.gnu.org/licenses/>. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::components::{text_entry::OnUpdate, ParseError, TextEntry}; use crate::{components::TextEntry, types::ParseError};
use gtk::prelude::*; use gtk::prelude::*;
#[derive(Default)] #[derive(Default)]

View File

@ -14,13 +14,11 @@ General Public License for more details.
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::types::{Distance, Duration, FormatOption, ParseError};
use dimensioned::si; use dimensioned::si;
use gtk::prelude::*; use gtk::prelude::*;
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
#[derive(Clone, Debug)]
pub struct ParseError;
pub type Renderer<T> = dyn Fn(&T) -> String; pub type Renderer<T> = dyn Fn(&T) -> String;
pub type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>; pub type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>;
pub type OnUpdate<T> = dyn Fn(Option<T>); pub type OnUpdate<T> = dyn Fn(Option<T>);
@ -153,47 +151,28 @@ where
) )
} }
pub fn distance_field<OnUpdate>( pub fn distance_field<OnUpdate>(value: Option<Distance>, on_update: OnUpdate) -> TextEntry<Distance>
value: Option<si::Meter<f64>>,
on_update: OnUpdate,
) -> TextEntry<si::Meter<f64>>
where where
OnUpdate: Fn(Option<si::Meter<f64>>) + 'static, OnUpdate: Fn(Option<Distance>) + 'static,
{ {
TextEntry::new( TextEntry::new(
"0 km", "0 km",
value, value,
|v| format!("{} km", v.value_unsafe / 1000.), |v| format!("{} km", v.value_unsafe / 1000.),
|s| { Distance::parse,
let digits = take_digits(s.to_owned());
let value = digits.parse::<f64>().map_err(|_| ParseError)?;
println!("value: {}", value);
Ok(value * 1000. * si::M)
},
on_update, on_update,
) )
} }
pub fn duration_field<OnUpdate>( pub fn duration_field<OnUpdate>(value: Option<Duration>, on_update: OnUpdate) -> TextEntry<Duration>
value: Option<si::Second<f64>>,
on_update: OnUpdate,
) -> TextEntry<si::Second<f64>>
where where
OnUpdate: Fn(Option<si::Second<f64>>) + 'static, OnUpdate: Fn(Option<Duration>) + 'static,
{ {
TextEntry::new( TextEntry::new(
"0 minutes", "0 minutes",
value, value,
|v| format!("{} minutes", v.value_unsafe / 1000.), |v| v.format(FormatOption::Abbreviated),
|s| { |s| Duration::parse(s),
let digits = take_digits(s.to_owned());
let value = digits.parse::<f64>().map_err(|_| ParseError)?;
Ok(value * 60. * si::S)
},
on_update, on_update,
) )
} }
fn take_digits(s: String) -> String {
s.chars().take_while(|t| t.is_digit(10)).collect::<String>()
}

View File

@ -17,28 +17,25 @@ You should have received a copy of the GNU General Public License along with Fit
// use crate::components::{EditView, ParseError, TextEntry}; // use crate::components::{EditView, ParseError, TextEntry};
// use chrono::{Local, NaiveDate}; // use chrono::{Local, NaiveDate};
// use dimensioned::si; // use dimensioned::si;
use crate::components::{distance_field, duration_field, time_field}; use crate::{
components::{distance_field, duration_field, time_field},
types::{Distance, Duration, FormatOption},
};
use dimensioned::si; use dimensioned::si;
use ft_core::{RecordType, TimeDistance}; use ft_core::{RecordType, TimeDistance};
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
pub fn time_distance_summary( pub fn time_distance_summary(distance: Distance, duration: Duration) -> Option<gtk::Label> {
distance: si::Meter<f64>, let text = match (*distance > si::M, *duration > si::S) {
duration: si::Second<f64>,
) -> Option<gtk::Label> {
let text = match (distance > si::M, duration > si::S) {
(true, true) => Some(format!( (true, true) => Some(format!(
"{} kilometers of biking in {} minutes", "{} of biking in {}",
distance.value_unsafe / 1000., distance.format(FormatOption::Full),
duration.value_unsafe / 60. duration.format(FormatOption::Full)
)), )),
(true, false) => Some(format!( (true, false) => Some(format!("{} of biking", distance.format(FormatOption::Full))),
"{} kilometers of biking", (false, true) => Some(format!("{} of biking", duration.format(FormatOption::Full))),
distance.value_unsafe / 1000.
)),
(false, true) => Some(format!("{} seconds of biking", duration.value_unsafe / 60.)),
(false, false) => None, (false, false) => None,
}; };
@ -72,7 +69,7 @@ pub fn time_distance_detail(type_: ft_core::RecordType, record: ft_core::TimeDis
.label( .label(
record record
.distance .distance
.map(|dist| format!("{}", dist)) .map(|dist| Distance::from(dist).format(FormatOption::Abbreviated))
.unwrap_or("".to_owned()), .unwrap_or("".to_owned()),
) )
.build(), .build(),
@ -84,7 +81,7 @@ pub fn time_distance_detail(type_: ft_core::RecordType, record: ft_core::TimeDis
.label( .label(
record record
.duration .duration
.map(|duration| format!("{}", duration)) .map(|duration| Duration::from(duration).format(FormatOption::Abbreviated))
.unwrap_or("".to_owned()), .unwrap_or("".to_owned()),
) )
.build(), .build(),
@ -173,16 +170,16 @@ impl TimeDistanceEdit {
.widget(), .widget(),
); );
details_row.append( details_row.append(
&distance_field(workout.distance, { &distance_field(workout.distance.map(Distance::from), {
let s = s.clone(); let s = s.clone();
move |d| s.update_distance(d) move |d| s.update_distance(d.map(|d| *d))
}) })
.widget(), .widget(),
); );
details_row.append( details_row.append(
&duration_field(workout.duration, { &duration_field(workout.duration.map(Duration::from), {
let s = s.clone(); let s = s.clone();
move |d| s.update_duration(d) move |d| s.update_duration(d.map(|d| *d))
}) })
.widget(), .widget(),
); );

View File

@ -1,4 +1,8 @@
use chrono::{Duration, Local, NaiveDate}; use chrono::{Local, NaiveDate};
use dimensioned::si;
#[derive(Clone, Debug)]
pub struct ParseError;
// This interval doesn't feel right, either. The idea that I have a specific interval type for just // This interval doesn't feel right, either. The idea that I have a specific interval type for just
// NaiveDate is odd. This should be genericized, as should the iterator. Also, it shouldn't live // NaiveDate is odd. This should be genericized, as should the iterator. Also, it shouldn't live
@ -12,7 +16,7 @@ pub struct DayInterval {
impl Default for DayInterval { impl Default for DayInterval {
fn default() -> Self { fn default() -> Self {
Self { Self {
start: (Local::now() - Duration::days(7)).date_naive(), start: (Local::now() - chrono::Duration::days(7)).date_naive(),
end: Local::now().date_naive(), end: Local::now().date_naive(),
} }
} }
@ -38,10 +42,132 @@ impl Iterator for DayIterator {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
if self.current <= self.end { if self.current <= self.end {
let val = self.current; let val = self.current;
self.current += Duration::days(1); self.current += chrono::Duration::days(1);
Some(val) Some(val)
} else { } else {
None None
} }
} }
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FormatOption {
Abbreviated,
Full,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
pub struct Distance {
value: si::Meter<f64>,
}
impl Distance {
pub fn format(&self, option: FormatOption) -> String {
match option {
FormatOption::Abbreviated => format!("{} km", self.value.value_unsafe / 1000.),
FormatOption::Full => format!("{} kilometers", self.value.value_unsafe / 1000.),
}
}
pub fn parse(s: &str) -> Result<Distance, ParseError> {
let digits = take_digits(s.to_owned());
let value = digits.parse::<f64>().map_err(|_| ParseError)?;
println!("value: {}", value);
Ok(Distance {
value: value * 1000. * si::M,
})
}
}
impl std::ops::Add for Distance {
type Output = Distance;
fn add(self, rside: Self) -> Self::Output {
Self::Output::from(self.value + rside.value)
}
}
impl std::ops::Sub for Distance {
type Output = Distance;
fn sub(self, rside: Self) -> Self::Output {
Self::Output::from(self.value - rside.value)
}
}
impl std::ops::Deref for Distance {
type Target = si::Meter<f64>;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl From<si::Meter<f64>> for Distance {
fn from(value: si::Meter<f64>) -> Self {
Self { value }
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
pub struct Duration {
value: si::Second<f64>,
}
impl Duration {
pub fn format(&self, option: FormatOption) -> String {
let (hours, minutes) = self.hours_and_minutes();
let (h, m) = match option {
FormatOption::Abbreviated => ("h", "m"),
FormatOption::Full => ("hours", "minutes"),
};
if hours > 0 {
format!("{}{} {}{}", hours, h, minutes, m)
} else {
format!("{}{}", minutes, m)
}
}
pub fn parse(s: &str) -> Result<Duration, ParseError> {
let digits = take_digits(s.to_owned());
let value = digits.parse::<f64>().map_err(|_| ParseError)?;
Ok(Duration {
value: value * 60. * si::S,
})
}
fn hours_and_minutes(&self) -> (i64, i64) {
let minutes: i64 = (self.value.value_unsafe / 60.).round() as i64;
let hours: i64 = (minutes / 60) as i64;
let minutes = minutes - (hours * 60);
(hours, minutes)
}
}
impl std::ops::Add for Duration {
type Output = Duration;
fn add(self, rside: Self) -> Self::Output {
Self::Output::from(self.value + rside.value)
}
}
impl std::ops::Sub for Duration {
type Output = Duration;
fn sub(self, rside: Self) -> Self::Output {
Self::Output::from(self.value - rside.value)
}
}
impl std::ops::Deref for Duration {
type Target = si::Second<f64>;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl From<si::Second<f64>> for Duration {
fn from(value: si::Second<f64>) -> Self {
Self { value }
}
}
fn take_digits(s: String) -> String {
s.chars().take_while(|t| t.is_digit(10)).collect::<String>()
}

View File

@ -14,7 +14,10 @@ General Public License for more details.
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::app::App; use crate::{
app::App,
types::{Distance, Duration},
};
use dimensioned::si; use dimensioned::si;
use emseries::{Record, RecordId, Recordable}; use emseries::{Record, RecordId, Recordable};
use ft_core::{RecordType, TimeDistance, TraxRecord}; use ft_core::{RecordType, TimeDistance, TraxRecord};
@ -90,33 +93,26 @@ impl<T: Clone + emseries::Recordable> Deref for RecordState<T> {
} }
} }
#[derive(Default)] #[derive(Clone)]
struct DayDetailViewModelInner {}
#[derive(Clone, Default)]
pub struct DayDetailViewModel { pub struct DayDetailViewModel {
app: Option<App>, app: App,
pub date: chrono::NaiveDate, pub date: chrono::NaiveDate,
weight: Arc<RwLock<Option<RecordState<ft_core::Weight>>>>, weight: Arc<RwLock<Option<RecordState<ft_core::Weight>>>>,
steps: Arc<RwLock<Option<RecordState<ft_core::Steps>>>>, steps: Arc<RwLock<Option<RecordState<ft_core::Steps>>>>,
records: Arc<RwLock<HashMap<RecordId, RecordState<TraxRecord>>>>, records: Arc<RwLock<HashMap<RecordId, RecordState<TraxRecord>>>>,
original_records: Vec<Record<TraxRecord>>,
} }
impl DayDetailViewModel { impl DayDetailViewModel {
pub fn new(date: chrono::NaiveDate, records: Vec<Record<TraxRecord>>, app: App) -> Self { pub async fn new(date: chrono::NaiveDate, app: App) -> Self {
let s = Self { let s = Self {
app: Some(app), app,
date, date,
weight: Arc::new(RwLock::new(None)), weight: Arc::new(RwLock::new(None)),
steps: Arc::new(RwLock::new(None)), steps: Arc::new(RwLock::new(None)),
records: Arc::new(RwLock::new(HashMap::new())), records: Arc::new(RwLock::new(HashMap::new())),
original_records: records,
}; };
s.populate_records(); s.populate_records().await;
s s
} }
@ -164,9 +160,9 @@ impl DayDetailViewModel {
*record = Some(new_record); *record = Some(new_record);
} }
pub fn biking_summary(&self) -> (si::Meter<f64>, si::Second<f64>) { pub fn biking_summary(&self) -> (Distance, Duration) {
self.records.read().unwrap().iter().fold( self.records.read().unwrap().iter().fold(
(0. * si::M, 0. * si::S), (Distance::default(), Duration::default()),
|(acc_distance, acc_duration), (_, record)| match record.data() { |(acc_distance, acc_duration), (_, record)| match record.data() {
Some(Record { Some(Record {
data: data:
@ -176,11 +172,11 @@ impl DayDetailViewModel {
.. ..
}) => ( }) => (
distance distance
.map(|distance| acc_distance + distance) .map(|distance| acc_distance + Distance::from(distance))
.unwrap_or(acc_distance), .unwrap_or(acc_distance),
(duration duration
.map(|duration| acc_duration + duration) .map(|duration| acc_duration + Duration::from(duration))
.unwrap_or(acc_duration)), .unwrap_or(acc_duration),
), ),
_ => (acc_distance, acc_duration), _ => (acc_distance, acc_duration),
@ -223,19 +219,19 @@ impl DayDetailViewModel {
.collect::<Vec<Record<TraxRecord>>>() .collect::<Vec<Record<TraxRecord>>>()
} }
pub fn save(&self) { pub fn save(&self) -> glib::JoinHandle<()> {
glib::spawn_future({ glib::spawn_future({
let s = self.clone(); let s = self.clone();
async move { async move {
if let Some(app) = s.app {
let weight_record = s.weight.read().unwrap().clone(); let weight_record = s.weight.read().unwrap().clone();
match weight_record { match weight_record {
Some(RecordState::New(Record { data, .. })) => { Some(RecordState::New(Record { data, .. })) => {
let _ = app.put_record(TraxRecord::Weight(data)).await; let _ = s.app.put_record(TraxRecord::Weight(data)).await;
} }
Some(RecordState::Original(_)) => {} Some(RecordState::Original(_)) => {}
Some(RecordState::Updated(weight)) => { Some(RecordState::Updated(weight)) => {
let _ = app let _ = s
.app
.update_record(Record { .update_record(Record {
id: weight.id, id: weight.id,
data: TraxRecord::Weight(weight.data), data: TraxRecord::Weight(weight.data),
@ -249,11 +245,12 @@ impl DayDetailViewModel {
let steps_record = s.steps.read().unwrap().clone(); let steps_record = s.steps.read().unwrap().clone();
match steps_record { match steps_record {
Some(RecordState::New(Record { data, .. })) => { Some(RecordState::New(Record { data, .. })) => {
let _ = app.put_record(TraxRecord::Steps(data)).await; let _ = s.app.put_record(TraxRecord::Steps(data)).await;
} }
Some(RecordState::Original(_)) => {} Some(RecordState::Original(_)) => {}
Some(RecordState::Updated(steps)) => { Some(RecordState::Updated(steps)) => {
let _ = app let _ = s
.app
.update_record(Record { .update_record(Record {
id: steps.id, id: steps.id,
data: TraxRecord::Steps(steps.data), data: TraxRecord::Steps(steps.data),
@ -276,26 +273,27 @@ impl DayDetailViewModel {
println!("saving record: {:?}", record); println!("saving record: {:?}", record);
match record { match record {
RecordState::New(Record { data, .. }) => { RecordState::New(Record { data, .. }) => {
let _ = app.put_record(data).await; let _ = s.app.put_record(data).await;
} }
RecordState::Original(_) => {} RecordState::Original(_) => {}
RecordState::Updated(r) => { RecordState::Updated(r) => {
let _ = app.update_record(r.clone()).await; let _ = s.app.update_record(r.clone()).await;
} }
RecordState::Deleted(_) => unimplemented!(), RecordState::Deleted(_) => unimplemented!(),
} }
} }
s.populate_records().await;
} }
} })
});
} }
pub fn revert(&self) { pub fn revert(&self) {
self.populate_records(); self.populate_records();
} }
fn populate_records(&self) { async fn populate_records(&self) {
let records = self.original_records.clone(); let records = self.app.records(self.date, self.date).await.unwrap();
let (weight_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) = let (weight_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
records.into_iter().partition(|r| r.data.is_weight()); records.into_iter().partition(|r| r.data.is_weight());

View File

@ -25,7 +25,7 @@ use std::cell::RefCell;
#[derive(Default)] #[derive(Default)]
pub struct DayDetailViewPrivate { pub struct DayDetailViewPrivate {
container: Singleton, container: Singleton,
view_model: RefCell<DayDetailViewModel>, view_model: RefCell<Option<DayDetailViewModel>>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -47,7 +47,7 @@ glib::wrapper! {
impl DayDetailView { impl DayDetailView {
pub fn new(view_model: DayDetailViewModel) -> 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().view_model.borrow_mut() = Some(view_model);
s.append(&s.imp().container); s.append(&s.imp().container);
@ -57,18 +57,26 @@ impl DayDetailView {
} }
fn view(&self) { fn view(&self) {
self.imp() let view_model = self.imp().view_model.borrow();
.container let view_model = view_model
.swap(&DayDetail::new(self.imp().view_model.borrow().clone(), { .as_ref()
.expect("DayDetailView has not been initialized with a view_model")
.clone();
self.imp().container.swap(&DayDetail::new(view_model, {
let s = self.clone(); let s = self.clone();
move || s.edit() move || s.edit()
})); }));
} }
fn edit(&self) { fn edit(&self) {
self.imp() let view_model = self.imp().view_model.borrow();
.container let view_model = view_model
.swap(&DayEdit::new(self.imp().view_model.borrow().clone(), { .as_ref()
.expect("DayDetailView has not been initialized with a view_model")
.clone();
self.imp().container.swap(&DayEdit::new(view_model, {
let s = self.clone(); let s = self.clone();
move || s.view() move || s.view()
})); }));

View File

@ -60,12 +60,12 @@ impl ObjectSubclass for HistoricalViewPrivate {
factory.connect_bind({ factory.connect_bind({
let app = s.app.clone(); let app = s.app.clone();
move |_, list_item| { move |_, list_item| {
let records = list_item let date = list_item
.downcast_ref::<gtk::ListItem>() .downcast_ref::<gtk::ListItem>()
.expect("should be a ListItem") .expect("should be a ListItem")
.item() .item()
.and_downcast::<DayRecords>() .and_downcast::<Date>()
.expect("should be a DaySummary"); .expect("should be a Date");
let summary = list_item let summary = list_item
.downcast_ref::<gtk::ListItem>() .downcast_ref::<gtk::ListItem>()
@ -75,11 +75,14 @@ impl ObjectSubclass for HistoricalViewPrivate {
.expect("should be a DaySummary"); .expect("should be a DaySummary");
if let Some(app) = app.borrow().clone() { if let Some(app) = app.borrow().clone() {
summary.set_data(DayDetailViewModel::new( let _ = glib::spawn_future_local(async move {
records.date(), println!(
records.records(), "setting up a DayDetailViewModel for {}",
app.clone(), date.date().format("%Y-%m-%d")
)); );
let view_model = DayDetailViewModel::new(date.date(), app.clone()).await;
summary.set_data(view_model);
});
} }
} }
}); });
@ -97,11 +100,7 @@ glib::wrapper! {
} }
impl HistoricalView { impl HistoricalView {
pub fn new<SelectFn>( pub fn new<SelectFn>(app: App, interval: DayInterval, on_select_day: Rc<SelectFn>) -> Self
app: App,
records: Vec<Record<TraxRecord>>,
on_select_day: Rc<SelectFn>,
) -> Self
where where
SelectFn: Fn(chrono::NaiveDate, Vec<Record<TraxRecord>>) + 'static, SelectFn: Fn(chrono::NaiveDate, Vec<Record<TraxRecord>>) + 'static,
{ {
@ -111,11 +110,8 @@ impl HistoricalView {
*s.imp().app.borrow_mut() = Some(app); *s.imp().app.borrow_mut() = Some(app);
let grouped_records = let mut model = gio::ListStore::new::<Date>();
GroupedRecords::new((*s.imp().time_window.borrow()).clone()).with_data(records); model.extend(interval.days().map(|d| Date::new(d)));
let mut model = gio::ListStore::new::<DayRecords>();
model.extend(grouped_records.items());
s.imp() s.imp()
.list_view .list_view
.set_model(Some(&gtk::NoSelection::new(Some(model)))); .set_model(Some(&gtk::NoSelection::new(Some(model))));
@ -125,8 +121,8 @@ impl HistoricalView {
move |s, idx| { move |s, idx| {
// This gets triggered whenever the user clicks on an item on the list. // This gets triggered whenever the user clicks on an item on the list.
let item = s.model().unwrap().item(idx).unwrap(); let item = s.model().unwrap().item(idx).unwrap();
let records = item.downcast_ref::<DayRecords>().unwrap(); let date = item.downcast_ref::<Date>().unwrap();
on_select_day(records.date(), records.records()); on_select_day(date.date(), vec![]);
} }
}); });
@ -135,101 +131,37 @@ impl HistoricalView {
s s
} }
pub fn set_records(&self, records: Vec<Record<TraxRecord>>) {
println!("set_records: {:?}", records);
let grouped_records =
GroupedRecords::new((self.imp().time_window.borrow()).clone()).with_data(records);
let mut model = gio::ListStore::new::<DayRecords>();
model.extend(grouped_records.items());
self.imp()
.list_view
.set_model(Some(&gtk::NoSelection::new(Some(model))));
}
pub fn time_window(&self) -> DayInterval { pub fn time_window(&self) -> DayInterval {
self.imp().time_window.borrow().clone() self.imp().time_window.borrow().clone()
} }
} }
#[derive(Default)] #[derive(Default)]
pub struct DayRecordsPrivate { pub struct DatePrivate {
date: RefCell<chrono::NaiveDate>, date: RefCell<chrono::NaiveDate>,
records: RefCell<Vec<Record<TraxRecord>>>,
} }
#[glib::object_subclass] #[glib::object_subclass]
impl ObjectSubclass for DayRecordsPrivate { impl ObjectSubclass for DatePrivate {
const NAME: &'static str = "DayRecords"; const NAME: &'static str = "Date";
type Type = DayRecords; type Type = Date;
} }
impl ObjectImpl for DayRecordsPrivate {} impl ObjectImpl for DatePrivate {}
glib::wrapper! { glib::wrapper! {
pub struct DayRecords(ObjectSubclass<DayRecordsPrivate>); pub struct Date(ObjectSubclass<DatePrivate>);
} }
impl DayRecords { impl Date {
pub fn new(date: chrono::NaiveDate, records: Vec<Record<TraxRecord>>) -> Self { pub fn new(date: chrono::NaiveDate) -> Self {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
*s.imp().date.borrow_mut() = date; *s.imp().date.borrow_mut() = date;
*s.imp().records.borrow_mut() = records;
s s
} }
pub fn date(&self) -> chrono::NaiveDate { pub fn date(&self) -> chrono::NaiveDate {
*self.imp().date.borrow() self.imp().date.borrow().clone()
}
pub fn records(&self) -> Vec<Record<TraxRecord>> {
self.imp().records.borrow().clone()
}
pub fn add_record(&self, record: Record<TraxRecord>) {
self.imp().records.borrow_mut().push(record);
}
}
// This isn't feeling quite right. DayRecords is a glib object, but I'm not sure that I want to
// really be passing that around. It seems not generic enough. I feel like this whole grouped
// records thing can be made more generic.
struct GroupedRecords {
interval: DayInterval,
data: HashMap<NaiveDate, DayRecords>,
}
impl GroupedRecords {
fn new(interval: DayInterval) -> Self {
let mut s = Self {
interval: interval.clone(),
data: HashMap::new(),
};
interval.days().for_each(|date| {
let _ = s.data.insert(date, DayRecords::new(date, vec![]));
});
s
}
fn with_data(mut self, records: Vec<Record<TraxRecord>>) -> Self {
records.into_iter().for_each(|record| {
self.data
.entry(record.date())
.and_modify(|entry: &mut DayRecords| (*entry).add_record(record.clone()))
.or_insert(DayRecords::new(record.date(), vec![record]));
});
self
}
fn items(&self) -> impl Iterator<Item = DayRecords> + '_ {
self.interval.days().map(|date| {
self.data
.get(&date)
.cloned()
.unwrap_or(DayRecords::new(date, vec![]))
})
} }
} }