Compare commits
No commits in common. "33c85ee7b35c9175ace8521ac49843946332b1fa" and "4fd377a3f15e8a71e0cd4407934707149b7542c1" have entirely different histories.
33c85ee7b3
...
4fd377a3f1
|
@ -57,20 +57,6 @@ 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,
|
||||||
|
|
|
@ -16,7 +16,6 @@ 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},
|
||||||
};
|
};
|
||||||
|
@ -134,34 +133,25 @@ impl AppWindow {
|
||||||
self.swap_main(view);
|
self.swap_main(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_historical_view(&self, start_date: chrono::NaiveDate, end_date: chrono::NaiveDate) {
|
fn show_historical_view(&self, records: Vec<Record<TraxRecord>>) {
|
||||||
let on_select_day = {
|
let view = View::Historical(HistoricalView::new(self.app.clone(), records, {
|
||||||
let s = self.clone();
|
let s = self.clone();
|
||||||
move |date, records| {
|
Rc::new(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(view_model));
|
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)
|
||||||
.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +162,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(start, end),
|
Ok(records) => s.show_historical_view(records),
|
||||||
Err(_) => s.show_welcome_view(),
|
Err(_) => s.show_welcome_view(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,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::{
|
use crate::{
|
||||||
components::{steps_editor, time_distance_summary, weight_field, ActionGroup, Steps, Weight},
|
components::{
|
||||||
|
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};
|
||||||
|
@ -140,6 +143,31 @@ 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();
|
||||||
|
@ -172,7 +200,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<Option<DayDetailViewModel>>,
|
view_model: RefCell<DayDetailViewModel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DayEditPrivate {
|
impl Default for DayEditPrivate {
|
||||||
|
@ -185,7 +213,7 @@ impl Default for DayEditPrivate {
|
||||||
.hexpand(true)
|
.hexpand(true)
|
||||||
.build(),
|
.build(),
|
||||||
),
|
),
|
||||||
view_model: RefCell::new(None),
|
view_model: RefCell::new(DayDetailViewModel::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -214,7 +242,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() = Some(view_model.clone());
|
*s.imp().view_model.borrow_mut() = 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();
|
||||||
|
@ -256,17 +284,7 @@ impl DayEdit {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(&self) {
|
fn finish(&self) {
|
||||||
glib::spawn_future_local({
|
(self.imp().on_finished.borrow())()
|
||||||
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>) {
|
||||||
|
@ -305,11 +323,7 @@ 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 };
|
||||||
let view_model = self.imp().view_model.borrow();
|
self.imp().view_model.borrow().update_record(record);
|
||||||
let view_model = view_model
|
|
||||||
.as_ref()
|
|
||||||
.expect("DayEdit has not been initialized with a view model");
|
|
||||||
view_model.update_record(record);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,7 +332,10 @@ 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 || s.finish()
|
move || {
|
||||||
|
view_model.save();
|
||||||
|
s.finish();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.secondary_action("Cancel", {
|
.secondary_action("Cancel", {
|
||||||
let s = s.clone();
|
let s = s.clone();
|
||||||
|
@ -402,7 +419,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::BikeRide);
|
let workout = view_model.new_record(RecordType::Walk);
|
||||||
add_row(workout);
|
add_row(workout);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,7 +27,9 @@ mod steps;
|
||||||
pub use steps::{steps_editor, Steps};
|
pub use steps::{steps_editor, Steps};
|
||||||
|
|
||||||
mod text_entry;
|
mod text_entry;
|
||||||
pub use text_entry::{distance_field, duration_field, time_field, weight_field, TextEntry};
|
pub use text_entry::{
|
||||||
|
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};
|
||||||
|
|
|
@ -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::TextEntry, types::ParseError};
|
use crate::components::{text_entry::OnUpdate, ParseError, TextEntry};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
|
@ -14,11 +14,13 @@ 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>);
|
||||||
|
@ -151,28 +153,47 @@ where
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn distance_field<OnUpdate>(value: Option<Distance>, on_update: OnUpdate) -> TextEntry<Distance>
|
pub fn distance_field<OnUpdate>(
|
||||||
|
value: Option<si::Meter<f64>>,
|
||||||
|
on_update: OnUpdate,
|
||||||
|
) -> TextEntry<si::Meter<f64>>
|
||||||
where
|
where
|
||||||
OnUpdate: Fn(Option<Distance>) + 'static,
|
OnUpdate: Fn(Option<si::Meter<f64>>) + '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.),
|
||||||
Distance::parse,
|
|s| {
|
||||||
|
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>(value: Option<Duration>, on_update: OnUpdate) -> TextEntry<Duration>
|
pub fn duration_field<OnUpdate>(
|
||||||
|
value: Option<si::Second<f64>>,
|
||||||
|
on_update: OnUpdate,
|
||||||
|
) -> TextEntry<si::Second<f64>>
|
||||||
where
|
where
|
||||||
OnUpdate: Fn(Option<Duration>) + 'static,
|
OnUpdate: Fn(Option<si::Second<f64>>) + 'static,
|
||||||
{
|
{
|
||||||
TextEntry::new(
|
TextEntry::new(
|
||||||
"0 minutes",
|
"0 minutes",
|
||||||
value,
|
value,
|
||||||
|v| v.format(FormatOption::Abbreviated),
|
|v| format!("{} minutes", v.value_unsafe / 1000.),
|
||||||
|s| Duration::parse(s),
|
|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>()
|
||||||
|
}
|
||||||
|
|
|
@ -17,25 +17,28 @@ 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::{
|
use crate::components::{distance_field, duration_field, time_field};
|
||||||
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(distance: Distance, duration: Duration) -> Option<gtk::Label> {
|
pub fn time_distance_summary(
|
||||||
let text = match (*distance > si::M, *duration > si::S) {
|
distance: si::Meter<f64>,
|
||||||
|
duration: si::Second<f64>,
|
||||||
|
) -> Option<gtk::Label> {
|
||||||
|
let text = match (distance > si::M, duration > si::S) {
|
||||||
(true, true) => Some(format!(
|
(true, true) => Some(format!(
|
||||||
"{} of biking in {}",
|
"{} kilometers of biking in {} minutes",
|
||||||
distance.format(FormatOption::Full),
|
distance.value_unsafe / 1000.,
|
||||||
duration.format(FormatOption::Full)
|
duration.value_unsafe / 60.
|
||||||
)),
|
)),
|
||||||
(true, false) => Some(format!("{} of biking", distance.format(FormatOption::Full))),
|
(true, false) => Some(format!(
|
||||||
(false, true) => Some(format!("{} of biking", duration.format(FormatOption::Full))),
|
"{} kilometers of biking",
|
||||||
|
distance.value_unsafe / 1000.
|
||||||
|
)),
|
||||||
|
(false, true) => Some(format!("{} seconds of biking", duration.value_unsafe / 60.)),
|
||||||
(false, false) => None,
|
(false, false) => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -69,7 +72,7 @@ pub fn time_distance_detail(type_: ft_core::RecordType, record: ft_core::TimeDis
|
||||||
.label(
|
.label(
|
||||||
record
|
record
|
||||||
.distance
|
.distance
|
||||||
.map(|dist| Distance::from(dist).format(FormatOption::Abbreviated))
|
.map(|dist| format!("{}", dist))
|
||||||
.unwrap_or("".to_owned()),
|
.unwrap_or("".to_owned()),
|
||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
|
@ -81,7 +84,7 @@ pub fn time_distance_detail(type_: ft_core::RecordType, record: ft_core::TimeDis
|
||||||
.label(
|
.label(
|
||||||
record
|
record
|
||||||
.duration
|
.duration
|
||||||
.map(|duration| Duration::from(duration).format(FormatOption::Abbreviated))
|
.map(|duration| format!("{}", duration))
|
||||||
.unwrap_or("".to_owned()),
|
.unwrap_or("".to_owned()),
|
||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
|
@ -170,16 +173,16 @@ impl TimeDistanceEdit {
|
||||||
.widget(),
|
.widget(),
|
||||||
);
|
);
|
||||||
details_row.append(
|
details_row.append(
|
||||||
&distance_field(workout.distance.map(Distance::from), {
|
&distance_field(workout.distance, {
|
||||||
let s = s.clone();
|
let s = s.clone();
|
||||||
move |d| s.update_distance(d.map(|d| *d))
|
move |d| s.update_distance(d)
|
||||||
})
|
})
|
||||||
.widget(),
|
.widget(),
|
||||||
);
|
);
|
||||||
details_row.append(
|
details_row.append(
|
||||||
&duration_field(workout.duration.map(Duration::from), {
|
&duration_field(workout.duration, {
|
||||||
let s = s.clone();
|
let s = s.clone();
|
||||||
move |d| s.update_duration(d.map(|d| *d))
|
move |d| s.update_duration(d)
|
||||||
})
|
})
|
||||||
.widget(),
|
.widget(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
use chrono::{Local, NaiveDate};
|
use chrono::{Duration, 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
|
||||||
|
@ -16,7 +12,7 @@ pub struct DayInterval {
|
||||||
impl Default for DayInterval {
|
impl Default for DayInterval {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
start: (Local::now() - chrono::Duration::days(7)).date_naive(),
|
start: (Local::now() - Duration::days(7)).date_naive(),
|
||||||
end: Local::now().date_naive(),
|
end: Local::now().date_naive(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,132 +38,10 @@ 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 += chrono::Duration::days(1);
|
self.current += 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>()
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 <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::{
|
use crate::app::App;
|
||||||
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};
|
||||||
|
@ -93,26 +90,33 @@ impl<T: Clone + emseries::Recordable> Deref for RecordState<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Default)]
|
||||||
|
struct DayDetailViewModelInner {}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
pub struct DayDetailViewModel {
|
pub struct DayDetailViewModel {
|
||||||
app: App,
|
app: Option<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 async fn new(date: chrono::NaiveDate, app: App) -> Self {
|
pub fn new(date: chrono::NaiveDate, records: Vec<Record<TraxRecord>>, app: App) -> Self {
|
||||||
let s = Self {
|
let s = Self {
|
||||||
app,
|
app: Some(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().await;
|
s.populate_records();
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,9 +164,9 @@ impl DayDetailViewModel {
|
||||||
*record = Some(new_record);
|
*record = Some(new_record);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn biking_summary(&self) -> (Distance, Duration) {
|
pub fn biking_summary(&self) -> (si::Meter<f64>, si::Second<f64>) {
|
||||||
self.records.read().unwrap().iter().fold(
|
self.records.read().unwrap().iter().fold(
|
||||||
(Distance::default(), Duration::default()),
|
(0. * si::M, 0. * si::S),
|
||||||
|(acc_distance, acc_duration), (_, record)| match record.data() {
|
|(acc_distance, acc_duration), (_, record)| match record.data() {
|
||||||
Some(Record {
|
Some(Record {
|
||||||
data:
|
data:
|
||||||
|
@ -172,11 +176,11 @@ impl DayDetailViewModel {
|
||||||
..
|
..
|
||||||
}) => (
|
}) => (
|
||||||
distance
|
distance
|
||||||
.map(|distance| acc_distance + Distance::from(distance))
|
.map(|distance| acc_distance + distance)
|
||||||
.unwrap_or(acc_distance),
|
.unwrap_or(acc_distance),
|
||||||
duration
|
(duration
|
||||||
.map(|duration| acc_duration + Duration::from(duration))
|
.map(|duration| acc_duration + duration)
|
||||||
.unwrap_or(acc_duration),
|
.unwrap_or(acc_duration)),
|
||||||
),
|
),
|
||||||
|
|
||||||
_ => (acc_distance, acc_duration),
|
_ => (acc_distance, acc_duration),
|
||||||
|
@ -219,19 +223,19 @@ impl DayDetailViewModel {
|
||||||
.collect::<Vec<Record<TraxRecord>>>()
|
.collect::<Vec<Record<TraxRecord>>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self) -> glib::JoinHandle<()> {
|
pub fn save(&self) {
|
||||||
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 _ = s.app.put_record(TraxRecord::Weight(data)).await;
|
let _ = app.put_record(TraxRecord::Weight(data)).await;
|
||||||
}
|
}
|
||||||
Some(RecordState::Original(_)) => {}
|
Some(RecordState::Original(_)) => {}
|
||||||
Some(RecordState::Updated(weight)) => {
|
Some(RecordState::Updated(weight)) => {
|
||||||
let _ = s
|
let _ = app
|
||||||
.app
|
|
||||||
.update_record(Record {
|
.update_record(Record {
|
||||||
id: weight.id,
|
id: weight.id,
|
||||||
data: TraxRecord::Weight(weight.data),
|
data: TraxRecord::Weight(weight.data),
|
||||||
|
@ -245,12 +249,11 @@ 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 _ = s.app.put_record(TraxRecord::Steps(data)).await;
|
let _ = app.put_record(TraxRecord::Steps(data)).await;
|
||||||
}
|
}
|
||||||
Some(RecordState::Original(_)) => {}
|
Some(RecordState::Original(_)) => {}
|
||||||
Some(RecordState::Updated(steps)) => {
|
Some(RecordState::Updated(steps)) => {
|
||||||
let _ = s
|
let _ = app
|
||||||
.app
|
|
||||||
.update_record(Record {
|
.update_record(Record {
|
||||||
id: steps.id,
|
id: steps.id,
|
||||||
data: TraxRecord::Steps(steps.data),
|
data: TraxRecord::Steps(steps.data),
|
||||||
|
@ -273,27 +276,26 @@ impl DayDetailViewModel {
|
||||||
println!("saving record: {:?}", record);
|
println!("saving record: {:?}", record);
|
||||||
match record {
|
match record {
|
||||||
RecordState::New(Record { data, .. }) => {
|
RecordState::New(Record { data, .. }) => {
|
||||||
let _ = s.app.put_record(data).await;
|
let _ = app.put_record(data).await;
|
||||||
}
|
}
|
||||||
RecordState::Original(_) => {}
|
RecordState::Original(_) => {}
|
||||||
RecordState::Updated(r) => {
|
RecordState::Updated(r) => {
|
||||||
let _ = s.app.update_record(r.clone()).await;
|
let _ = 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn populate_records(&self) {
|
fn populate_records(&self) {
|
||||||
let records = self.app.records(self.date, self.date).await.unwrap();
|
let records = self.original_records.clone();
|
||||||
|
|
||||||
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());
|
||||||
|
|
|
@ -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<Option<DayDetailViewModel>>,
|
view_model: RefCell<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() = Some(view_model);
|
*s.imp().view_model.borrow_mut() = view_model;
|
||||||
|
|
||||||
s.append(&s.imp().container);
|
s.append(&s.imp().container);
|
||||||
|
|
||||||
|
@ -57,26 +57,18 @@ impl DayDetailView {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self) {
|
fn view(&self) {
|
||||||
let view_model = self.imp().view_model.borrow();
|
self.imp()
|
||||||
let view_model = view_model
|
.container
|
||||||
.as_ref()
|
.swap(&DayDetail::new(self.imp().view_model.borrow().clone(), {
|
||||||
.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) {
|
||||||
let view_model = self.imp().view_model.borrow();
|
self.imp()
|
||||||
let view_model = view_model
|
.container
|
||||||
.as_ref()
|
.swap(&DayEdit::new(self.imp().view_model.borrow().clone(), {
|
||||||
.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()
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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 date = list_item
|
let records = list_item
|
||||||
.downcast_ref::<gtk::ListItem>()
|
.downcast_ref::<gtk::ListItem>()
|
||||||
.expect("should be a ListItem")
|
.expect("should be a ListItem")
|
||||||
.item()
|
.item()
|
||||||
.and_downcast::<Date>()
|
.and_downcast::<DayRecords>()
|
||||||
.expect("should be a Date");
|
.expect("should be a DaySummary");
|
||||||
|
|
||||||
let summary = list_item
|
let summary = list_item
|
||||||
.downcast_ref::<gtk::ListItem>()
|
.downcast_ref::<gtk::ListItem>()
|
||||||
|
@ -75,14 +75,11 @@ 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() {
|
||||||
let _ = glib::spawn_future_local(async move {
|
summary.set_data(DayDetailViewModel::new(
|
||||||
println!(
|
records.date(),
|
||||||
"setting up a DayDetailViewModel for {}",
|
records.records(),
|
||||||
date.date().format("%Y-%m-%d")
|
app.clone(),
|
||||||
);
|
));
|
||||||
let view_model = DayDetailViewModel::new(date.date(), app.clone()).await;
|
|
||||||
summary.set_data(view_model);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -100,7 +97,11 @@ glib::wrapper! {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HistoricalView {
|
impl HistoricalView {
|
||||||
pub fn new<SelectFn>(app: App, interval: DayInterval, on_select_day: Rc<SelectFn>) -> Self
|
pub fn new<SelectFn>(
|
||||||
|
app: App,
|
||||||
|
records: Vec<Record<TraxRecord>>,
|
||||||
|
on_select_day: Rc<SelectFn>,
|
||||||
|
) -> Self
|
||||||
where
|
where
|
||||||
SelectFn: Fn(chrono::NaiveDate, Vec<Record<TraxRecord>>) + 'static,
|
SelectFn: Fn(chrono::NaiveDate, Vec<Record<TraxRecord>>) + 'static,
|
||||||
{
|
{
|
||||||
|
@ -110,8 +111,11 @@ impl HistoricalView {
|
||||||
|
|
||||||
*s.imp().app.borrow_mut() = Some(app);
|
*s.imp().app.borrow_mut() = Some(app);
|
||||||
|
|
||||||
let mut model = gio::ListStore::new::<Date>();
|
let grouped_records =
|
||||||
model.extend(interval.days().map(|d| Date::new(d)));
|
GroupedRecords::new((*s.imp().time_window.borrow()).clone()).with_data(records);
|
||||||
|
|
||||||
|
let mut model = gio::ListStore::new::<DayRecords>();
|
||||||
|
model.extend(grouped_records.items());
|
||||||
s.imp()
|
s.imp()
|
||||||
.list_view
|
.list_view
|
||||||
.set_model(Some(>k::NoSelection::new(Some(model))));
|
.set_model(Some(>k::NoSelection::new(Some(model))));
|
||||||
|
@ -121,8 +125,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 date = item.downcast_ref::<Date>().unwrap();
|
let records = item.downcast_ref::<DayRecords>().unwrap();
|
||||||
on_select_day(date.date(), vec![]);
|
on_select_day(records.date(), records.records());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -131,37 +135,101 @@ 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(>k::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 DatePrivate {
|
pub struct DayRecordsPrivate {
|
||||||
date: RefCell<chrono::NaiveDate>,
|
date: RefCell<chrono::NaiveDate>,
|
||||||
|
records: RefCell<Vec<Record<TraxRecord>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
impl ObjectSubclass for DatePrivate {
|
impl ObjectSubclass for DayRecordsPrivate {
|
||||||
const NAME: &'static str = "Date";
|
const NAME: &'static str = "DayRecords";
|
||||||
type Type = Date;
|
type Type = DayRecords;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjectImpl for DatePrivate {}
|
impl ObjectImpl for DayRecordsPrivate {}
|
||||||
|
|
||||||
glib::wrapper! {
|
glib::wrapper! {
|
||||||
pub struct Date(ObjectSubclass<DatePrivate>);
|
pub struct DayRecords(ObjectSubclass<DayRecordsPrivate>);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Date {
|
impl DayRecords {
|
||||||
pub fn new(date: chrono::NaiveDate) -> Self {
|
pub fn new(date: chrono::NaiveDate, records: Vec<Record<TraxRecord>>) -> 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().clone()
|
*self.imp().date.borrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
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![]))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue