/* Copyright 2023-2024, Savanni D'Gerinel 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 . */ // 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) @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) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; } impl DayDetail { pub fn new(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 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::() { 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(); 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>, #[allow(unused)] workout_rows: RefCell, view_model: RefCell>, } 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) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; } impl DayEdit { pub fn new(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) { (self.imp().on_finished.borrow())() } fn add_row(&self, workout: Record) { 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(); let view_model = view_model.clone(); move || { view_model.save(); 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(view_model: DayDetailViewModel, add_row: AddRow) -> gtk::Box where AddRow: Fn(Record) + '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 }