/* Copyright 2023, 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 dimensioned::si; use emseries::Record; use ft_core::{RecordType, TimeDistance, TraxRecord, Weight}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; use std::{cell::RefCell, rc::Rc}; pub struct DaySummaryPrivate { date: gtk::Label, weight: RefCell>, } #[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, weight: RefCell::new(None), } } } 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 DaySummary { pub fn new() -> 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 } pub fn set_data(&self, date: chrono::NaiveDate, records: Vec>) { self.imp() .date .set_text(&date.format("%Y-%m-%d").to_string()); if let Some(ref weight_label) = *self.imp().weight.borrow() { self.remove(weight_label); } if let Some(Record { data: TraxRecord::Weight(weight_record), .. }) = records.iter().filter(|f| f.data.is_weight()).next() { let label = gtk::Label::builder() .halign(gtk::Align::Start) .label(&format!("{}", weight_record.weight)) .css_classes(["day-summary__weight"]) .build(); self.append(&label); *self.imp().weight.borrow_mut() = Some(label); } /* self.append( >k::Label::builder() .halign(gtk::Align::Start) .label("15km of biking in 60 minutes") .build(), ); */ } } pub struct DayDetailPrivate { date: gtk::Label, weight: RefCell>, } #[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 {} 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( date: chrono::NaiveDate, records: Vec>, on_save_record: SaveRecordFn, on_update_record: UpdateRecordFn, ) -> Self where SaveRecordFn: Fn(TraxRecord) + 'static, UpdateRecordFn: Fn(Record) + 'static, { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); 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: TraxRecord::Weight(record), } => Some((id.clone(), record.clone())), _ => None, }); let weight_view = match weight_record { Some((id, data)) => WeightView::new(Some(data.clone()), move |weight| { on_update_record(Record { id: id.clone(), data: TraxRecord::Weight(Weight { date, weight }), }) }), None => WeightView::new(None, move |weight| { on_save_record(TraxRecord::Weight(Weight { date, weight })); }), }; s.append(&weight_view); records.into_iter().for_each(|record| { let record_view = match record { Record { data: TraxRecord::BikeRide(record), .. } => Some( TimeDistanceView::new(RecordType::BikeRide, record).upcast::(), ), Record { data: TraxRecord::Row(record), .. } => Some(TimeDistanceView::new(RecordType::Row, record).upcast::()), Record { data: TraxRecord::Run(record), .. } => Some(TimeDistanceView::new(RecordType::Row, record).upcast::()), Record { data: TraxRecord::Swim(record), .. } => Some(TimeDistanceView::new(RecordType::Row, record).upcast::()), Record { data: TraxRecord::Walk(record), .. } => Some(TimeDistanceView::new(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 WeightViewPrivate { record: RefCell>, view: RefCell, edit: RefCell, current: RefCell, on_edit_finished: RefCell)>>, } impl Default for WeightViewPrivate { fn default() -> Self { let view = gtk::Label::builder() .css_classes(["card", "weight-view"]) .halign(gtk::Align::Start) .can_focus(true) .build(); let edit = gtk::Entry::builder().halign(gtk::Align::Start).build(); let current = view.clone(); Self { record: RefCell::new(None), view: RefCell::new(view), edit: RefCell::new(edit), current: RefCell::new(current.upcast()), on_edit_finished: RefCell::new(Box::new(|_| {})), } } } #[glib::object_subclass] impl ObjectSubclass for WeightViewPrivate { const NAME: &'static str = "WeightView"; type Type = WeightView; type ParentType = gtk::Box; } impl ObjectImpl for WeightViewPrivate {} impl WidgetImpl for WeightViewPrivate {} impl BoxImpl for WeightViewPrivate {} glib::wrapper! { pub struct WeightView(ObjectSubclass) @extends gtk::Box, gtk::Widget; } impl WeightView { pub fn new(weight: Option, on_edit_finished: OnEditFinished) -> Self where OnEditFinished: Fn(si::Kilogram) + 'static, { let s: Self = Object::builder().build(); *s.imp().on_edit_finished.borrow_mut() = Box::new(on_edit_finished); *s.imp().record.borrow_mut() = weight; s.view(); let view_click_controller = gtk::GestureClick::new(); view_click_controller.connect_released({ let s = s.clone(); move |_, _, _, _| { s.edit(); } }); s.imp().view.borrow().add_controller(view_click_controller); s } fn view(&self) { let view = self.imp().view.borrow(); match *self.imp().record.borrow() { Some(ref record) => { view.remove_css_class("dim_label"); view.set_label(&format!("{:?}", record.weight)); } None => { view.add_css_class("dim_label"); view.set_label("No weight recorded"); } } self.swap(view.clone().upcast()); } fn edit(&self) { let edit = self.imp().edit.borrow(); match *self.imp().record.borrow() { Some(ref record) => edit.buffer().set_text(&format!("{:?}", record.weight)), None => edit.buffer().set_text(""), } self.swap(edit.clone().upcast()); edit.grab_focus(); } fn swap(&self, new_view: gtk::Widget) { let mut current = self.imp().current.borrow_mut(); self.remove(&*current); self.append(&new_view); *current = new_view; } fn blur(&self) { if *self.imp().current.borrow() == *self.imp().edit.borrow() { self.imp().on_edit_finished.borrow()(0. * si::KG); self.view(); } } } #[derive(Default)] pub struct TimeDistanceViewPrivate { record: RefCell>, } #[glib::object_subclass] impl ObjectSubclass for TimeDistanceViewPrivate { const NAME: &'static str = "TimeDistanceView"; type Type = TimeDistanceView; type ParentType = gtk::Box; } impl ObjectImpl for TimeDistanceViewPrivate {} impl WidgetImpl for TimeDistanceViewPrivate {} impl BoxImpl for TimeDistanceViewPrivate {} glib::wrapper! { pub struct TimeDistanceView(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; } impl TimeDistanceView { pub fn new(type_: RecordType, record: TimeDistance) -> Self { let s: Self = Object::builder().build(); s.set_orientation(gtk::Orientation::Vertical); s.set_hexpand(true); let first_row = gtk::Box::builder().homogeneous(true).build(); first_row.append( >k::Label::builder() .halign(gtk::Align::Start) .label(&record.datetime.format("%H:%M").to_string()) .build(), ); first_row.append( >k::Label::builder() .halign(gtk::Align::Start) .label(format!("{:?}", type_)) .build(), ); first_row.append( >k::Label::builder() .halign(gtk::Align::Start) .label( record .distance .map(|dist| format!("{}", dist)) .unwrap_or("".to_owned()), ) .build(), ); first_row.append( >k::Label::builder() .halign(gtk::Align::Start) .label( record .duration .map(|duration| format!("{}", duration)) .unwrap_or("".to_owned()), ) .build(), ); s.append(&first_row); s.append( >k::Label::builder() .halign(gtk::Align::Start) .label( record .comments .map(|comments| format!("{}", comments)) .unwrap_or("".to_owned()), ) .build(), ); s } }