/* 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, weight_field, ActionGroup, Steps, WeightLabel}, types::WeightFormatter, view_models::DayDetailViewModel, }; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::cell::RefCell; 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); } } #[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); /* records.into_iter().for_each(|record| { let record_view = match record { Record { data: ft_core::TraxRecord::BikeRide(record), .. } => Some( TimeDistanceView::new(ft_core::RecordType::BikeRide, record) .upcast::(), ), Record { data: ft_core::TraxRecord::Row(record), .. } => Some( TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::(), ), Record { data: ft_core::TraxRecord::Run(record), .. } => Some( TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::(), ), Record { data: ft_core::TraxRecord::Swim(record), .. } => Some( TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::(), ), Record { data: ft_core::TraxRecord::Walk(record), .. } => Some( TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::(), ), _ => None, }; if let Some(record_view) = record_view { record_view.add_css_class("day-detail"); record_view.set_halign(gtk::Align::Start); s.append(&record_view); } }); */ 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()); s.append( &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 || { view_model.revert(); s.finish(); } }) .build(), ); let top_row = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .build(); top_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(), ); top_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(), ); s.append(&top_row); s } fn finish(&self) { (self.imp().on_finished.borrow())() } }