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(
&self,
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::{
app::App,
types::DayInterval,
view_models::DayDetailViewModel,
views::{DayDetailView, HistoricalView, PlaceholderView, View, WelcomeView},
};
@ -133,25 +134,34 @@ impl AppWindow {
self.swap_main(view);
}
fn show_historical_view(&self, records: Vec<Record<TraxRecord>>) {
let view = View::Historical(HistoricalView::new(self.app.clone(), records, {
fn show_historical_view(&self, start_date: chrono::NaiveDate, end_date: chrono::NaiveDate) {
let on_select_day = {
let s = self.clone();
Rc::new(move |date, records| {
let layout = gtk::Box::new(gtk::Orientation::Vertical, 0);
layout.append(&adw::HeaderBar::new());
// layout.append(&DayDetailView::new(date, records, s.app.clone()));
layout.append(&DayDetailView::new(DayDetailViewModel::new(
date,
records,
s.app.clone(),
)));
let page = &adw::NavigationPage::builder()
.title(date.format("%Y-%m-%d").to_string())
.child(&layout)
.build();
s.navigation.push(page);
})
}));
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);
layout.append(&adw::HeaderBar::new());
// layout.append(&DayDetailView::new(date, records, s.app.clone()));
layout.append(&DayDetailView::new(view_model));
let page = &adw::NavigationPage::builder()
.title(date.format("%Y-%m-%d").to_string())
.child(&layout)
.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);
}
@ -162,7 +172,7 @@ impl AppWindow {
let end = Local::now().date_naive();
let start = end - Duration::days(7);
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(),
}
}

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 ft_core::TraxRecord;
use crate::{
components::{
steps_editor, text_entry::distance_field, time_distance_summary, weight_field, ActionGroup,
Steps, Weight,
},
components::{steps_editor, time_distance_summary, weight_field, ActionGroup, Steps, Weight},
view_models::DayDetailViewModel,
};
use emseries::{Record, RecordId};
@ -143,31 +140,6 @@ impl DayDetail {
.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()
.orientation(gtk::Orientation::Horizontal)
.build();
@ -200,7 +172,7 @@ impl DayDetail {
pub struct DayEditPrivate {
on_finished: RefCell<Box<dyn Fn()>>,
workout_rows: RefCell<gtk::Box>,
view_model: RefCell<DayDetailViewModel>,
view_model: RefCell<Option<DayDetailViewModel>>,
}
impl Default for DayEditPrivate {
@ -213,7 +185,7 @@ impl Default for DayEditPrivate {
.hexpand(true)
.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_hexpand(true);
*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 s = s.clone();
@ -284,7 +256,17 @@ impl DayEdit {
}
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>) {
@ -323,7 +305,11 @@ impl DayEdit {
_ => panic!("Record type {:?} is not a Time/Distance record", type_),
};
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", {
let s = s.clone();
let view_model = view_model.clone();
move || {
view_model.save();
s.finish();
}
move || s.finish()
})
.secondary_action("Cancel", {
let s = s.clone();
@ -419,7 +402,7 @@ where
biking_button.connect_clicked({
let view_model = view_model.clone();
move |_| {
let workout = view_model.new_record(RecordType::Walk);
let workout = view_model.new_record(RecordType::BikeRide);
add_row(workout);
}
});

View File

@ -27,9 +27,7 @@ mod steps;
pub use steps::{steps_editor, Steps};
mod text_entry;
pub use text_entry::{
distance_field, duration_field, time_field, weight_field, ParseError, TextEntry,
};
pub use text_entry::{distance_field, duration_field, time_field, weight_field, TextEntry};
mod time_distance;
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/>.
*/
use crate::components::{text_entry::OnUpdate, ParseError, TextEntry};
use crate::{components::TextEntry, types::ParseError};
use gtk::prelude::*;
#[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/>.
*/
use crate::types::{Distance, Duration, FormatOption, ParseError};
use dimensioned::si;
use gtk::prelude::*;
use std::{cell::RefCell, rc::Rc};
#[derive(Clone, Debug)]
pub struct ParseError;
pub type Renderer<T> = dyn Fn(&T) -> String;
pub type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>;
pub type OnUpdate<T> = dyn Fn(Option<T>);
@ -153,47 +151,28 @@ where
)
}
pub fn distance_field<OnUpdate>(
value: Option<si::Meter<f64>>,
on_update: OnUpdate,
) -> TextEntry<si::Meter<f64>>
pub fn distance_field<OnUpdate>(value: Option<Distance>, on_update: OnUpdate) -> TextEntry<Distance>
where
OnUpdate: Fn(Option<si::Meter<f64>>) + 'static,
OnUpdate: Fn(Option<Distance>) + 'static,
{
TextEntry::new(
"0 km",
value,
|v| format!("{} km", v.value_unsafe / 1000.),
|s| {
let digits = take_digits(s.to_owned());
let value = digits.parse::<f64>().map_err(|_| ParseError)?;
println!("value: {}", value);
Ok(value * 1000. * si::M)
},
Distance::parse,
on_update,
)
}
pub fn duration_field<OnUpdate>(
value: Option<si::Second<f64>>,
on_update: OnUpdate,
) -> TextEntry<si::Second<f64>>
pub fn duration_field<OnUpdate>(value: Option<Duration>, on_update: OnUpdate) -> TextEntry<Duration>
where
OnUpdate: Fn(Option<si::Second<f64>>) + 'static,
OnUpdate: Fn(Option<Duration>) + 'static,
{
TextEntry::new(
"0 minutes",
value,
|v| format!("{} minutes", v.value_unsafe / 1000.),
|s| {
let digits = take_digits(s.to_owned());
let value = digits.parse::<f64>().map_err(|_| ParseError)?;
Ok(value * 60. * si::S)
},
|v| v.format(FormatOption::Abbreviated),
|s| Duration::parse(s),
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 chrono::{Local, NaiveDate};
// 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 ft_core::{RecordType, TimeDistance};
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use std::{cell::RefCell, rc::Rc};
pub fn time_distance_summary(
distance: si::Meter<f64>,
duration: si::Second<f64>,
) -> Option<gtk::Label> {
let text = match (distance > si::M, duration > si::S) {
pub fn time_distance_summary(distance: Distance, duration: Duration) -> Option<gtk::Label> {
let text = match (*distance > si::M, *duration > si::S) {
(true, true) => Some(format!(
"{} kilometers of biking in {} minutes",
distance.value_unsafe / 1000.,
duration.value_unsafe / 60.
"{} of biking in {}",
distance.format(FormatOption::Full),
duration.format(FormatOption::Full)
)),
(true, false) => Some(format!(
"{} kilometers of biking",
distance.value_unsafe / 1000.
)),
(false, true) => Some(format!("{} seconds of biking", duration.value_unsafe / 60.)),
(true, false) => Some(format!("{} of biking", distance.format(FormatOption::Full))),
(false, true) => Some(format!("{} of biking", duration.format(FormatOption::Full))),
(false, false) => None,
};
@ -72,7 +69,7 @@ pub fn time_distance_detail(type_: ft_core::RecordType, record: ft_core::TimeDis
.label(
record
.distance
.map(|dist| format!("{}", dist))
.map(|dist| Distance::from(dist).format(FormatOption::Abbreviated))
.unwrap_or("".to_owned()),
)
.build(),
@ -84,7 +81,7 @@ pub fn time_distance_detail(type_: ft_core::RecordType, record: ft_core::TimeDis
.label(
record
.duration
.map(|duration| format!("{}", duration))
.map(|duration| Duration::from(duration).format(FormatOption::Abbreviated))
.unwrap_or("".to_owned()),
)
.build(),
@ -173,16 +170,16 @@ impl TimeDistanceEdit {
.widget(),
);
details_row.append(
&distance_field(workout.distance, {
&distance_field(workout.distance.map(Distance::from), {
let s = s.clone();
move |d| s.update_distance(d)
move |d| s.update_distance(d.map(|d| *d))
})
.widget(),
);
details_row.append(
&duration_field(workout.duration, {
&duration_field(workout.duration.map(Duration::from), {
let s = s.clone();
move |d| s.update_duration(d)
move |d| s.update_duration(d.map(|d| *d))
})
.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
// 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 {
fn default() -> Self {
Self {
start: (Local::now() - Duration::days(7)).date_naive(),
start: (Local::now() - chrono::Duration::days(7)).date_naive(),
end: Local::now().date_naive(),
}
}
@ -38,10 +42,132 @@ impl Iterator for DayIterator {
fn next(&mut self) -> Option<Self::Item> {
if self.current <= self.end {
let val = self.current;
self.current += Duration::days(1);
self.current += chrono::Duration::days(1);
Some(val)
} else {
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/>.
*/
use crate::app::App;
use crate::{
app::App,
types::{Distance, Duration},
};
use dimensioned::si;
use emseries::{Record, RecordId, Recordable};
use ft_core::{RecordType, TimeDistance, TraxRecord};
@ -90,33 +93,26 @@ impl<T: Clone + emseries::Recordable> Deref for RecordState<T> {
}
}
#[derive(Default)]
struct DayDetailViewModelInner {}
#[derive(Clone, Default)]
#[derive(Clone)]
pub struct DayDetailViewModel {
app: Option<App>,
app: App,
pub date: chrono::NaiveDate,
weight: Arc<RwLock<Option<RecordState<ft_core::Weight>>>>,
steps: Arc<RwLock<Option<RecordState<ft_core::Steps>>>>,
records: Arc<RwLock<HashMap<RecordId, RecordState<TraxRecord>>>>,
original_records: Vec<Record<TraxRecord>>,
}
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 {
app: Some(app),
app,
date,
weight: Arc::new(RwLock::new(None)),
steps: Arc::new(RwLock::new(None)),
records: Arc::new(RwLock::new(HashMap::new())),
original_records: records,
};
s.populate_records();
s.populate_records().await;
s
}
@ -164,9 +160,9 @@ impl DayDetailViewModel {
*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(
(0. * si::M, 0. * si::S),
(Distance::default(), Duration::default()),
|(acc_distance, acc_duration), (_, record)| match record.data() {
Some(Record {
data:
@ -176,11 +172,11 @@ impl DayDetailViewModel {
..
}) => (
distance
.map(|distance| acc_distance + distance)
.map(|distance| acc_distance + Distance::from(distance))
.unwrap_or(acc_distance),
(duration
.map(|duration| acc_duration + duration)
.unwrap_or(acc_duration)),
duration
.map(|duration| acc_duration + Duration::from(duration))
.unwrap_or(acc_duration),
),
_ => (acc_distance, acc_duration),
@ -223,79 +219,81 @@ impl DayDetailViewModel {
.collect::<Vec<Record<TraxRecord>>>()
}
pub fn save(&self) {
pub fn save(&self) -> glib::JoinHandle<()> {
glib::spawn_future({
let s = self.clone();
async move {
if let Some(app) = s.app {
let weight_record = s.weight.read().unwrap().clone();
match weight_record {
Some(RecordState::New(Record { data, .. })) => {
let _ = app.put_record(TraxRecord::Weight(data)).await;
}
Some(RecordState::Original(_)) => {}
Some(RecordState::Updated(weight)) => {
let _ = app
.update_record(Record {
id: weight.id,
data: TraxRecord::Weight(weight.data),
})
.await;
}
Some(RecordState::Deleted(_)) => {}
None => {}
let weight_record = s.weight.read().unwrap().clone();
match weight_record {
Some(RecordState::New(Record { data, .. })) => {
let _ = s.app.put_record(TraxRecord::Weight(data)).await;
}
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 => {}
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 records = s
.records
.write()
.unwrap()
.drain()
.map(|(_, record)| record)
.collect::<Vec<RecordState<TraxRecord>>>();
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 => {}
}
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!(),
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(_) => {}
RecordState::Updated(r) => {
let _ = s.app.update_record(r.clone()).await;
}
RecordState::Deleted(_) => unimplemented!(),
}
}
s.populate_records().await;
}
});
})
}
pub fn revert(&self) {
self.populate_records();
}
fn populate_records(&self) {
let records = self.original_records.clone();
async fn populate_records(&self) {
let records = self.app.records(self.date, self.date).await.unwrap();
let (weight_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
records.into_iter().partition(|r| r.data.is_weight());

View File

@ -25,7 +25,7 @@ use std::cell::RefCell;
#[derive(Default)]
pub struct DayDetailViewPrivate {
container: Singleton,
view_model: RefCell<DayDetailViewModel>,
view_model: RefCell<Option<DayDetailViewModel>>,
}
#[glib::object_subclass]
@ -47,7 +47,7 @@ glib::wrapper! {
impl DayDetailView {
pub fn new(view_model: DayDetailViewModel) -> Self {
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);
@ -57,20 +57,28 @@ impl DayDetailView {
}
fn view(&self) {
self.imp()
.container
.swap(&DayDetail::new(self.imp().view_model.borrow().clone(), {
let s = self.clone();
move || s.edit()
}));
let view_model = self.imp().view_model.borrow();
let view_model = view_model
.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();
move || s.edit()
}));
}
fn edit(&self) {
self.imp()
.container
.swap(&DayEdit::new(self.imp().view_model.borrow().clone(), {
let s = self.clone();
move || s.view()
}));
let view_model = self.imp().view_model.borrow();
let view_model = view_model
.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();
move || s.view()
}));
}
}

View File

@ -60,12 +60,12 @@ impl ObjectSubclass for HistoricalViewPrivate {
factory.connect_bind({
let app = s.app.clone();
move |_, list_item| {
let records = list_item
let date = list_item
.downcast_ref::<gtk::ListItem>()
.expect("should be a ListItem")
.item()
.and_downcast::<DayRecords>()
.expect("should be a DaySummary");
.and_downcast::<Date>()
.expect("should be a Date");
let summary = list_item
.downcast_ref::<gtk::ListItem>()
@ -75,11 +75,14 @@ impl ObjectSubclass for HistoricalViewPrivate {
.expect("should be a DaySummary");
if let Some(app) = app.borrow().clone() {
summary.set_data(DayDetailViewModel::new(
records.date(),
records.records(),
app.clone(),
));
let _ = glib::spawn_future_local(async move {
println!(
"setting up a DayDetailViewModel for {}",
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 {
pub fn new<SelectFn>(
app: App,
records: Vec<Record<TraxRecord>>,
on_select_day: Rc<SelectFn>,
) -> Self
pub fn new<SelectFn>(app: App, interval: DayInterval, on_select_day: Rc<SelectFn>) -> Self
where
SelectFn: Fn(chrono::NaiveDate, Vec<Record<TraxRecord>>) + 'static,
{
@ -111,11 +110,8 @@ impl HistoricalView {
*s.imp().app.borrow_mut() = Some(app);
let grouped_records =
GroupedRecords::new((*s.imp().time_window.borrow()).clone()).with_data(records);
let mut model = gio::ListStore::new::<DayRecords>();
model.extend(grouped_records.items());
let mut model = gio::ListStore::new::<Date>();
model.extend(interval.days().map(|d| Date::new(d)));
s.imp()
.list_view
.set_model(Some(&gtk::NoSelection::new(Some(model))));
@ -125,8 +121,8 @@ impl HistoricalView {
move |s, idx| {
// This gets triggered whenever the user clicks on an item on the list.
let item = s.model().unwrap().item(idx).unwrap();
let records = item.downcast_ref::<DayRecords>().unwrap();
on_select_day(records.date(), records.records());
let date = item.downcast_ref::<Date>().unwrap();
on_select_day(date.date(), vec![]);
}
});
@ -135,101 +131,37 @@ impl HistoricalView {
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 {
self.imp().time_window.borrow().clone()
}
}
#[derive(Default)]
pub struct DayRecordsPrivate {
pub struct DatePrivate {
date: RefCell<chrono::NaiveDate>,
records: RefCell<Vec<Record<TraxRecord>>>,
}
#[glib::object_subclass]
impl ObjectSubclass for DayRecordsPrivate {
const NAME: &'static str = "DayRecords";
type Type = DayRecords;
impl ObjectSubclass for DatePrivate {
const NAME: &'static str = "Date";
type Type = Date;
}
impl ObjectImpl for DayRecordsPrivate {}
impl ObjectImpl for DatePrivate {}
glib::wrapper! {
pub struct DayRecords(ObjectSubclass<DayRecordsPrivate>);
pub struct Date(ObjectSubclass<DatePrivate>);
}
impl DayRecords {
pub fn new(date: chrono::NaiveDate, records: Vec<Record<TraxRecord>>) -> Self {
impl Date {
pub fn new(date: chrono::NaiveDate) -> Self {
let s: Self = Object::builder().build();
*s.imp().date.borrow_mut() = date;
*s.imp().records.borrow_mut() = records;
s
}
pub fn date(&self) -> chrono::NaiveDate {
*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![]))
})
self.imp().date.borrow().clone()
}
}