318 lines
11 KiB
Rust
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
|
|
}
|
|
}
|
|
*/
|