Compare commits

...

3 Commits

Author SHA1 Message Date
Savanni D'Gerinel 9bedb7a76c Tons of linting and get tests running again 2024-01-20 15:04:46 -05:00
Savanni D'Gerinel 1fe318068b Set up a view model for the day detail view 2024-01-20 11:16:31 -05:00
Savanni D'Gerinel 18e7e4fe2f Start setting up the day detail view model
I've created the view model and added a getter function for the weight.
I'm passing the view model now to the DayDetailView, DayDetail, and
DayEdit.

I'm starting to set up the Save function for the view model, draining
all of the updated records and saving them.

None of the components yet save any updates to the view model, so
updated_records is always going to be empty until I figure that out.
2024-01-18 09:00:08 -05:00
21 changed files with 401 additions and 302 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"rust-analyzer.showUnlinkedFileNotification": false
}

View File

@ -108,7 +108,7 @@ where
Ok(line_) => {
match serde_json::from_str::<RecordOnDisk<T>>(line_.as_ref())
.map_err(EmseriesReadError::JSONParseError)
.and_then(|record| Record::try_from(record))
.and_then(Record::try_from)
{
Ok(record) => records.insert(record.id.clone(), record.clone()),
Err(EmseriesReadError::RecordDeleted(id)) => records.remove(&id),

View File

@ -11,9 +11,6 @@ You should have received a copy of the GNU General Public License along with Lum
*/
use chrono::{DateTime, FixedOffset, NaiveDate};
use chrono_tz::UTC;
use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use std::{cmp::Ordering, fmt, io, str};
use thiserror::Error;
use uuid::Uuid;
@ -93,33 +90,9 @@ impl str::FromStr for Timestamp {
}
}
/*
impl PartialEq for Timestamp {
fn eq(&self, other: &Timestamp) -> bool {
match (self, other) {
(Timestamp::DateTime(dt1), Timestamp::DateTime(dt2)) => {
dt1.with_timezone(&UTC) == dt2.with_timezone(&UTC)
}
// It's not clear to me what would make sense when I'm comparing a date and a
// timestamp. I'm going with a naive date comparison on the idea that what I'm wanting
// here is human scale, again.
(Timestamp::DateTime(dt1), Timestamp::Date(dt2)) => dt1.date_naive() == *dt2,
(Timestamp::Date(dt1), Timestamp::DateTime(dt2)) => *dt1 == dt2.date_naive(),
(Timestamp::Date(dt1), Timestamp::Date(dt2)) => *dt1 == *dt2,
}
}
}
*/
impl PartialOrd for Timestamp {
fn partial_cmp(&self, other: &Timestamp) -> Option<Ordering> {
// Some(self.cmp(other))
match (self, other) {
(Timestamp::DateTime(dt1), Timestamp::DateTime(dt2)) => dt1.partial_cmp(dt2),
(Timestamp::DateTime(dt1), Timestamp::Date(dt2)) => dt1.date_naive().partial_cmp(dt2),
(Timestamp::Date(dt1), Timestamp::DateTime(dt2)) => dt1.partial_cmp(&dt2.date_naive()),
(Timestamp::Date(dt1), Timestamp::Date(dt2)) => dt1.partial_cmp(dt2),
}
Some(self.cmp(other))
}
}

View File

@ -14,7 +14,6 @@ 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::DayInterval;
use chrono::NaiveDate;
use emseries::{time_range, Record, RecordId, Series, Timestamp};
use ft_core::TraxRecord;
@ -52,12 +51,10 @@ impl App {
.unwrap(),
);
let s = Self {
Self {
runtime,
database: Arc::new(RwLock::new(database)),
};
s
}
}
pub async fn records(
@ -76,7 +73,7 @@ impl App {
Timestamp::Date(end),
true,
))
.map(|record| record.clone())
.cloned()
.collect::<Vec<Record<TraxRecord>>>();
Ok(records)
} else {

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,
view_models::DayDetailViewModel,
views::{DayDetailView, HistoricalView, PlaceholderView, View, WelcomeView},
};
use adw::prelude::*;
@ -80,7 +81,7 @@ impl AppWindow {
.orientation(gtk::Orientation::Vertical)
.build();
let initial_view = View::Placeholder(PlaceholderView::new().upcast());
let initial_view = View::Placeholder(PlaceholderView::default().upcast());
layout.append(&initial_view.widget());
@ -114,9 +115,10 @@ impl AppWindow {
s.navigation.connect_popped({
let s = s.clone();
move |_, _| match *s.current_view.borrow() {
View::Historical(_) => s.load_records(),
_ => {}
move |_, _| {
if let View::Historical(_) = *s.current_view.borrow() {
s.load_records();
}
}
});
@ -137,7 +139,12 @@ impl AppWindow {
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(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)
@ -185,22 +192,4 @@ impl AppWindow {
}
});
}
fn on_put_record(&self, record: TraxRecord) {
glib::spawn_future_local({
let s = self.clone();
async move {
s.app.put_record(record).await;
}
});
}
fn on_update_record(&self, record: Record<TraxRecord>) {
glib::spawn_future_local({
let s = self.clone();
async move {
s.app.update_record(record).await;
}
});
}
}

View File

@ -14,6 +14,8 @@ 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/>.
*/
//! ActionGroup and related structures
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};

View File

@ -16,11 +16,14 @@ 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::{ActionGroup, TimeDistanceView, Weight};
use crate::{
components::{ActionGroup, Weight},
view_models::DayDetailViewModel,
};
use emseries::Record;
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use std::{cell::RefCell, rc::Rc};
use std::cell::RefCell;
use super::weight::WeightEdit;
@ -57,8 +60,8 @@ glib::wrapper! {
pub struct DaySummary(ObjectSubclass<DaySummaryPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
}
impl DaySummary {
pub fn new() -> Self {
impl Default for DaySummary {
fn default() -> Self {
let s: Self = Object::builder().build();
s.set_orientation(gtk::Orientation::Vertical);
s.set_css_classes(&["day-summary"]);
@ -67,6 +70,12 @@ impl DaySummary {
s
}
}
impl DaySummary {
pub fn new() -> Self {
Self::default()
}
pub fn set_data(&self, date: chrono::NaiveDate, records: Vec<Record<ft_core::TraxRecord>>) {
self.imp()
@ -80,11 +89,11 @@ impl DaySummary {
if let Some(Record {
data: ft_core::TraxRecord::Weight(weight_record),
..
}) = records.iter().filter(|f| f.data.is_weight()).next()
}) = records.iter().find(|f| f.data.is_weight())
{
let label = gtk::Label::builder()
.halign(gtk::Align::Start)
.label(&format!("{}", weight_record.weight))
.label(weight_record.weight.to_string())
.css_classes(["day-summary__weight"])
.build();
self.append(&label);
@ -102,27 +111,14 @@ impl DaySummary {
}
}
pub struct DayDetailPrivate {
date: gtk::Label,
weight: RefCell<Option<gtk::Label>>,
}
#[derive(Default)]
pub struct DayDetailPrivate {}
#[glib::object_subclass]
impl ObjectSubclass for DayDetailPrivate {
const NAME: &'static str = "DayDetail";
type Type = DayDetail;
type ParentType = gtk::Box;
fn new() -> Self {
let date = gtk::Label::builder()
.css_classes(["daysummary-date"])
.halign(gtk::Align::Start)
.build();
Self {
date,
weight: RefCell::new(None),
}
}
}
impl ObjectImpl for DayDetailPrivate {}
@ -134,11 +130,7 @@ glib::wrapper! {
}
impl DayDetail {
pub fn new<OnEdit>(
date: chrono::NaiveDate,
records: Vec<Record<ft_core::TraxRecord>>,
on_edit: OnEdit,
) -> Self
pub fn new<OnEdit>(view_model: DayDetailViewModel, on_edit: OnEdit) -> Self
where
OnEdit: Fn() + 'static,
{
@ -167,6 +159,7 @@ impl DayDetail {
s.add_controller(click_controller);
*/
/*
let weight_record = records.iter().find_map(|record| match record {
Record {
id,
@ -174,13 +167,12 @@ impl DayDetail {
} => Some((id.clone(), record.clone())),
_ => None,
});
*/
let weight_view = match weight_record {
Some((id, data)) => Weight::new(Some(data.clone())),
None => Weight::new(None),
};
let weight_view = Weight::new(view_model.weight());
s.append(&weight_view.widget());
/*
records.into_iter().for_each(|record| {
let record_view = match record {
Record {
@ -224,21 +216,20 @@ impl DayDetail {
s.append(&record_view);
}
});
*/
s
}
}
pub struct DayEditPrivate {
date: gtk::Label,
weight: Rc<WeightEdit>,
on_finished: RefCell<Box<dyn Fn()>>,
}
impl Default for DayEditPrivate {
fn default() -> Self {
Self {
date: gtk::Label::new(None),
weight: Rc::new(WeightEdit::new(None)),
on_finished: RefCell::new(Box::new(|| {})),
}
}
}
@ -259,75 +250,51 @@ glib::wrapper! {
}
impl DayEdit {
pub fn new<PutRecordFn, UpdateRecordFn, CancelFn>(
date: chrono::NaiveDate,
records: Vec<Record<ft_core::TraxRecord>>,
on_put_record: PutRecordFn,
on_update_record: UpdateRecordFn,
on_cancel: CancelFn,
) -> Self
pub fn new<OnFinished>(view_model: DayDetailViewModel, on_finished: OnFinished) -> Self
where
PutRecordFn: Fn(ft_core::TraxRecord) + 'static,
UpdateRecordFn: Fn(Record<ft_core::TraxRecord>) + 'static,
CancelFn: Fn() + 'static,
OnFinished: Fn() + 'static,
{
let s: Self = Object::builder().build();
s.set_orientation(gtk::Orientation::Vertical);
s.set_hexpand(true);
*s.imp().on_finished.borrow_mut() = Box::new(on_finished);
s.append(
&ActionGroup::builder()
.primary_action("Save", {
let s = s.clone();
let records = records.clone();
let view_model = view_model.clone();
move || {
let weight_record = records.iter().find_map(|record| match record {
Record {
id,
data: ft_core::TraxRecord::Weight(w),
} => Some((id, w)),
_ => None,
});
let weight = s.imp().weight.value();
if let Some(weight) = weight {
match weight_record {
Some((id, _)) => on_update_record(Record {
id: id.clone(),
data: ft_core::TraxRecord::Weight(ft_core::Weight {
date,
weight,
}),
}),
None => {
on_put_record(ft_core::TraxRecord::Weight(ft_core::Weight {
date,
weight,
}))
}
}
};
view_model.save();
s.finish();
}
})
.secondary_action("Cancel", {
let s = s.clone();
let view_model = view_model.clone();
move || {
view_model.revert();
s.finish();
}
})
.secondary_action("Cancel", on_cancel)
.build(),
);
let weight_record = records.iter().find_map(|record| match record {
Record {
id,
data: ft_core::TraxRecord::Weight(record),
} => Some((id.clone(), record.clone())),
_ => None,
});
match weight_record {
Some((_id, data)) => s.imp().weight.set_value(Some(data.weight)),
None => s.imp().weight.set_value(None),
};
s.append(&s.imp().weight.widget());
s.append(
&WeightEdit::new(view_model.weight(), {
let view_model = view_model.clone();
move |w| {
view_model.set_weight(w);
}
})
.widget(),
);
s
}
fn finish(&self) {
(self.imp().on_finished.borrow())()
}
}

View File

@ -20,9 +20,6 @@ pub use action_group::ActionGroup;
mod day;
pub use day::{DayDetail, DayEdit, DaySummary};
mod edit_view;
pub use edit_view::EditView;
mod singleton;
pub use singleton::{Singleton, SingletonImpl};

View File

@ -17,14 +17,19 @@ You should have received a copy of the GNU General Public License along with Fit
use gtk::prelude::*;
use std::{cell::RefCell, rc::Rc};
#[derive(Clone, Debug)]
pub struct ParseError;
type Renderer<T> = dyn Fn(&T) -> String;
type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>;
#[derive(Clone)]
pub struct TextEntry<T: Clone + std::fmt::Debug> {
value: Rc<RefCell<Option<T>>>,
widget: gtk::Entry,
renderer: Rc<Box<dyn Fn(&T) -> String>>,
parser: Rc<Box<dyn Fn(&str) -> Result<T, ParseError>>>,
#[allow(unused)]
renderer: Rc<Renderer<T>>,
parser: Rc<Parser<T>>,
}
impl<T: Clone + std::fmt::Debug> std::fmt::Debug for TextEntry<T> {
@ -45,16 +50,15 @@ impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
V: Fn(&str) -> Result<T, ParseError> + 'static,
{
let widget = gtk::Entry::builder().placeholder_text(placeholder).build();
match value {
Some(ref v) => widget.set_text(&renderer(&v)),
None => {}
if let Some(ref v) = value {
widget.set_text(&renderer(v))
}
let s = Self {
value: Rc::new(RefCell::new(value)),
widget,
renderer: Rc::new(Box::new(renderer)),
parser: Rc::new(Box::new(parser)),
renderer: Rc::new(renderer),
parser: Rc::new(parser),
};
s.widget.buffer().connect_text_notify({
@ -83,19 +87,20 @@ impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
}
}
#[allow(unused)]
pub fn value(&self) -> Option<T> {
let v = self.value.borrow().clone();
self.value.borrow().clone()
}
pub fn set_value(&self, value: Option<T>) {
match value {
Some(ref v) => self.widget.set_text(&(self.renderer)(&v)),
None => {}
if let Some(ref v) = value {
self.widget.set_text(&(self.renderer)(v))
}
*self.value.borrow_mut() = value;
}
#[allow(unused)]
pub fn grab_focus(&self) {
self.widget.grab_focus();
}

View File

@ -24,6 +24,7 @@ use std::cell::RefCell;
#[derive(Default)]
pub struct TimeDistanceViewPrivate {
#[allow(unused)]
record: RefCell<Option<TimeDistance>>,
}
@ -53,7 +54,7 @@ impl TimeDistanceView {
first_row.append(
&gtk::Label::builder()
.halign(gtk::Align::Start)
.label(&record.datetime.format("%H:%M").to_string())
.label(record.datetime.format("%H:%M").to_string())
.build(),
);
@ -96,7 +97,7 @@ impl TimeDistanceView {
.label(
record
.comments
.map(|comments| format!("{}", comments))
.map(|comments| comments.to_string())
.unwrap_or("".to_owned()),
)
.build(),

View File

@ -14,12 +14,9 @@ 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::{EditView, ParseError, Singleton, TextEntry};
use chrono::{Local, NaiveDate};
use crate::components::{ParseError, TextEntry};
use dimensioned::si;
use glib::{object::ObjectRef, Object};
use gtk::{prelude::*, subclass::prelude::*};
use std::{borrow::Borrow, cell::RefCell};
use gtk::prelude::*;
#[derive(Default)]
pub struct WeightViewPrivate {}
@ -29,14 +26,14 @@ pub struct Weight {
}
impl Weight {
pub fn new(weight: Option<ft_core::Weight>) -> Self {
pub fn new(weight: Option<si::Kilogram<f64>>) -> Self {
let label = gtk::Label::builder()
.css_classes(["card", "weight-view"])
.can_focus(true)
.build();
match weight {
Some(w) => label.set_text(&format!("{:?}", w.weight)),
Some(w) => label.set_text(&format!("{:?}", w)),
None => label.set_text("No weight recorded"),
}
@ -54,17 +51,30 @@ pub struct WeightEdit {
}
impl WeightEdit {
pub fn new(weight: Option<ft_core::Weight>) -> Self {
pub fn new<OnUpdate>(weight: Option<si::Kilogram<f64>>, on_update: OnUpdate) -> Self
where
OnUpdate: Fn(si::Kilogram<f64>) + 'static,
{
Self {
entry: TextEntry::new(
"0 kg",
weight.map(|w| w.weight),
weight,
|val: &si::Kilogram<f64>| val.to_string(),
|v: &str| v.parse::<f64>().map(|w| w * si::KG).map_err(|_| ParseError),
move |v: &str| {
let new_weight = v.parse::<f64>().map(|w| w * si::KG).map_err(|_| ParseError);
match new_weight {
Ok(w) => {
on_update(w);
Ok(w)
}
Err(err) => Err(err),
}
},
),
}
}
#[allow(unused)]
pub fn set_value(&self, value: Option<si::Kilogram<f64>>) {
self.entry.set_value(value);
}

View File

@ -18,12 +18,12 @@ mod app;
mod app_window;
mod components;
mod types;
mod view_models;
mod views;
use adw::prelude::*;
use app_window::AppWindow;
use std::{env, path::PathBuf};
use types::DayInterval;
const APP_ID_DEV: &str = "com.luminescent-dreams.fitnesstrax.dev";
const APP_ID_PROD: &str = "com.luminescent-dreams.fitnesstrax";

View File

@ -21,8 +21,8 @@ impl Default for DayInterval {
impl DayInterval {
pub fn days(&self) -> impl Iterator<Item = NaiveDate> {
DayIterator {
current: self.start.clone(),
end: self.end.clone(),
current: self.start,
end: self.end,
}
}
}
@ -37,7 +37,7 @@ impl Iterator for DayIterator {
fn next(&mut self) -> Option<Self::Item> {
if self.current <= self.end {
let val = self.current.clone();
let val = self.current;
self.current += Duration::days(1);
Some(val)
} else {

View File

@ -0,0 +1,217 @@
/*
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::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 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>>>>,
}
impl DayDetailViewModel {
pub fn new(date: chrono::NaiveDate, records: Vec<Record<TraxRecord>>, app: App) -> Self {
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 {
app: Some(app),
date,
weight: Arc::new(RwLock::new(
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 })),
)),
steps: Arc::new(RwLock::new(
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 })),
)),
records: Arc::new(RwLock::new(
records
.into_iter()
.map(|r| (r.id.clone(), RecordState::Original(r)))
.collect::<HashMap<RecordId, RecordState<TraxRecord>>>(),
)),
}
}
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 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 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) {
unimplemented!();
}
}

View File

@ -1,5 +1,5 @@
/*
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of FitnessTrax.
@ -14,9 +14,5 @@ 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/>.
*/
#[derive(Clone)]
pub enum EditView<View, Edit> {
Unconfigured,
View(View),
Edit(Edit),
}
mod day_detail;
pub use day_detail::DayDetailViewModel;

View File

@ -15,21 +15,17 @@ You should have received a copy of the GNU General Public License along with Fit
*/
use crate::{
app::App,
components::{DayDetail, DayEdit, Singleton, SingletonImpl},
view_models::DayDetailViewModel,
};
use emseries::Record;
use ft_core::TraxRecord;
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use std::cell::RefCell;
#[derive(Default)]
pub struct DayDetailViewPrivate {
app: RefCell<Option<App>>,
container: Singleton,
date: RefCell<chrono::NaiveDate>,
records: RefCell<Vec<Record<TraxRecord>>>,
view_model: RefCell<DayDetailViewModel>,
}
#[glib::object_subclass]
@ -49,116 +45,32 @@ glib::wrapper! {
}
impl DayDetailView {
pub fn new(date: chrono::NaiveDate, records: Vec<Record<TraxRecord>>, app: App) -> Self {
pub fn new(view_model: DayDetailViewModel) -> Self {
let s: Self = Object::builder().build();
*s.imp().date.borrow_mut() = date;
*s.imp().records.borrow_mut() = records;
*s.imp().app.borrow_mut() = Some(app);
*s.imp().view_model.borrow_mut() = view_model;
s.append(&s.imp().container);
/*
s.imp()
.container
.swap(&DayDetail::new(date, records.clone(), {
let s = s.clone();
let records = records.clone();
move || {
s.imp().container.swap(&DayEdit::new(
date,
records,
s.on_put_record(),
// s.on_update_record(),
|_| {},
))
}
}));
*/
s.view();
s
}
fn view(&self) {
self.imp().container.swap(&DayDetail::new(
self.imp().date.borrow().clone(),
self.imp().records.borrow().clone(),
{
self.imp()
.container
.swap(&DayDetail::new(self.imp().view_model.borrow().clone(), {
let s = self.clone();
move || s.edit()
},
));
}));
}
fn edit(&self) {
self.imp().container.swap(&DayEdit::new(
self.imp().date.borrow().clone(),
self.imp().records.borrow().clone(),
self.on_put_record(),
self.on_update_record(),
{
self.imp()
.container
.swap(&DayEdit::new(self.imp().view_model.borrow().clone(), {
let s = self.clone();
move || s.view()
},
));
}
fn on_put_record(&self) -> Box<dyn Fn(ft_core::TraxRecord)> {
let s = self.clone();
let app = self.imp().app.clone();
Box::new(move |record| {
let s = s.clone();
let app = app.clone();
glib::spawn_future_local({
async move {
match &*app.borrow() {
Some(app) => {
let id = app
.put_record(record.clone())
.await
.expect("successful write");
s.imp()
.records
.borrow_mut()
.push(Record { id, data: record });
}
None => {}
}
s.view();
}
});
})
}
fn on_update_record(&self) -> Box<dyn Fn(Record<ft_core::TraxRecord>)> {
let s = self.clone();
let app = self.imp().app.clone();
Box::new(move |updated_record| {
let app = app.clone();
let mut records = s.imp().records.borrow_mut();
let idx = records.iter().position(|r| r.id == updated_record.id);
match idx {
Some(i) => records[i] = updated_record.clone(),
None => records.push(updated_record.clone()),
}
glib::spawn_future_local({
let s = s.clone();
async move {
match &*app.borrow() {
Some(app) => {
let _ = app.update_record(updated_record).await;
}
None => {
println!("no app!");
}
}
s.view();
}
});
})
}));
}
}

View File

@ -15,7 +15,7 @@ You should have received a copy of the GNU General Public License along with Fit
*/
use crate::{components::DaySummary, types::DayInterval};
use chrono::{Duration, Local, NaiveDate};
use chrono::NaiveDate;
use emseries::Record;
use ft_core::TraxRecord;
use glib::Object;
@ -161,7 +161,7 @@ impl DayRecords {
}
pub fn date(&self) -> chrono::NaiveDate {
self.imp().date.borrow().clone()
*self.imp().date.borrow()
}
pub fn records(&self) -> Vec<Record<TraxRecord>> {
@ -204,11 +204,11 @@ impl GroupedRecords {
self
}
fn items<'a>(&'a self) -> impl Iterator<Item = DayRecords> + 'a {
fn items(&self) -> impl Iterator<Item = DayRecords> + '_ {
self.interval.days().map(|date| {
self.data
.get(&date)
.map(|rec| rec.clone())
.cloned()
.unwrap_or(DayRecords::new(date, vec![]))
})
}
@ -217,6 +217,7 @@ impl GroupedRecords {
#[cfg(test)]
mod test {
use super::GroupedRecords;
use crate::types::DayInterval;
use chrono::{FixedOffset, NaiveDate, TimeZone};
use dimensioned::si::{KG, M, S};
use emseries::{Record, RecordId};
@ -272,7 +273,12 @@ mod test {
},
];
let groups = GroupedRecords::from(records).0;
let groups = GroupedRecords::new(DayInterval {
start: NaiveDate::from_ymd_opt(2023, 10, 14).unwrap(),
end: NaiveDate::from_ymd_opt(2023, 10, 14).unwrap(),
})
.with_data(records)
.data;
assert_eq!(groups.len(), 3);
}
}

View File

@ -38,8 +38,8 @@ glib::wrapper! {
pub struct PlaceholderView(ObjectSubclass<PlaceholderViewPrivate>) @extends gtk::Box, gtk::Widget;
}
impl PlaceholderView {
pub fn new() -> Self {
impl Default for PlaceholderView {
fn default() -> Self {
let s: Self = Object::builder().build();
s
}

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::{app::App, components::FileChooserRow};
use crate::components::FileChooserRow;
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use std::path::PathBuf;

View File

@ -1,23 +1,25 @@
use crate::types;
#[cfg(test)]
mod test {
#[test]
#[ignore]
fn read_a_legacy_set_rep_record() {
unimplemented!()
}
#[test]
#[ignore]
fn read_a_legacy_steps_record() {
unimplemented!()
}
#[test]
#[ignore]
fn read_a_legacy_time_distance_record() {
unimplemented!()
}
#[test]
#[ignore]
fn read_a_legacy_weight_record() {
unimplemented!()
}

View File

@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
/// SetRep represents workouts like pushups or situps, which involve doing a "set" of a number of
/// actions, resting, and then doing another set.
#[allow(dead_code)]
pub struct SetRep {
/// I assume that a set/rep workout is only done once in a day.
date: NaiveDate,
@ -22,6 +23,16 @@ pub struct Steps {
pub count: u32,
}
impl Recordable for Steps {
fn timestamp(&self) -> Timestamp {
Timestamp::Date(self.date)
}
fn tags(&self) -> Vec<String> {
vec![]
}
}
/// TimeDistance represents workouts characterized by a duration and a distance travelled. These
/// sorts of workouts can occur many times a day, depending on how one records things. I might
/// record a single 30-km workout if I go on a long-distanec ride. Or I might record multiple 5km
@ -54,6 +65,16 @@ pub struct Weight {
pub weight: si::Kilogram<f64>,
}
impl Recordable for Weight {
fn timestamp(&self) -> Timestamp {
Timestamp::Date(self.date)
}
fn tags(&self) -> Vec<String> {
vec![]
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum RecordType {
BikeRide,
@ -91,23 +112,24 @@ impl TraxRecord {
}
pub fn is_weight(&self) -> bool {
match self {
TraxRecord::Weight(_) => true,
_ => false,
matches!(self, TraxRecord::Weight(_))
}
pub fn is_steps(&self) -> bool {
matches!(self, TraxRecord::Steps(_))
}
}
impl Recordable for TraxRecord {
fn timestamp(&self) -> Timestamp {
match self {
TraxRecord::BikeRide(rec) => Timestamp::DateTime(rec.datetime.clone()),
TraxRecord::Row(rec) => Timestamp::DateTime(rec.datetime.clone()),
TraxRecord::Run(rec) => Timestamp::DateTime(rec.datetime.clone()),
TraxRecord::Steps(rec) => Timestamp::Date(rec.date),
TraxRecord::Swim(rec) => Timestamp::DateTime(rec.datetime.clone()),
TraxRecord::Walk(rec) => Timestamp::DateTime(rec.datetime.clone()),
TraxRecord::Weight(rec) => Timestamp::Date(rec.date),
TraxRecord::BikeRide(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Row(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Run(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Steps(rec) => rec.timestamp(),
TraxRecord::Swim(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Walk(rec) => Timestamp::DateTime(rec.datetime),
TraxRecord::Weight(rec) => rec.timestamp(),
}
}
@ -135,6 +157,6 @@ mod test {
let id = series.put(record.clone()).unwrap();
let record_ = series.get(&id).unwrap();
assert_eq!(record_, record);
assert_eq!(record_.data, record);
}
}