Filter the historical list according to a date picker #194
|
@ -0,0 +1,174 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||||
|
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::{components::{i32_field_builder, TextEntry, month_field_builder}, types::ParseError};
|
||||||
|
use chrono::{Datelike, Local};
|
||||||
|
use glib::Object;
|
||||||
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
pub struct DateFieldPrivate {
|
||||||
|
date: Rc<RefCell<chrono::NaiveDate>>,
|
||||||
|
year: TextEntry<i32>,
|
||||||
|
month: TextEntry<u32>,
|
||||||
|
day: TextEntry<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for DateFieldPrivate {
|
||||||
|
const NAME: &'static str = "DateField";
|
||||||
|
type Type = DateField;
|
||||||
|
type ParentType = gtk::Box;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
let date = Rc::new(RefCell::new(Local::now().date_naive()));
|
||||||
|
|
||||||
|
let year = i32_field_builder()
|
||||||
|
.with_value(date.borrow().year())
|
||||||
|
.with_on_update(
|
||||||
|
{
|
||||||
|
let date = date.clone();
|
||||||
|
move |value| {
|
||||||
|
if let Some(year) = value {
|
||||||
|
let mut date = date.borrow_mut();
|
||||||
|
if let Some(new_date) = date.with_year(year) {
|
||||||
|
*date = new_date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_length(4)
|
||||||
|
.with_css_classes(vec!["date-field__year".to_owned()]).build();
|
||||||
|
|
||||||
|
let month = month_field_builder()
|
||||||
|
.with_value(date.borrow().month())
|
||||||
|
.with_on_update(
|
||||||
|
{
|
||||||
|
let date = date.clone();
|
||||||
|
move |value| {
|
||||||
|
if let Some(month) = value {
|
||||||
|
let mut date = date.borrow_mut();
|
||||||
|
if let Some(new_date) = date.with_month(month) {
|
||||||
|
*date = new_date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_css_classes(vec!["date-field__month".to_owned()])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
/* Modify this so that it enforces the number of days per month */
|
||||||
|
let day = TextEntry::builder()
|
||||||
|
.with_placeholder("day".to_owned())
|
||||||
|
.with_value(date.borrow().day())
|
||||||
|
.with_renderer(|v| format!("{}", v))
|
||||||
|
.with_parser(|v| v.parse::<u32>().map_err(|_| ParseError))
|
||||||
|
.with_on_update({
|
||||||
|
let date = date.clone();
|
||||||
|
move |value| {
|
||||||
|
if let Some(day) = value {
|
||||||
|
let mut date = date.borrow_mut();
|
||||||
|
if let Some(new_date) = date.with_day(day) {
|
||||||
|
*date = new_date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_css_classes(vec!["date-field__day".to_owned()])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
date,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for DateFieldPrivate {}
|
||||||
|
impl WidgetImpl for DateFieldPrivate {}
|
||||||
|
impl BoxImpl for DateFieldPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct DateField(ObjectSubclass<DateFieldPrivate>) @extends gtk::Box, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Render a date in the format 2006 Jan 01. The entire date is editable. When the user moves to one part of the date, it will be erased and replaced with a grey placeholder.
|
||||||
|
*/
|
||||||
|
impl DateField {
|
||||||
|
pub fn new(date: chrono::NaiveDate) -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
|
||||||
|
println!("{}", date);
|
||||||
|
|
||||||
|
s.append(&s.imp().year.widget());
|
||||||
|
s.append(&s.imp().month.widget());
|
||||||
|
s.append(&s.imp().day.widget());
|
||||||
|
|
||||||
|
s.set_date(date);
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_date(&self, date: chrono::NaiveDate) {
|
||||||
|
self.imp().year.set_value(Some(date.year()));
|
||||||
|
self.imp().month.set_value(Some(date.month()));
|
||||||
|
self.imp().day.set_value(Some(date.day()));
|
||||||
|
|
||||||
|
*self.imp().date.borrow_mut() = date;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn date(&self) -> chrono::NaiveDate {
|
||||||
|
*self.imp().date.borrow()
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::gtk_init::gtk_init;
|
||||||
|
|
||||||
|
// Enabling this test pushes tests on the TextField into an infinite loop. That likely indicates a bad interaction within the TextField itself, and that is going to need to be fixed.
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn it_allows_valid_dates() {
|
||||||
|
let reference = chrono::NaiveDate::from_ymd_opt(2006, 01, 02).unwrap();
|
||||||
|
let field = DateField::new(reference);
|
||||||
|
field.imp().year.set_value(Some(2023));
|
||||||
|
field.imp().month.set_value(Some(10));
|
||||||
|
field.imp().day.set_value(Some(13));
|
||||||
|
// assert!(field.is_valid());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn it_disallows_out_of_range_months() {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn it_allows_days_within_range_for_month() {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||||
|
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
components::{DateField},
|
||||||
|
types::DayInterval,
|
||||||
|
};
|
||||||
|
use chrono::{Duration, Local, Months};
|
||||||
|
use glib::Object;
|
||||||
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
|
use std::{cell::RefCell};
|
||||||
|
|
||||||
|
type OnSearch = dyn Fn(DayInterval) + 'static;
|
||||||
|
|
||||||
|
pub struct DateRangePickerPrivate {
|
||||||
|
start: DateField,
|
||||||
|
end: DateField,
|
||||||
|
|
||||||
|
on_search: RefCell<Box<OnSearch>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for DateRangePickerPrivate {
|
||||||
|
const NAME: &'static str = "DateRangePicker";
|
||||||
|
type Type = DateRangePicker;
|
||||||
|
type ParentType = gtk::Box;
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
let default_date = Local::now().date_naive();
|
||||||
|
Self {
|
||||||
|
start: DateField::new(default_date),
|
||||||
|
end: DateField::new(default_date),
|
||||||
|
on_search: RefCell::new(Box::new(|_| {})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for DateRangePickerPrivate {}
|
||||||
|
impl WidgetImpl for DateRangePickerPrivate {}
|
||||||
|
impl BoxImpl for DateRangePickerPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct DateRangePicker(ObjectSubclass<DateRangePickerPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl DateRangePicker {
|
||||||
|
pub fn connect_on_search<OnSearch>(&self, f: OnSearch)
|
||||||
|
where
|
||||||
|
OnSearch: Fn(DayInterval) + 'static,
|
||||||
|
{
|
||||||
|
*self.imp().on_search.borrow_mut() = Box::new(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_interval(&self, start: chrono::NaiveDate, end: chrono::NaiveDate) {
|
||||||
|
self.imp().start.set_date(start);
|
||||||
|
self.imp().end.set_date(end);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interval(&self) -> DayInterval {
|
||||||
|
DayInterval {
|
||||||
|
start: self.imp().start.date(),
|
||||||
|
end: self.imp().end.date(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Default for DateRangePicker {
|
||||||
|
fn default() -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
s.set_orientation(gtk::Orientation::Vertical);
|
||||||
|
|
||||||
|
let search_button = gtk::Button::with_label("Search");
|
||||||
|
search_button.connect_clicked({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_| (s.imp().on_search.borrow())(s.interval())
|
||||||
|
});
|
||||||
|
|
||||||
|
let last_week_button = gtk::Button::builder().label("last week").build();
|
||||||
|
last_week_button.connect_clicked({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_| {
|
||||||
|
let end = Local::now().date_naive();
|
||||||
|
let start = end - Duration::days(7);
|
||||||
|
s.set_interval(start, end);
|
||||||
|
(s.imp().on_search.borrow())(s.interval());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let two_weeks_button = gtk::Button::builder().label("last two weeks").build();
|
||||||
|
two_weeks_button.connect_clicked({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_| {
|
||||||
|
let end = Local::now().date_naive();
|
||||||
|
let start = end - Duration::days(14);
|
||||||
|
s.set_interval(start, end);
|
||||||
|
(s.imp().on_search.borrow())(s.interval());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let last_month_button = gtk::Button::builder().label("last month").build();
|
||||||
|
last_month_button.connect_clicked({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_| {
|
||||||
|
let end = Local::now().date_naive();
|
||||||
|
let start = end - Months::new(1);
|
||||||
|
s.set_interval(start, end);
|
||||||
|
(s.imp().on_search.borrow())(s.interval());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let six_months_button = gtk::Button::builder().label("last six months").build();
|
||||||
|
six_months_button.connect_clicked({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_| {
|
||||||
|
let end = Local::now().date_naive();
|
||||||
|
let start = end - Months::new(6);
|
||||||
|
s.set_interval(start, end);
|
||||||
|
(s.imp().on_search.borrow())(s.interval());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let last_year_button = gtk::Button::builder().label("last year").build();
|
||||||
|
last_year_button.connect_clicked({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_| {
|
||||||
|
let end = Local::now().date_naive();
|
||||||
|
let start = end - Months::new(12);
|
||||||
|
s.set_interval(start, end);
|
||||||
|
(s.imp().on_search.borrow())(s.interval());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let date_row = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.build();
|
||||||
|
date_row.append(&s.imp().start);
|
||||||
|
date_row.append(>k::Label::new(Some("to")));
|
||||||
|
date_row.append(&s.imp().end);
|
||||||
|
date_row.append(&search_button);
|
||||||
|
|
||||||
|
let quick_picker = gtk::Box::builder()
|
||||||
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
.build();
|
||||||
|
quick_picker.append(&last_week_button);
|
||||||
|
quick_picker.append(&two_weeks_button);
|
||||||
|
quick_picker.append(&last_month_button);
|
||||||
|
quick_picker.append(&six_months_button);
|
||||||
|
quick_picker.append(&last_year_button);
|
||||||
|
|
||||||
|
s.append(&date_row);
|
||||||
|
s.append(&quick_picker);
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,12 @@ pub use action_group::ActionGroup;
|
||||||
mod day;
|
mod day;
|
||||||
pub use day::{DayDetail, DayEdit, DaySummary};
|
pub use day::{DayDetail, DayEdit, DaySummary};
|
||||||
|
|
||||||
|
mod date_field;
|
||||||
|
pub use date_field::DateField;
|
||||||
|
|
||||||
|
mod date_range;
|
||||||
|
pub use date_range::DateRangePicker;
|
||||||
|
|
||||||
mod singleton;
|
mod singleton;
|
||||||
pub use singleton::{Singleton, SingletonImpl};
|
pub use singleton::{Singleton, SingletonImpl};
|
||||||
|
|
||||||
|
@ -28,7 +34,7 @@ mod steps;
|
||||||
pub use steps::{steps_editor, Steps};
|
pub use steps::{steps_editor, Steps};
|
||||||
|
|
||||||
mod text_entry;
|
mod text_entry;
|
||||||
pub use text_entry::{distance_field, duration_field, time_field, weight_field, TextEntry};
|
pub use text_entry::{distance_field, duration_field, time_field, weight_field, i32_field_builder, month_field_builder, TextEntry};
|
||||||
|
|
||||||
mod time_distance;
|
mod time_distance;
|
||||||
pub use time_distance::{time_distance_detail, time_distance_summary};
|
pub use time_distance::{time_distance_detail, time_distance_summary};
|
||||||
|
|
|
@ -46,11 +46,15 @@ pub fn steps_editor<OnUpdate>(value: Option<u32>, on_update: OnUpdate) -> TextEn
|
||||||
where
|
where
|
||||||
OnUpdate: Fn(Option<u32>) + 'static,
|
OnUpdate: Fn(Option<u32>) + 'static,
|
||||||
{
|
{
|
||||||
TextEntry::new(
|
let text_entry = TextEntry::builder()
|
||||||
"0",
|
.with_placeholder( "0".to_owned())
|
||||||
value,
|
.with_renderer(|v| format!("{}", v))
|
||||||
|v| format!("{}", v),
|
.with_parser(|v| v.parse::<u32>().map_err(|_| ParseError))
|
||||||
|v| v.parse::<u32>().map_err(|_| ParseError),
|
.with_on_update(on_update);
|
||||||
on_update,
|
|
||||||
)
|
if let Some(time) = value {
|
||||||
|
text_entry.with_value(time)
|
||||||
|
} else {
|
||||||
|
text_entry
|
||||||
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,11 +23,13 @@ use std::{cell::RefCell, rc::Rc};
|
||||||
pub type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>;
|
pub type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>;
|
||||||
pub type OnUpdate<T> = dyn Fn(Option<T>);
|
pub type OnUpdate<T> = dyn Fn(Option<T>);
|
||||||
|
|
||||||
|
// TextEntry is not a proper widget because I was never able to figure out how to do a type parameterization on a GTK widget.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TextEntry<T: Clone + std::fmt::Debug> {
|
pub struct TextEntry<T: Clone + std::fmt::Debug> {
|
||||||
value: Rc<RefCell<Option<T>>>,
|
value: Rc<RefCell<Option<T>>>,
|
||||||
|
|
||||||
widget: gtk::Entry,
|
widget: gtk::Entry,
|
||||||
|
renderer: Rc<dyn Fn(&T) -> String>,
|
||||||
parser: Rc<Parser<T>>,
|
parser: Rc<Parser<T>>,
|
||||||
on_update: Rc<OnUpdate<T>>,
|
on_update: Rc<OnUpdate<T>>,
|
||||||
}
|
}
|
||||||
|
@ -44,28 +46,20 @@ impl<T: Clone + std::fmt::Debug> std::fmt::Debug for TextEntry<T> {
|
||||||
|
|
||||||
// I do not understand why the data should be 'static.
|
// I do not understand why the data should be 'static.
|
||||||
impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
|
impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
|
||||||
pub fn new<R, V, U>(
|
fn from_builder(builder: TextEntryBuilder<T>) -> TextEntry<T> {
|
||||||
placeholder: &str,
|
let widget = gtk::Entry::builder()
|
||||||
value: Option<T>,
|
.placeholder_text(builder.placeholder)
|
||||||
renderer: R,
|
.build();
|
||||||
parser: V,
|
if let Some(ref v) = builder.value {
|
||||||
on_update: U,
|
widget.set_text(&(builder.renderer)(v))
|
||||||
) -> Self
|
|
||||||
where
|
|
||||||
R: Fn(&T) -> String + 'static,
|
|
||||||
V: Fn(&str) -> Result<T, ParseError> + 'static,
|
|
||||||
U: Fn(Option<T>) + 'static,
|
|
||||||
{
|
|
||||||
let widget = gtk::Entry::builder().placeholder_text(placeholder).build();
|
|
||||||
if let Some(ref v) = value {
|
|
||||||
widget.set_text(&renderer(v))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let s = Self {
|
let s = Self {
|
||||||
value: Rc::new(RefCell::new(value)),
|
value: Rc::new(RefCell::new(builder.value)),
|
||||||
widget,
|
widget,
|
||||||
parser: Rc::new(parser),
|
renderer: Rc::new(builder.renderer),
|
||||||
on_update: Rc::new(on_update),
|
parser: Rc::new(builder.parser),
|
||||||
|
on_update: Rc::new(builder.on_update),
|
||||||
};
|
};
|
||||||
|
|
||||||
s.widget.buffer().connect_text_notify({
|
s.widget.buffer().connect_text_notify({
|
||||||
|
@ -73,9 +67,27 @@ impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
|
||||||
move |buffer| s.handle_text_change(buffer)
|
move |buffer| s.handle_text_change(buffer)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Some(length) = builder.length {
|
||||||
|
s.widget.set_max_length(length.try_into().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// let classes: Vec<&str> = builder.css_classes.iter(|v| v.as_ref()).collect();
|
||||||
|
let classes: Vec<&str> = builder.css_classes.iter().map(AsRef::as_ref).collect();
|
||||||
|
s.widget.set_css_classes(&classes);
|
||||||
|
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn builder() -> TextEntryBuilder<T> {
|
||||||
|
TextEntryBuilder::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_value(&self, val: Option<T>) {
|
||||||
|
if let Some(ref v) = val {
|
||||||
|
self.widget.set_text(&(self.renderer)(v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_text_change(&self, buffer: >k::EntryBuffer) {
|
fn handle_text_change(&self, buffer: >k::EntryBuffer) {
|
||||||
if buffer.text().is_empty() {
|
if buffer.text().is_empty() {
|
||||||
*self.value.borrow_mut() = None;
|
*self.value.borrow_mut() = None;
|
||||||
|
@ -106,7 +118,85 @@ impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
pub struct TextEntryBuilder<T: Clone + std::fmt::Debug + 'static> {
|
||||||
|
placeholder: String,
|
||||||
|
value: Option<T>,
|
||||||
|
length: Option<usize>,
|
||||||
|
css_classes: Vec<String>,
|
||||||
|
renderer: Box<dyn Fn(&T) -> String>,
|
||||||
|
parser: Box<Parser<T>>,
|
||||||
|
on_update: Box<OnUpdate<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + std::fmt::Debug + 'static> Default for TextEntryBuilder<T> {
|
||||||
|
fn default() -> TextEntryBuilder<T> {
|
||||||
|
TextEntryBuilder {
|
||||||
|
placeholder: "".to_owned(),
|
||||||
|
value: None,
|
||||||
|
length: None,
|
||||||
|
css_classes: vec![],
|
||||||
|
renderer: Box::new(|_| "".to_owned()),
|
||||||
|
parser: Box::new(|_| Err(ParseError)),
|
||||||
|
on_update: Box::new(|_| {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + std::fmt::Debug + 'static> TextEntryBuilder<T> {
|
||||||
|
pub fn build(self) -> TextEntry<T> {
|
||||||
|
TextEntry::from_builder(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_placeholder(self, placeholder: String) -> Self {
|
||||||
|
Self {
|
||||||
|
placeholder,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_value(self, value: T) -> Self {
|
||||||
|
Self {
|
||||||
|
value: Some(value),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_length(self, length: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
length: Some(length),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_css_classes(self, classes: Vec<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
css_classes: classes,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_renderer(self, renderer: impl Fn(&T) -> String + 'static) -> Self {
|
||||||
|
Self {
|
||||||
|
renderer: Box::new(renderer),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_parser(self, parser: impl Fn(&str) -> Result<T, ParseError> + 'static) -> Self {
|
||||||
|
Self {
|
||||||
|
parser: Box::new(parser),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_on_update(self, on_update: impl Fn(Option<T>) + 'static) -> Self {
|
||||||
|
Self {
|
||||||
|
on_update: Box::new(on_update),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn time_field<OnUpdate>(
|
pub fn time_field<OnUpdate>(
|
||||||
value: Option<TimeFormatter>,
|
value: Option<TimeFormatter>,
|
||||||
on_update: OnUpdate,
|
on_update: OnUpdate,
|
||||||
|
@ -114,16 +204,20 @@ pub fn time_field<OnUpdate>(
|
||||||
where
|
where
|
||||||
OnUpdate: Fn(Option<TimeFormatter>) + 'static,
|
OnUpdate: Fn(Option<TimeFormatter>) + 'static,
|
||||||
{
|
{
|
||||||
TextEntry::new(
|
let text_entry = TextEntry::builder()
|
||||||
"HH:MM",
|
.with_placeholder("HH:MM".to_owned())
|
||||||
value,
|
.with_renderer(|val: &TimeFormatter| val.format(FormatOption::Abbreviated))
|
||||||
|val| val.format(FormatOption::Abbreviated),
|
.with_parser(TimeFormatter::parse)
|
||||||
TimeFormatter::parse,
|
.with_on_update(on_update);
|
||||||
on_update,
|
|
||||||
)
|
if let Some(time) = value {
|
||||||
|
text_entry.with_value(time)
|
||||||
|
} else {
|
||||||
|
text_entry
|
||||||
|
}
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub fn distance_field<OnUpdate>(
|
pub fn distance_field<OnUpdate>(
|
||||||
value: Option<DistanceFormatter>,
|
value: Option<DistanceFormatter>,
|
||||||
on_update: OnUpdate,
|
on_update: OnUpdate,
|
||||||
|
@ -131,16 +225,20 @@ pub fn distance_field<OnUpdate>(
|
||||||
where
|
where
|
||||||
OnUpdate: Fn(Option<DistanceFormatter>) + 'static,
|
OnUpdate: Fn(Option<DistanceFormatter>) + 'static,
|
||||||
{
|
{
|
||||||
TextEntry::new(
|
let text_entry = TextEntry::builder()
|
||||||
"0 km",
|
.with_placeholder("0 km".to_owned())
|
||||||
value,
|
.with_renderer(|val: &DistanceFormatter| val.format(FormatOption::Abbreviated))
|
||||||
|val| val.format(FormatOption::Abbreviated),
|
.with_parser(DistanceFormatter::parse)
|
||||||
DistanceFormatter::parse,
|
.with_on_update(on_update);
|
||||||
on_update,
|
|
||||||
)
|
if let Some(distance) = value {
|
||||||
|
text_entry.with_value(distance)
|
||||||
|
} else {
|
||||||
|
text_entry
|
||||||
|
}
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub fn duration_field<OnUpdate>(
|
pub fn duration_field<OnUpdate>(
|
||||||
value: Option<DurationFormatter>,
|
value: Option<DurationFormatter>,
|
||||||
on_update: OnUpdate,
|
on_update: OnUpdate,
|
||||||
|
@ -148,13 +246,18 @@ pub fn duration_field<OnUpdate>(
|
||||||
where
|
where
|
||||||
OnUpdate: Fn(Option<DurationFormatter>) + 'static,
|
OnUpdate: Fn(Option<DurationFormatter>) + 'static,
|
||||||
{
|
{
|
||||||
TextEntry::new(
|
let text_entry = TextEntry::builder()
|
||||||
"0 m",
|
.with_placeholder("0 m".to_owned())
|
||||||
value,
|
.with_renderer(|val: &DurationFormatter| val.format(FormatOption::Abbreviated))
|
||||||
|val| val.format(FormatOption::Abbreviated),
|
.with_parser(DurationFormatter::parse)
|
||||||
DurationFormatter::parse,
|
.with_on_update(on_update);
|
||||||
on_update,
|
|
||||||
)
|
if let Some(duration) = value {
|
||||||
|
text_entry.with_value(duration)
|
||||||
|
} else {
|
||||||
|
text_entry
|
||||||
|
}
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
pub fn weight_field<OnUpdate>(
|
pub fn weight_field<OnUpdate>(
|
||||||
weight: Option<WeightFormatter>,
|
weight: Option<WeightFormatter>,
|
||||||
|
@ -163,13 +266,39 @@ pub fn weight_field<OnUpdate>(
|
||||||
where
|
where
|
||||||
OnUpdate: Fn(Option<WeightFormatter>) + 'static,
|
OnUpdate: Fn(Option<WeightFormatter>) + 'static,
|
||||||
{
|
{
|
||||||
TextEntry::new(
|
let text_entry = TextEntry::builder()
|
||||||
"0 kg",
|
.with_placeholder("0 kg".to_owned())
|
||||||
weight,
|
.with_renderer(|val: &WeightFormatter| val.format(FormatOption::Abbreviated))
|
||||||
|val| val.format(FormatOption::Abbreviated),
|
.with_parser(WeightFormatter::parse)
|
||||||
WeightFormatter::parse,
|
.with_on_update(on_update);
|
||||||
on_update,
|
if let Some(weight) = weight {
|
||||||
)
|
text_entry.with_value(weight)
|
||||||
|
} else {
|
||||||
|
text_entry
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn i32_field_builder() -> TextEntryBuilder<i32>
|
||||||
|
{
|
||||||
|
TextEntry::builder()
|
||||||
|
.with_placeholder("0".to_owned())
|
||||||
|
.with_renderer(|val| format!("{}", val))
|
||||||
|
.with_parser(|v| v.parse::<i32>().map_err(|_| ParseError))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn month_field_builder() -> TextEntryBuilder<u32>
|
||||||
|
{
|
||||||
|
TextEntry::builder()
|
||||||
|
.with_placeholder("0".to_owned())
|
||||||
|
.with_renderer(|val| format!("{}", val))
|
||||||
|
.with_parser(|v| {
|
||||||
|
let val = v.parse::<u32>().map_err(|_| ParseError)?;
|
||||||
|
if val == 0 || val > 12 {
|
||||||
|
return Err(ParseError);
|
||||||
|
}
|
||||||
|
Ok(val)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -180,16 +309,15 @@ mod test {
|
||||||
fn setup_u32_entry() -> (Rc<RefCell<Option<u32>>>, TextEntry<u32>) {
|
fn setup_u32_entry() -> (Rc<RefCell<Option<u32>>>, TextEntry<u32>) {
|
||||||
let current_value = Rc::new(RefCell::new(None));
|
let current_value = Rc::new(RefCell::new(None));
|
||||||
|
|
||||||
let entry = TextEntry::new(
|
let entry = TextEntry::builder()
|
||||||
"step count",
|
.with_placeholder("step count".to_owned())
|
||||||
None,
|
.with_renderer(|steps| format!("{}", steps))
|
||||||
|steps| format!("{}", steps),
|
.with_parser(|v| v.parse::<u32>().map_err(|_| ParseError))
|
||||||
|v| v.parse::<u32>().map_err(|_| ParseError),
|
.with_on_update({
|
||||||
{
|
|
||||||
let current_value = current_value.clone();
|
let current_value = current_value.clone();
|
||||||
move |v| *current_value.borrow_mut() = v
|
move |v| *current_value.borrow_mut() = v
|
||||||
},
|
})
|
||||||
);
|
.build();
|
||||||
|
|
||||||
(current_value, entry)
|
(current_value, entry)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,11 @@ You should have received a copy of the GNU General Public License along with Fit
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::App, components::DaySummary, types::DayInterval, view_models::DayDetailViewModel,
|
app::App,
|
||||||
|
components::{DateRangePicker, DaySummary},
|
||||||
|
types::DayInterval,
|
||||||
|
view_models::DayDetailViewModel,
|
||||||
};
|
};
|
||||||
|
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
@ -27,7 +29,6 @@ use std::{cell::RefCell, rc::Rc};
|
||||||
/// records.
|
/// records.
|
||||||
pub struct HistoricalViewPrivate {
|
pub struct HistoricalViewPrivate {
|
||||||
app: Rc<RefCell<Option<App>>>,
|
app: Rc<RefCell<Option<App>>>,
|
||||||
time_window: Rc<RefCell<DayInterval>>,
|
|
||||||
list_view: gtk::ListView,
|
list_view: gtk::ListView,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +49,6 @@ impl ObjectSubclass for HistoricalViewPrivate {
|
||||||
|
|
||||||
let s = Self {
|
let s = Self {
|
||||||
app: Rc::new(RefCell::new(None)),
|
app: Rc::new(RefCell::new(None)),
|
||||||
time_window: Rc::new(RefCell::new(DayInterval::default())),
|
|
||||||
list_view: gtk::ListView::builder()
|
list_view: gtk::ListView::builder()
|
||||||
.factory(&factory)
|
.factory(&factory)
|
||||||
.single_click_activate(true)
|
.single_click_activate(true)
|
||||||
|
@ -106,6 +106,12 @@ impl HistoricalView {
|
||||||
|
|
||||||
*s.imp().app.borrow_mut() = Some(app);
|
*s.imp().app.borrow_mut() = Some(app);
|
||||||
|
|
||||||
|
let date_range_picker = DateRangePicker::default();
|
||||||
|
date_range_picker.connect_on_search({
|
||||||
|
let s = s.clone();
|
||||||
|
move |interval| s.set_interval(interval)
|
||||||
|
});
|
||||||
|
|
||||||
let mut model = gio::ListStore::new::<Date>();
|
let mut model = gio::ListStore::new::<Date>();
|
||||||
model.extend(interval.days().map(Date::new));
|
model.extend(interval.days().map(Date::new));
|
||||||
s.imp()
|
s.imp()
|
||||||
|
@ -122,7 +128,15 @@ impl HistoricalView {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
s.append(&s.imp().list_view);
|
let scroller = gtk::ScrolledWindow::builder()
|
||||||
|
.child(&s.imp().list_view)
|
||||||
|
.hexpand(true)
|
||||||
|
.vexpand(true)
|
||||||
|
.hscrollbar_policy(gtk::PolicyType::Never)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
s.append(&date_range_picker);
|
||||||
|
s.append(&scroller);
|
||||||
|
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
@ -134,10 +148,6 @@ impl HistoricalView {
|
||||||
.list_view
|
.list_view
|
||||||
.set_model(Some(>k::NoSelection::new(Some(model))));
|
.set_model(Some(>k::NoSelection::new(Some(model))));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn time_window(&self) -> DayInterval {
|
|
||||||
self.imp().time_window.borrow().clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
Loading…
Reference in New Issue