diff --git a/Cargo.lock b/Cargo.lock index a252307..c823cc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -699,7 +699,7 @@ dependencies = [ [[package]] name = "dashboard" -version = "0.1.1" +version = "0.1.2" dependencies = [ "cairo-rs", "chrono", @@ -1031,6 +1031,7 @@ dependencies = [ "glib-build-tools 0.18.0", "gtk4", "libadwaita", + "thiserror", "tokio", ] diff --git a/fitnesstrax/app/Cargo.toml b/fitnesstrax/app/Cargo.toml index 980cc44..2f58d54 100644 --- a/fitnesstrax/app/Cargo.toml +++ b/fitnesstrax/app/Cargo.toml @@ -16,6 +16,7 @@ ft-core = { path = "../core" } gio = { version = "0.18" } glib = { version = "0.18" } gtk = { version = "0.7", package = "gtk4", features = [ "v4_10" ] } +thiserror = { version = "1.0" } tokio = { version = "1.34", features = [ "full" ] } [build-dependencies] diff --git a/fitnesstrax/app/src/app.rs b/fitnesstrax/app/src/app.rs index 655b2c4..94a6e00 100644 --- a/fitnesstrax/app/src/app.rs +++ b/fitnesstrax/app/src/app.rs @@ -22,11 +22,16 @@ use std::{ path::PathBuf, sync::{Arc, RwLock}, }; +use thiserror::Error; use tokio::runtime::Runtime; +#[derive(Debug, Error)] pub enum AppError { + #[error("no database loaded")] NoDatabase, + #[error("failed to open the database")] FailedToOpenDatabase, + #[error("unhandled error")] Unhandled, } diff --git a/fitnesstrax/app/src/app_window.rs b/fitnesstrax/app/src/app_window.rs index 69d6ab1..5fc3213 100644 --- a/fitnesstrax/app/src/app_window.rs +++ b/fitnesstrax/app/src/app_window.rs @@ -1,5 +1,5 @@ /* -Copyright 2023, Savanni D'Gerinel +Copyright 2023-2024, Savanni D'Gerinel This file is part of FitnessTrax. @@ -16,8 +16,7 @@ You should have received a copy of the GNU General Public License along with Fit use crate::{ app::App, - components::DayDetail, - views::{HistoricalView, PlaceholderView, View, WelcomeView}, + views::{DayDetailView, HistoricalView, PlaceholderView, View, WelcomeView}, }; use adw::prelude::*; use chrono::{Duration, Local}; @@ -138,18 +137,7 @@ impl AppWindow { Rc::new(move |date, records| { let layout = gtk::Box::new(gtk::Orientation::Vertical, 0); layout.append(&adw::HeaderBar::new()); - layout.append(&DayDetail::new( - date, - records, - { - let s = s.clone(); - move |record| s.on_put_record(record) - }, - { - let s = s.clone(); - move |record| s.on_update_record(record) - }, - )); + layout.append(&DayDetailView::new(date, records, s.app.clone())); let page = &adw::NavigationPage::builder() .title(date.format("%Y-%m-%d").to_string()) .child(&layout) diff --git a/fitnesstrax/app/src/components/action_group.rs b/fitnesstrax/app/src/components/action_group.rs new file mode 100644 index 0000000..4197edd --- /dev/null +++ b/fitnesstrax/app/src/components/action_group.rs @@ -0,0 +1,135 @@ +/* +Copyright 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 glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; + +#[derive(Default)] +pub struct ActionGroupPrivate; + +#[glib::object_subclass] +impl ObjectSubclass for ActionGroupPrivate { + const NAME: &'static str = "ActionGroup"; + type Type = ActionGroup; + type ParentType = gtk::Box; +} + +impl ObjectImpl for ActionGroupPrivate {} +impl WidgetImpl for ActionGroupPrivate {} +impl BoxImpl for ActionGroupPrivate {} + +glib::wrapper! { + pub struct ActionGroup(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; +} + +impl ActionGroup { + fn new(builder: ActionGroupBuilder) -> Self { + let s: Self = Object::builder().build(); + s.set_orientation(builder.orientation); + + let primary_button = builder.primary_action.button(); + let secondary_button = builder.secondary_action.map(|action| action.button()); + let tertiary_button = builder.tertiary_action.map(|action| action.button()); + + if let Some(button) = tertiary_button { + s.append(&button); + } + + s.set_halign(gtk::Align::End); + if let Some(button) = secondary_button { + s.append(&button); + } + s.append(&primary_button); + + s + } + + pub fn builder() -> ActionGroupBuilder { + ActionGroupBuilder { + orientation: gtk::Orientation::Horizontal, + primary_action: Action { + label: "Ok".to_owned(), + action: Box::new(|| {}), + }, + secondary_action: None, + tertiary_action: None, + } + } +} + +struct Action { + label: String, + action: Box, +} + +impl Action { + fn button(self) -> gtk::Button { + let button = gtk::Button::builder().label(self.label).build(); + button.connect_clicked(move |_| (self.action)()); + button + } +} + +pub struct ActionGroupBuilder { + orientation: gtk::Orientation, + primary_action: Action, + secondary_action: Option, + tertiary_action: Option, +} + +impl ActionGroupBuilder { + pub fn orientation(mut self, orientation: gtk::Orientation) -> Self { + self.orientation = orientation; + self + } + + pub fn primary_action(mut self, label: &str, action: A) -> Self + where + A: Fn() + 'static, + { + self.primary_action = Action { + label: label.to_owned(), + action: Box::new(action), + }; + self + } + + pub fn secondary_action(mut self, label: &str, action: A) -> Self + where + A: Fn() + 'static, + { + self.secondary_action = Some(Action { + label: label.to_owned(), + action: Box::new(action), + }); + self + } + + pub fn tertiary_action(mut self, label: &str, action: A) -> Self + where + A: Fn() + 'static, + { + self.tertiary_action = Some(Action { + label: label.to_owned(), + action: Box::new(action), + }); + self + } + + pub fn build(self) -> ActionGroup { + ActionGroup::new(self) + } +} diff --git a/fitnesstrax/app/src/components/day.rs b/fitnesstrax/app/src/components/day.rs index 8735086..1652d6c 100644 --- a/fitnesstrax/app/src/components/day.rs +++ b/fitnesstrax/app/src/components/day.rs @@ -1,5 +1,5 @@ /* -Copyright 2023, Savanni D'Gerinel +Copyright 2023-2024, Savanni D'Gerinel This file is part of FitnessTrax. @@ -16,12 +16,13 @@ You should have received a copy of the GNU General Public License along with Fit // use chrono::NaiveDate; // use ft_core::TraxRecord; -use crate::components::{TimeDistanceView, WeightView}; +use crate::components::{ActionGroup, TimeDistanceView, Weight}; use emseries::Record; -use ft_core::{RecordType, TraxRecord, Weight}; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; -use std::cell::RefCell; +use std::{cell::RefCell, rc::Rc}; + +use super::weight::WeightEdit; pub struct DaySummaryPrivate { date: gtk::Label, @@ -67,7 +68,7 @@ impl DaySummary { s } - pub fn set_data(&self, date: chrono::NaiveDate, records: Vec>) { + pub fn set_data(&self, date: chrono::NaiveDate, records: Vec>) { self.imp() .date .set_text(&date.format("%Y-%m-%d").to_string()); @@ -77,7 +78,7 @@ impl DaySummary { } if let Some(Record { - data: TraxRecord::Weight(weight_record), + data: ft_core::TraxRecord::Weight(weight_record), .. }) = records.iter().filter(|f| f.data.is_weight()).next() { @@ -133,19 +134,25 @@ glib::wrapper! { } impl DayDetail { - pub fn new( + pub fn new( date: chrono::NaiveDate, - records: Vec>, - on_put_record: PutRecordFn, - on_update_record: UpdateRecordFn, + records: Vec>, + on_edit: OnEdit, ) -> Self where - PutRecordFn: Fn(TraxRecord) + 'static, - UpdateRecordFn: Fn(Record) + 'static, + 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(); @@ -158,52 +165,55 @@ impl DayDetail { } }); s.add_controller(click_controller); + */ let weight_record = records.iter().find_map(|record| match record { Record { id, - data: TraxRecord::Weight(record), + data: ft_core::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 })); - }), + Some((id, data)) => Weight::new(Some(data.clone())), + None => Weight::new(None), }; - s.append(&weight_view); + s.append(&weight_view.widget()); records.into_iter().for_each(|record| { let record_view = match record { Record { - data: TraxRecord::BikeRide(record), + data: ft_core::TraxRecord::BikeRide(record), .. } => Some( - TimeDistanceView::new(RecordType::BikeRide, record).upcast::(), + TimeDistanceView::new(ft_core::RecordType::BikeRide, record) + .upcast::(), ), Record { - data: TraxRecord::Row(record), + data: ft_core::TraxRecord::Row(record), .. - } => Some(TimeDistanceView::new(RecordType::Row, record).upcast::()), + } => Some( + TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::(), + ), Record { - data: TraxRecord::Run(record), + data: ft_core::TraxRecord::Run(record), .. - } => Some(TimeDistanceView::new(RecordType::Row, record).upcast::()), + } => Some( + TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::(), + ), Record { - data: TraxRecord::Swim(record), + data: ft_core::TraxRecord::Swim(record), .. - } => Some(TimeDistanceView::new(RecordType::Row, record).upcast::()), + } => Some( + TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::(), + ), Record { - data: TraxRecord::Walk(record), + data: ft_core::TraxRecord::Walk(record), .. - } => Some(TimeDistanceView::new(RecordType::Row, record).upcast::()), + } => Some( + TimeDistanceView::new(ft_core::RecordType::Row, record).upcast::(), + ), _ => None, }; @@ -218,3 +228,106 @@ impl DayDetail { s } } + +pub struct DayEditPrivate { + date: gtk::Label, + weight: Rc, +} + +impl Default for DayEditPrivate { + fn default() -> Self { + Self { + date: gtk::Label::new(None), + weight: Rc::new(WeightEdit::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( + date: chrono::NaiveDate, + records: Vec>, + on_put_record: PutRecordFn, + on_update_record: UpdateRecordFn, + on_cancel: CancelFn, + ) -> Self + where + PutRecordFn: Fn(ft_core::TraxRecord) + 'static, + UpdateRecordFn: Fn(Record) + 'static, + CancelFn: Fn() + 'static, + { + let s: Self = Object::builder().build(); + s.set_orientation(gtk::Orientation::Vertical); + s.set_hexpand(true); + + s.append( + &ActionGroup::builder() + .primary_action("Save", { + let s = s.clone(); + let records = records.clone(); + move || { + let weight_record = records.iter().find_map(|record| match record { + Record { + id, + data: ft_core::TraxRecord::Weight(w), + } => Some((id, w)), + _ => None, + }); + + let weight = s.imp().weight.value(); + + if let Some(weight) = weight { + match weight_record { + Some((id, _)) => on_update_record(Record { + id: id.clone(), + data: ft_core::TraxRecord::Weight(ft_core::Weight { + date, + weight, + }), + }), + None => { + on_put_record(ft_core::TraxRecord::Weight(ft_core::Weight { + date, + weight, + })) + } + } + }; + } + }) + .secondary_action("Cancel", on_cancel) + .build(), + ); + + let weight_record = records.iter().find_map(|record| match record { + Record { + id, + data: ft_core::TraxRecord::Weight(record), + } => Some((id.clone(), record.clone())), + _ => None, + }); + + match weight_record { + Some((_id, data)) => s.imp().weight.set_value(Some(data.weight)), + None => s.imp().weight.set_value(None), + }; + s.append(&s.imp().weight.widget()); + + s + } +} diff --git a/fitnesstrax/app/src/components/mod.rs b/fitnesstrax/app/src/components/mod.rs index 19ee548..75ecdd2 100644 --- a/fitnesstrax/app/src/components/mod.rs +++ b/fitnesstrax/app/src/components/mod.rs @@ -1,5 +1,5 @@ /* -Copyright 2023, Savanni D'Gerinel +Copyright 2023-2024, Savanni D'Gerinel This file is part of FitnessTrax. @@ -14,12 +14,18 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ +mod action_group; +pub use action_group::ActionGroup; + mod day; -pub use day::{DayDetail, DaySummary}; +pub use day::{DayDetail, DayEdit, DaySummary}; mod edit_view; pub use edit_view::EditView; +mod singleton; +pub use singleton::{Singleton, SingletonImpl}; + mod text_entry; pub use text_entry::{ParseError, TextEntry}; @@ -27,7 +33,7 @@ mod time_distance; pub use time_distance::TimeDistanceView; mod weight; -pub use weight::WeightView; +pub use weight::Weight; use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; diff --git a/fitnesstrax/app/src/components/singleton.rs b/fitnesstrax/app/src/components/singleton.rs new file mode 100644 index 0000000..340c090 --- /dev/null +++ b/fitnesstrax/app/src/components/singleton.rs @@ -0,0 +1,71 @@ +/* +Copyright 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 . +*/ + +//! A Widget container for a single components +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; +use std::cell::RefCell; + +pub struct SingletonPrivate { + widget: RefCell, +} + +impl Default for SingletonPrivate { + fn default() -> Self { + Self { + widget: RefCell::new(gtk::Label::new(None).upcast()), + } + } +} + +#[glib::object_subclass] +impl ObjectSubclass for SingletonPrivate { + const NAME: &'static str = "Singleton"; + type Type = Singleton; + type ParentType = gtk::Box; +} + +impl ObjectImpl for SingletonPrivate {} +impl WidgetImpl for SingletonPrivate {} +impl BoxImpl for SingletonPrivate {} + +glib::wrapper! { + /// The Singleton component contains exactly one child widget. The swap function makes it easy + /// to handle the job of swapping that child out for a different one. + pub struct Singleton(ObjectSubclass) @extends gtk::Box, gtk::Widget; +} + +impl Default for Singleton { + fn default() -> Self { + let s: Self = Object::builder().build(); + + s.append(&*s.imp().widget.borrow()); + + s + } +} + +impl Singleton { + pub fn swap(&self, new_widget: &impl IsA) { + let new_widget = new_widget.clone().upcast(); + self.remove(&*self.imp().widget.borrow()); + self.append(&new_widget); + *self.imp().widget.borrow_mut() = new_widget; + } +} + +pub trait SingletonImpl: WidgetImpl + BoxImpl {} +unsafe impl IsSubclassable for Singleton {} diff --git a/fitnesstrax/app/src/components/text_entry.rs b/fitnesstrax/app/src/components/text_entry.rs index 2220ec9..996f1ed 100644 --- a/fitnesstrax/app/src/components/text_entry.rs +++ b/fitnesstrax/app/src/components/text_entry.rs @@ -20,16 +20,26 @@ use std::{cell::RefCell, rc::Rc}; pub struct ParseError; #[derive(Clone)] -pub struct TextEntry { +pub struct TextEntry { value: Rc>>, widget: gtk::Entry, renderer: Rc String>>, - validator: Rc Result>>, + parser: Rc Result>>, +} + +impl std::fmt::Debug for TextEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + f, + "{{ value: {:?}, widget: {:?} }}", + self.value, self.widget + ) + } } // I do not understand why the data should be 'static. -impl TextEntry { - pub fn new(placeholder: &str, value: Option, renderer: R, validator: V) -> Self +impl TextEntry { + pub fn new(placeholder: &str, value: Option, renderer: R, parser: V) -> Self where R: Fn(&T) -> String + 'static, V: Fn(&str) -> Result + 'static, @@ -44,7 +54,7 @@ impl TextEntry { value: Rc::new(RefCell::new(value)), widget, renderer: Rc::new(Box::new(renderer)), - validator: Rc::new(Box::new(validator)), + parser: Rc::new(Box::new(parser)), }; s.widget.buffer().connect_text_notify({ @@ -61,7 +71,7 @@ impl TextEntry { self.widget.remove_css_class("error"); return; } - match (self.validator)(buffer.text().as_str()) { + match (self.parser)(buffer.text().as_str()) { Ok(v) => { *self.value.borrow_mut() = Some(v); self.widget.remove_css_class("error"); @@ -74,6 +84,7 @@ impl TextEntry { } pub fn value(&self) -> Option { + let v = self.value.borrow().clone(); self.value.borrow().clone() } diff --git a/fitnesstrax/app/src/components/weight.rs b/fitnesstrax/app/src/components/weight.rs index f32d11e..552b107 100644 --- a/fitnesstrax/app/src/components/weight.rs +++ b/fitnesstrax/app/src/components/weight.rs @@ -14,173 +14,66 @@ 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 crate::components::{EditView, ParseError, TextEntry}; +use crate::components::{EditView, ParseError, Singleton, TextEntry}; use chrono::{Local, NaiveDate}; use dimensioned::si; -use ft_core::Weight; -use glib::Object; +use glib::{object::ObjectRef, Object}; use gtk::{prelude::*, subclass::prelude::*}; -use std::cell::RefCell; +use std::{borrow::Borrow, cell::RefCell}; -pub struct WeightViewPrivate { - date: RefCell, - record: RefCell>, +#[derive(Default)] +pub struct WeightViewPrivate {} - widget: RefCell>>>, - - on_edit_finished: RefCell)>>, +pub struct Weight { + label: gtk::Label, } -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() +impl Weight { + pub fn new(weight: Option) -> Self { + let label = 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"); - } + match weight { + Some(w) => label.set_text(&format!("{:?}", w.weight)), + None => label.set_text("No weight recorded"), } - self.swap(EditView::View(view)); + Self { label } } - 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; - } - - pub 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(); + pub fn widget(&self) -> gtk::Widget { + self.label.clone().upcast() + } +} + +#[derive(Debug)] +pub struct WeightEdit { + entry: TextEntry>, +} + +impl WeightEdit { + pub fn new(weight: Option) -> Self { + Self { + entry: TextEntry::new( + "0 kg", + weight.map(|w| w.weight), + |val: &si::Kilogram| val.to_string(), + |v: &str| v.parse::().map(|w| w * si::KG).map_err(|_| ParseError), + ), + } + } + + pub fn set_value(&self, value: Option>) { + self.entry.set_value(value); + } + + pub fn value(&self) -> Option> { + self.entry.value() + } + + pub fn widget(&self) -> gtk::Widget { + self.entry.widget() } } diff --git a/fitnesstrax/app/src/views/day_detail_view.rs b/fitnesstrax/app/src/views/day_detail_view.rs new file mode 100644 index 0000000..c73d1f0 --- /dev/null +++ b/fitnesstrax/app/src/views/day_detail_view.rs @@ -0,0 +1,164 @@ +/* +Copyright 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 crate::{ + app::App, + components::{DayDetail, DayEdit, Singleton, SingletonImpl}, +}; +use emseries::Record; +use ft_core::TraxRecord; +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; +use std::cell::RefCell; + +#[derive(Default)] +pub struct DayDetailViewPrivate { + app: RefCell>, + container: Singleton, + date: RefCell, + records: RefCell>>, +} + +#[glib::object_subclass] +impl ObjectSubclass for DayDetailViewPrivate { + const NAME: &'static str = "DayDetailView"; + type Type = DayDetailView; + type ParentType = gtk::Box; +} + +impl ObjectImpl for DayDetailViewPrivate {} +impl WidgetImpl for DayDetailViewPrivate {} +impl BoxImpl for DayDetailViewPrivate {} +impl SingletonImpl for DayDetailViewPrivate {} + +glib::wrapper! { + pub struct DayDetailView(ObjectSubclass) @extends gtk::Box, gtk::Widget; +} + +impl DayDetailView { + pub fn new(date: chrono::NaiveDate, records: Vec>, app: App) -> Self { + let s: Self = Object::builder().build(); + + *s.imp().date.borrow_mut() = date; + *s.imp().records.borrow_mut() = records; + *s.imp().app.borrow_mut() = Some(app); + + s.append(&s.imp().container); + + /* + s.imp() + .container + .swap(&DayDetail::new(date, records.clone(), { + let s = s.clone(); + let records = records.clone(); + move || { + s.imp().container.swap(&DayEdit::new( + date, + records, + s.on_put_record(), + // s.on_update_record(), + |_| {}, + )) + } + })); + */ + + s.view(); + + s + } + + fn view(&self) { + self.imp().container.swap(&DayDetail::new( + self.imp().date.borrow().clone(), + self.imp().records.borrow().clone(), + { + let s = self.clone(); + move || s.edit() + }, + )); + } + + fn edit(&self) { + self.imp().container.swap(&DayEdit::new( + self.imp().date.borrow().clone(), + self.imp().records.borrow().clone(), + self.on_put_record(), + self.on_update_record(), + { + let s = self.clone(); + move || s.view() + }, + )); + } + + fn on_put_record(&self) -> Box { + let s = self.clone(); + let app = self.imp().app.clone(); + Box::new(move |record| { + let s = s.clone(); + let app = app.clone(); + glib::spawn_future_local({ + async move { + match &*app.borrow() { + Some(app) => { + let id = app + .put_record(record.clone()) + .await + .expect("successful write"); + s.imp() + .records + .borrow_mut() + .push(Record { id, data: record }); + } + None => {} + } + s.view(); + } + }); + }) + } + + fn on_update_record(&self) -> Box)> { + let s = self.clone(); + let app = self.imp().app.clone(); + Box::new(move |updated_record| { + let app = app.clone(); + + let mut records = s.imp().records.borrow_mut(); + let idx = records.iter().position(|r| r.id == updated_record.id); + match idx { + Some(i) => records[i] = updated_record.clone(), + None => records.push(updated_record.clone()), + } + + glib::spawn_future_local({ + let s = s.clone(); + async move { + match &*app.borrow() { + Some(app) => { + let _ = app.update_record(updated_record).await; + } + None => { + println!("no app!"); + } + } + s.view(); + } + }); + }) + } +} diff --git a/fitnesstrax/app/src/views/mod.rs b/fitnesstrax/app/src/views/mod.rs index e014581..9957823 100644 --- a/fitnesstrax/app/src/views/mod.rs +++ b/fitnesstrax/app/src/views/mod.rs @@ -16,6 +16,9 @@ You should have received a copy of the GNU General Public License along with Fit use gtk::prelude::*; +mod day_detail_view; +pub use day_detail_view::DayDetailView; + mod historical_view; pub use historical_view::HistoricalView;