/* 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 crate::components::{EditView, ParseError, TextEntry}; use chrono::{Local, NaiveDate}; 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_put_record: PutRecordFn, on_update_record: UpdateRecordFn, ) -> Self where PutRecordFn: 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(date.clone(), Some(data.clone()), move |weight| { on_update_record(Record { id: id.clone(), data: TraxRecord::Weight(Weight { date, weight }), }) }), None => WeightView::new(date.clone(), None, move |weight| { on_put_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 { date: RefCell, record: RefCell>, widget: RefCell>>>, on_edit_finished: RefCell)>>, } impl Default for WeightViewPrivate { fn default() -> Self { Self { date: RefCell::new(Local::now().date_naive()), record: RefCell::new(None), widget: RefCell::new(EditView::Unconfigured), 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( date: NaiveDate, 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().date.borrow_mut() = date; *s.imp().record.borrow_mut() = weight; s.view(); s } fn view(&self) { let view = gtk::Label::builder() .css_classes(["card", "weight-view"]) .halign(gtk::Align::Start) .can_focus(true) .build(); let view_click_controller = gtk::GestureClick::new(); view_click_controller.connect_released({ let s = self.clone(); move |_, _, _, _| { s.edit(); } }); view.add_controller(view_click_controller); 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(EditView::View(view)); } fn edit(&self) { let edit = TextEntry::>::new( "weight", None, |val: &si::Kilogram| val.to_string(), |v: &str| v.parse::().map(|w| w * si::KG).map_err(|_| ParseError), ); match *self.imp().record.borrow() { Some(ref record) => edit.set_value(Some(record.weight)), None => edit.set_value(None), } self.swap(EditView::Edit(edit.clone())); edit.grab_focus(); } fn swap(&self, new_view: EditView>>) { let mut widget = self.imp().widget.borrow_mut(); match *widget { EditView::Unconfigured => {} EditView::View(ref view) => self.remove(view), EditView::Edit(ref editor) => self.remove(&editor.widget()), } match new_view { EditView::Unconfigured => {} EditView::View(ref view) => self.append(view), EditView::Edit(ref editor) => self.append(&editor.widget()), } *widget = new_view; } fn blur(&self) { match *self.imp().widget.borrow() { EditView::Unconfigured => {} EditView::View(_) => {} EditView::Edit(ref editor) => { let weight = editor.value(); // This has really turned into rubbish // on_edit_finished needs to accept a full record now. // needs to be possible to delete a record if the value is None // it's hard to be sure whether I need the full record object or if I need to update // it. I probably don't. I think I need to borrow it and call on_edit_finished with an // updated version of it. // on_edit_finished still doesn't have a way to support a delete operation let record = match (self.imp().record.borrow().clone(), weight) { // update an existing record (Some(record), Some(weight)) => Some(Weight { date: record.date, weight, }), // create a new record (None, Some(weight)) => Some(Weight { date: self.imp().date.borrow().clone(), weight, }), // do nothing or delete an existing record (_, None) => None, }; match record { Some(record) => { self.imp().on_edit_finished.borrow()(record.weight); *self.imp().record.borrow_mut() = Some(record); } None => {} } } } 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 } }