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.
403 lines
12 KiB
Rust
403 lines
12 KiB
Rust
/*
|
|
Copyright 2023-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 chrono::NaiveDate;
|
|
// use ft_core::TraxRecord;
|
|
use crate::{
|
|
components::{
|
|
steps_editor, time_distance_summary, weight_field, ActionGroup, Steps, WeightLabel,
|
|
},
|
|
types::{DistanceFormatter, DurationFormatter, WeightFormatter},
|
|
view_models::DayDetailViewModel,
|
|
};
|
|
use emseries::{Record, RecordId};
|
|
use ft_core::{TimeDistanceActivity, TraxRecord};
|
|
use glib::Object;
|
|
use gtk::{prelude::*, subclass::prelude::*};
|
|
use std::{cell::RefCell, rc::Rc};
|
|
|
|
use super::{time_distance::TimeDistanceEdit, time_distance_detail};
|
|
|
|
pub struct DaySummaryPrivate {
|
|
date: gtk::Label,
|
|
}
|
|
|
|
#[glib::object_subclass]
|
|
impl ObjectSubclass for DaySummaryPrivate {
|
|
const NAME: &'static str = "DaySummary";
|
|
type Type = DaySummary;
|
|
type ParentType = gtk::Box;
|
|
|
|
fn new() -> Self {
|
|
let date = gtk::Label::builder()
|
|
.css_classes(["day-summary__date"])
|
|
.halign(gtk::Align::Start)
|
|
.build();
|
|
Self { date }
|
|
}
|
|
}
|
|
|
|
impl ObjectImpl for DaySummaryPrivate {}
|
|
impl WidgetImpl for DaySummaryPrivate {}
|
|
impl BoxImpl for DaySummaryPrivate {}
|
|
|
|
glib::wrapper! {
|
|
/// The DaySummary displays one day's activities in a narrative style. This is meant to give
|
|
/// an overall feel of everything that happened during the day without going into details.
|
|
pub struct DaySummary(ObjectSubclass<DaySummaryPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
|
|
}
|
|
|
|
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"]);
|
|
|
|
s.append(&s.imp().date);
|
|
|
|
s
|
|
}
|
|
}
|
|
|
|
impl DaySummary {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn set_data(&self, view_model: DayDetailViewModel) {
|
|
self.imp()
|
|
.date
|
|
.set_text(&view_model.date.format("%Y-%m-%d").to_string());
|
|
|
|
let row = gtk::Box::builder().build();
|
|
|
|
let label = gtk::Label::builder()
|
|
.halign(gtk::Align::Start)
|
|
.css_classes(["day-summary__weight"])
|
|
.build();
|
|
if let Some(w) = view_model.weight() {
|
|
label.set_label(&w.to_string())
|
|
}
|
|
row.append(&label);
|
|
|
|
self.append(&label);
|
|
|
|
let label = gtk::Label::builder()
|
|
.halign(gtk::Align::Start)
|
|
.css_classes(["day-summary__weight"])
|
|
.build();
|
|
if let Some(s) = view_model.steps() {
|
|
label.set_label(&format!("{} steps", s));
|
|
}
|
|
row.append(&label);
|
|
self.append(&row);
|
|
|
|
let biking_summary = view_model.time_distance_summary(TimeDistanceActivity::BikeRide);
|
|
if let Some(label) = time_distance_summary(
|
|
DistanceFormatter::from(biking_summary.0),
|
|
DurationFormatter::from(biking_summary.1),
|
|
) {
|
|
self.append(&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;
|
|
}
|
|
|
|
impl ObjectImpl for DayDetailPrivate {}
|
|
impl WidgetImpl for DayDetailPrivate {}
|
|
impl BoxImpl for DayDetailPrivate {}
|
|
|
|
glib::wrapper! {
|
|
pub struct DayDetail(ObjectSubclass<DayDetailPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
|
|
}
|
|
|
|
impl DayDetail {
|
|
pub fn new<OnEdit>(view_model: DayDetailViewModel, on_edit: OnEdit) -> Self
|
|
where
|
|
OnEdit: Fn() + 'static,
|
|
{
|
|
let s: Self = Object::builder().build();
|
|
s.set_orientation(gtk::Orientation::Vertical);
|
|
s.set_hexpand(true);
|
|
|
|
s.append(
|
|
&ActionGroup::builder()
|
|
.primary_action("Edit", Box::new(on_edit))
|
|
.build(),
|
|
);
|
|
|
|
let top_row = gtk::Box::builder()
|
|
.orientation(gtk::Orientation::Horizontal)
|
|
.build();
|
|
let weight_view = WeightLabel::new(view_model.weight().map(WeightFormatter::from));
|
|
top_row.append(&weight_view.widget());
|
|
|
|
let steps_view = Steps::new(view_model.steps());
|
|
top_row.append(&steps_view.widget());
|
|
|
|
s.append(&top_row);
|
|
|
|
let records = view_model.time_distance_records();
|
|
for emseries::Record { data, .. } in records {
|
|
s.append(&time_distance_detail(data));
|
|
}
|
|
|
|
s
|
|
}
|
|
}
|
|
|
|
pub struct DayEditPrivate {
|
|
on_finished: RefCell<Box<dyn Fn()>>,
|
|
#[allow(unused)]
|
|
workout_rows: RefCell<gtk::Box>,
|
|
view_model: RefCell<Option<DayDetailViewModel>>,
|
|
}
|
|
|
|
impl Default for DayEditPrivate {
|
|
fn default() -> Self {
|
|
Self {
|
|
on_finished: RefCell::new(Box::new(|| {})),
|
|
workout_rows: RefCell::new(
|
|
gtk::Box::builder()
|
|
.orientation(gtk::Orientation::Vertical)
|
|
.hexpand(true)
|
|
.build(),
|
|
),
|
|
view_model: RefCell::new(None),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[glib::object_subclass]
|
|
impl ObjectSubclass for DayEditPrivate {
|
|
const NAME: &'static str = "DayEdit";
|
|
type Type = DayEdit;
|
|
type ParentType = gtk::Box;
|
|
}
|
|
|
|
impl ObjectImpl for DayEditPrivate {}
|
|
impl WidgetImpl for DayEditPrivate {}
|
|
impl BoxImpl for DayEditPrivate {}
|
|
|
|
glib::wrapper! {
|
|
pub struct DayEdit(ObjectSubclass<DayEditPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
|
|
}
|
|
|
|
impl DayEdit {
|
|
pub fn new<OnFinished>(view_model: DayDetailViewModel, on_finished: OnFinished) -> Self
|
|
where
|
|
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.imp().view_model.borrow_mut() = Some(view_model.clone());
|
|
|
|
let workout_buttons = workout_buttons(view_model.clone(), {
|
|
let s = s.clone();
|
|
move |workout| s.add_row(workout)
|
|
});
|
|
|
|
view_model
|
|
.records()
|
|
.into_iter()
|
|
.filter_map({
|
|
let s = s.clone();
|
|
move |record| match record.data {
|
|
TraxRecord::TimeDistance(workout) => Some(TimeDistanceEdit::new(workout, {
|
|
let s = s.clone();
|
|
move |data| {
|
|
s.update_workout(record.id, data);
|
|
}
|
|
})),
|
|
_ => None,
|
|
}
|
|
})
|
|
.for_each(|row| s.imp().workout_rows.borrow().append(&row));
|
|
|
|
s.append(&control_buttons(&s, &view_model));
|
|
s.append(&weight_and_steps_row(&view_model));
|
|
s.append(&*s.imp().workout_rows.borrow());
|
|
s.append(&workout_buttons);
|
|
|
|
s
|
|
}
|
|
|
|
fn finish(&self) {
|
|
glib::spawn_future_local({
|
|
let s = self.clone();
|
|
async move {
|
|
let view_model = {
|
|
let view_model = s.imp().view_model.borrow();
|
|
view_model
|
|
.as_ref()
|
|
.expect("DayEdit has not been initialized with the view model")
|
|
.clone()
|
|
};
|
|
let _ = view_model.async_save().await;
|
|
(s.imp().on_finished.borrow())()
|
|
}
|
|
});
|
|
}
|
|
|
|
fn add_row(&self, workout: Record<TraxRecord>) {
|
|
println!("adding a row for {:?}", workout);
|
|
let workout_rows = self.imp().workout_rows.borrow();
|
|
|
|
#[allow(clippy::single_match)]
|
|
match workout.data {
|
|
TraxRecord::TimeDistance(r) => workout_rows.append(&TimeDistanceEdit::new(r, {
|
|
let s = self.clone();
|
|
move |data| {
|
|
println!("update workout callback on workout: {:?}", workout.id);
|
|
s.update_workout(workout.id, data)
|
|
}
|
|
})),
|
|
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn update_workout(&self, id: RecordId, data: ft_core::TimeDistance) {
|
|
if let Some(ref view_model) = *self.imp().view_model.borrow() {
|
|
let record = Record {
|
|
id,
|
|
data: TraxRecord::TimeDistance(data),
|
|
};
|
|
view_model.update_record(record);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn control_buttons(s: &DayEdit, view_model: &DayDetailViewModel) -> ActionGroup {
|
|
ActionGroup::builder()
|
|
.primary_action("Save", {
|
|
let s = s.clone();
|
|
move || s.finish()
|
|
})
|
|
.secondary_action("Cancel", {
|
|
let s = s.clone();
|
|
let view_model = view_model.clone();
|
|
move || {
|
|
let s = s.clone();
|
|
let view_model = view_model.clone();
|
|
glib::spawn_future_local(async move {
|
|
view_model.revert().await;
|
|
s.finish();
|
|
});
|
|
}
|
|
})
|
|
.build()
|
|
}
|
|
|
|
fn weight_and_steps_row(view_model: &DayDetailViewModel) -> gtk::Box {
|
|
let row = gtk::Box::builder()
|
|
.orientation(gtk::Orientation::Horizontal)
|
|
.build();
|
|
row.append(
|
|
&weight_field(view_model.weight().map(WeightFormatter::from), {
|
|
let view_model = view_model.clone();
|
|
move |w| match w {
|
|
Some(w) => view_model.set_weight(*w),
|
|
None => eprintln!("have not implemented record delete"),
|
|
}
|
|
})
|
|
.widget(),
|
|
);
|
|
|
|
row.append(
|
|
&steps_editor(view_model.steps(), {
|
|
let view_model = view_model.clone();
|
|
move |s| match s {
|
|
Some(s) => view_model.set_steps(s),
|
|
None => eprintln!("have not implemented record delete"),
|
|
}
|
|
})
|
|
.widget(),
|
|
);
|
|
|
|
row
|
|
}
|
|
|
|
fn workout_buttons<AddRow>(view_model: DayDetailViewModel, add_row: AddRow) -> gtk::Box
|
|
where
|
|
AddRow: Fn(Record<TraxRecord>) + 'static,
|
|
{
|
|
let add_row = Rc::new(add_row);
|
|
/*
|
|
let walking_button = gtk::Button::builder()
|
|
.icon_name("walking2-symbolic")
|
|
.width_request(64)
|
|
.height_request(64)
|
|
.build();
|
|
walking_button.connect_clicked({
|
|
let view_model = view_model.clone();
|
|
let add_row = add_row.clone();
|
|
move |_| {
|
|
let workout = view_model.new_time_distance(TimeDistanceActivity::Walking);
|
|
add_row(workout.map(TraxRecord::TimeDistance));
|
|
}
|
|
});
|
|
|
|
let running_button = gtk::Button::builder()
|
|
.icon_name("running-symbolic")
|
|
.width_request(64)
|
|
.height_request(64)
|
|
.build();
|
|
running_button.connect_clicked({
|
|
let view_model = view_model.clone();
|
|
move |_| {
|
|
let workout = view_model.new_time_distance(TimeDistanceActivity::Running);
|
|
add_row(workout.map(TraxRecord::TimeDistance));
|
|
}
|
|
});
|
|
*/
|
|
|
|
let biking_button = gtk::Button::builder()
|
|
.icon_name("cycling-symbolic")
|
|
.width_request(64)
|
|
.height_request(64)
|
|
.build();
|
|
biking_button.connect_clicked({
|
|
let view_model = view_model.clone();
|
|
move |_| {
|
|
let workout = view_model.new_time_distance(TimeDistanceActivity::BikeRide);
|
|
add_row(workout.map(TraxRecord::TimeDistance));
|
|
}
|
|
});
|
|
|
|
let layout = gtk::Box::builder()
|
|
.orientation(gtk::Orientation::Vertical)
|
|
.build();
|
|
let row = gtk::Box::builder()
|
|
.orientation(gtk::Orientation::Horizontal)
|
|
.build();
|
|
row.append(&biking_button);
|
|
layout.append(&row);
|
|
|
|
layout
|
|
}
|