Compare commits

..

No commits in common. "33c85ee7b35c9175ace8521ac49843946332b1fa" and "4fd377a3f15e8a71e0cd4407934707149b7542c1" have entirely different histories.

11 changed files with 299 additions and 344 deletions

View File

@ -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,

View File

@ -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(); let layout = gtk::Box::new(gtk::Orientation::Vertical, 0);
glib::spawn_future_local(async move { layout.append(&adw::HeaderBar::new());
let view_model = DayDetailViewModel::new(date, s.app.clone()).await; // layout.append(&DayDetailView::new(date, records, s.app.clone()));
let layout = gtk::Box::new(gtk::Orientation::Vertical, 0); layout.append(&DayDetailView::new(DayDetailViewModel::new(
layout.append(&adw::HeaderBar::new()); date,
// layout.append(&DayDetailView::new(date, records, s.app.clone())); records,
layout.append(&DayDetailView::new(view_model)); s.app.clone(),
let page = &adw::NavigationPage::builder() )));
.title(date.format("%Y-%m-%d").to_string()) let page = &adw::NavigationPage::builder()
.child(&layout) .title(date.format("%Y-%m-%d").to_string())
.build(); .child(&layout)
s.navigation.push(page); .build();
}); 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(),
} }
} }

View File

@ -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);
} }
}); });

View File

@ -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};

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::TextEntry, types::ParseError}; use crate::components::{text_entry::OnUpdate, ParseError, TextEntry};
use gtk::prelude::*; use gtk::prelude::*;
#[derive(Default)] #[derive(Default)]

View File

@ -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>()
}

View File

@ -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(),
); );

View File

@ -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>()
}

View File

@ -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,81 +223,79 @@ 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 {
let weight_record = s.weight.read().unwrap().clone(); if let Some(app) = s.app {
match weight_record { let weight_record = s.weight.read().unwrap().clone();
Some(RecordState::New(Record { data, .. })) => { match weight_record {
let _ = s.app.put_record(TraxRecord::Weight(data)).await; Some(RecordState::New(Record { data, .. })) => {
} let _ = app.put_record(TraxRecord::Weight(data)).await;
Some(RecordState::Original(_)) => {}
Some(RecordState::Updated(weight)) => {
let _ = s
.app
.update_record(Record {
id: weight.id,
data: TraxRecord::Weight(weight.data),
})
.await;
}
Some(RecordState::Deleted(_)) => {}
None => {}
}
let steps_record = s.steps.read().unwrap().clone();
match steps_record {
Some(RecordState::New(Record { data, .. })) => {
let _ = s.app.put_record(TraxRecord::Steps(data)).await;
}
Some(RecordState::Original(_)) => {}
Some(RecordState::Updated(steps)) => {
let _ = s
.app
.update_record(Record {
id: steps.id,
data: TraxRecord::Steps(steps.data),
})
.await;
}
Some(RecordState::Deleted(_)) => {}
None => {}
}
let records = s
.records
.write()
.unwrap()
.drain()
.map(|(_, record)| record)
.collect::<Vec<RecordState<TraxRecord>>>();
for record in records {
println!("saving record: {:?}", record);
match record {
RecordState::New(Record { data, .. }) => {
let _ = s.app.put_record(data).await;
} }
RecordState::Original(_) => {} Some(RecordState::Original(_)) => {}
RecordState::Updated(r) => { Some(RecordState::Updated(weight)) => {
let _ = s.app.update_record(r.clone()).await; let _ = app
.update_record(Record {
id: weight.id,
data: TraxRecord::Weight(weight.data),
})
.await;
}
Some(RecordState::Deleted(_)) => {}
None => {}
}
let steps_record = s.steps.read().unwrap().clone();
match steps_record {
Some(RecordState::New(Record { data, .. })) => {
let _ = app.put_record(TraxRecord::Steps(data)).await;
}
Some(RecordState::Original(_)) => {}
Some(RecordState::Updated(steps)) => {
let _ = app
.update_record(Record {
id: steps.id,
data: TraxRecord::Steps(steps.data),
})
.await;
}
Some(RecordState::Deleted(_)) => {}
None => {}
}
let records = s
.records
.write()
.unwrap()
.drain()
.map(|(_, record)| record)
.collect::<Vec<RecordState<TraxRecord>>>();
for record in records {
println!("saving record: {:?}", record);
match record {
RecordState::New(Record { data, .. }) => {
let _ = app.put_record(data).await;
}
RecordState::Original(_) => {}
RecordState::Updated(r) => {
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());

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<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,28 +57,20 @@ 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") let s = self.clone();
.clone(); move || s.edit()
}));
self.imp().container.swap(&DayDetail::new(view_model, {
let s = self.clone();
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") let s = self.clone();
.clone(); move || s.view()
}));
self.imp().container.swap(&DayEdit::new(view_model, {
let s = self.clone();
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 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(&gtk::NoSelection::new(Some(model)))); .set_model(Some(&gtk::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(&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 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![]))
})
} }
} }