monorepo/fitnesstrax/app/src/view_models/day_detail.rs

318 lines
11 KiB
Rust

/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of FitnessTrax.
FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU
General Public License as published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
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 dimensioned::si;
use emseries::{Record, RecordId, Recordable};
use ft_core::{TimeDistance, TraxRecord};
use std::{
collections::HashMap,
ops::Deref,
sync::{Arc, RwLock},
};
#[derive(Clone, Debug)]
enum RecordState<T: Clone + Recordable> {
Original(Record<T>),
New(T),
Updated(Record<T>),
#[allow(unused)]
Deleted(Record<T>),
}
impl<T: Clone + emseries::Recordable> RecordState<T> {
#[allow(unused)]
fn id(&self) -> Option<&RecordId> {
match self {
RecordState::Original(ref r) => Some(&r.id),
RecordState::New(ref r) => None,
RecordState::Updated(ref r) => Some(&r.id),
RecordState::Deleted(ref r) => Some(&r.id),
}
}
fn data(&self) -> Option<&Record<T>> {
match self {
RecordState::Original(ref r) => Some(&r),
RecordState::New(ref r) => None,
RecordState::Updated(ref r) => Some(&r),
RecordState::Deleted(ref r) => Some(&r),
}
}
fn with_value(self, value: T) -> RecordState<T> {
match self {
RecordState::Original(r) => RecordState::Updated(Record { data: value, ..r }),
RecordState::New(_) => RecordState::New(value),
RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..r }),
RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..r }),
}
}
#[allow(unused)]
fn with_delete(self) -> Option<RecordState<T>> {
match self {
RecordState::Original(r) => Some(RecordState::Deleted(r)),
RecordState::New(r) => None,
RecordState::Updated(r) => Some(RecordState::Deleted(r)),
RecordState::Deleted(r) => Some(RecordState::Deleted(r)),
}
}
}
impl<T: Clone + emseries::Recordable> Deref for RecordState<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
RecordState::Original(ref r) => &r.data,
RecordState::New(ref r) => r,
RecordState::Updated(ref r) => &r.data,
RecordState::Deleted(ref r) => &r.data,
}
}
}
#[derive(Default)]
struct DayDetailViewModelInner {}
#[derive(Clone, Default)]
pub struct DayDetailViewModel {
app: Option<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 {
let s = Self {
app: Some(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
}
pub fn weight(&self) -> Option<si::Kilogram<f64>> {
(*self.weight.read().unwrap()).as_ref().map(|w| w.weight)
}
pub fn set_weight(&self, new_weight: si::Kilogram<f64>) {
let mut record = self.weight.write().unwrap();
let new_record = match *record {
Some(ref rstate) => rstate.clone().with_value(ft_core::Weight {
date: self.date,
weight: new_weight,
}),
None => RecordState::New(ft_core::Weight {
date: self.date,
weight: new_weight,
}),
};
*record = Some(new_record);
}
pub fn steps(&self) -> Option<u32> {
(*self.steps.read().unwrap()).as_ref().map(|w| w.count)
}
pub fn set_steps(&self, new_count: u32) {
let mut record = self.steps.write().unwrap();
let new_record = match *record {
Some(ref rstate) => rstate.clone().with_value(ft_core::Steps {
date: self.date,
count: new_count,
}),
None => RecordState::New(ft_core::Steps {
date: self.date,
count: new_count,
}),
};
*record = Some(new_record);
}
pub fn biking_summary(&self) -> (si::Meter<f64>, si::Second<f64>) {
self.records.read().unwrap().iter().fold(
(0. * si::M, 0. * si::S),
|(acc_distance, acc_duration), (_, record)| match record.data() {
Some(Record {
data:
TraxRecord::BikeRide(TimeDistance {
distance, duration, ..
}),
..
}) => (
distance
.map(|distance| acc_distance + distance)
.unwrap_or(acc_distance),
(duration
.map(|duration| acc_duration + duration)
.unwrap_or(acc_duration)),
),
_ => (acc_distance, acc_duration),
},
)
}
pub fn records(&self) -> Vec<Record<TraxRecord>> {
let read_lock = self.records.read().unwrap();
read_lock
.iter()
.map(|(_, record_state)| record_state.data())
.filter_map(|r| r)
.cloned()
.collect::<Vec<Record<TraxRecord>>>()
}
pub fn save(&self) {
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(weight)) => {
let _ = app.put_record(TraxRecord::Weight(weight)).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 steps_record = s.steps.read().unwrap().clone();
match steps_record {
Some(RecordState::New(steps)) => {
let _ = app.put_record(TraxRecord::Steps(steps)).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 {
match record {
RecordState::New(data) => {
let _ = app.put_record(data).await;
}
RecordState::Original(_) => {}
RecordState::Updated(r) => {
let _ = app.update_record(r.clone()).await;
}
RecordState::Deleted(_) => unimplemented!(),
}
}
}
}
});
}
pub fn revert(&self) {
self.populate_records();
}
fn populate_records(&self) {
let records = self.original_records.clone();
let (weight_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
records.into_iter().partition(|r| r.data.is_weight());
let (step_records, records): (Vec<Record<TraxRecord>>, Vec<Record<TraxRecord>>) =
records.into_iter().partition(|r| r.data.is_steps());
*self.weight.write().unwrap() = weight_records
.first()
.and_then(|r| match r.data {
TraxRecord::Weight(ref w) => Some((r.id.clone(), w.clone())),
_ => None,
})
.map(|(id, w)| RecordState::Original(Record { id, data: w }));
*self.steps.write().unwrap() = step_records
.first()
.and_then(|r| match r.data {
TraxRecord::Steps(ref w) => Some((r.id.clone(), w.clone())),
_ => None,
})
.map(|(id, w)| RecordState::Original(Record { id, data: w }));
*self.records.write().unwrap() = records
.into_iter()
.map(|r| (r.id.clone(), RecordState::Original(r)))
.collect::<HashMap<RecordId, RecordState<TraxRecord>>>();
}
}
/*
struct SavedRecordIterator<'a> {
read_lock: RwLockReadGuard<'a, HashMap<RecordId, RecordState<TraxRecord>>>,
iter: Box<dyn Iterator<Item = &'a Record<TraxRecord>> + 'a>,
}
impl<'a> SavedRecordIterator<'a> {
fn new(records: Arc<RwLock<HashMap<RecordId, RecordState<TraxRecord>>>>) -> Self {
let read_lock = records.read().unwrap();
let iter = read_lock
.iter()
.map(|(_, record_state)| record_state.data())
.filter_map(|r| r);
Self {
read_lock,
iter: Box::new(iter),
}
}
}
impl<'a> Iterator for SavedRecordIterator<'a> {
type Item = &'a Record<TraxRecord>;
fn next(&mut self) -> Option<Self::Item> {
None
}
}
*/