From 35cc74578e969bae4812afd2ff041eba3746142d Mon Sep 17 00:00:00 2001 From: Joris Date: Sat, 19 Mar 2022 22:03:58 +0100 Subject: Allow to repeat an event until a specific date Also provide a shortcut to modify a repetead event from a specific occurence. This set the end repetition date under the hood and create a new repeated event. --- src/gui/app.rs | 4 +- src/gui/calendar.rs | 4 +- src/gui/form/mod.rs | 115 +++++++++++++++++++++++++++++++-------------- src/gui/form/repetition.rs | 40 ++++++++++++++-- src/gui/form/utils.rs | 21 +++++++++ src/gui/update.rs | 83 ++++++++++++++++++++++++++++---- 6 files changed, 216 insertions(+), 51 deletions(-) create mode 100644 src/gui/form/utils.rs (limited to 'src/gui') diff --git a/src/gui/app.rs b/src/gui/app.rs index 9f37301..8cd7096 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -16,8 +16,8 @@ pub struct App { pub conn: Rc, pub window: Rc, pub grid: gtk::Grid, - pub events: Vec, - pub recurring_events: Vec, + pub events: Vec, // TODO: use Hashmap to have fast access to events by id ? + pub recurring_events: Vec, // TODO: use Hashmap to have fast access to events by id ? pub today: NaiveDate, pub start_date: NaiveDate, pub end_date: NaiveDate, diff --git a/src/gui/calendar.rs b/src/gui/calendar.rs index cad2465..547c087 100644 --- a/src/gui/calendar.rs +++ b/src/gui/calendar.rs @@ -181,9 +181,9 @@ fn day_events(date: NaiveDate, tx: Sender, events: Vec<&Event>) -> gtk::Box gesture.set_state(gtk::EventSequenceState::Claimed); if n == 2 { if event.repetition.is_some() { - update::send(tx.clone(), Msg::ShowRepetitionDialog { date, event: event.clone() }); + update::send(tx.clone(), Msg::ShowRepetitionDialog { date, event_id: event.id }); } else { - update::send(tx.clone(), Msg::ShowUpdateForm { event: event.clone() }); + update::send(tx.clone(), Msg::ShowUpdateForm { event_id: event.id }); } } }), diff --git a/src/gui/form/mod.rs b/src/gui/form/mod.rs index 68e6539..bb43ef5 100644 --- a/src/gui/form/mod.rs +++ b/src/gui/form/mod.rs @@ -1,22 +1,24 @@ mod repetition; +mod utils; use gtk4 as gtk; use anyhow::Result; -use chrono::{NaiveDate, NaiveTime}; +use chrono::{Duration, NaiveDate}; use gtk::glib; use gtk::prelude::*; use rusqlite::Connection; +use std::collections::HashSet; use thiserror::Error; use uuid::Uuid; use crate::{ db, gui::{update, update::Msg, App}, - model::{event, event::Event}, + model::{event, event::Event, repetition::Repetition}, }; -pub async fn repetition_dialog(app: &App, date: NaiveDate, event: Event) { +pub async fn repetition_dialog(app: &App, date: NaiveDate, event: &Event) { let dialog = gtk::Dialog::builder() .transient_for(&*app.window) .modal(true) @@ -38,17 +40,28 @@ pub async fn repetition_dialog(app: &App, date: NaiveDate, event: Event) { lines.append(&button); let tx = app.tx.clone(); button.connect_clicked(glib::clone!(@weak dialog, @strong event => move |_| { - update::send(tx.clone(), Msg::ShowUpdateRepetitionForm { date, event: event.clone() }); + update::send(tx.clone(), Msg::ShowUpdateRepetitionForm { date, event_id: event.id }); dialog.close() })); let button = gtk::Button::builder() .label("Toutes les occurences") + .margin_bottom(10) .build(); lines.append(&button); let tx = app.tx.clone(); button.connect_clicked(glib::clone!(@weak dialog, @strong event => move |_| { - update::send(tx.clone(), Msg::ShowUpdateForm { event: event.clone() }); + update::send(tx.clone(), Msg::ShowUpdateForm { event_id: event.id }); + dialog.close() + })); + + let button = gtk::Button::builder() + .label("À partir de cette occurence") + .build(); + lines.append(&button); + let tx = app.tx.clone(); + button.connect_clicked(glib::clone!(@weak dialog, @strong event => move |_| { + update::send(tx.clone(), Msg::ShowUpdateFromOccurence { date, event_id: event.id }); dialog.close() })); @@ -60,6 +73,7 @@ pub enum Target { New { date: NaiveDate }, Update { event: Event }, UpdateRepetition { event: Event, date: NaiveDate }, + UpdateFromOccurence { event: Event, date: NaiveDate }, } pub async fn show(app: &App, target: Target) { @@ -67,6 +81,7 @@ pub async fn show(app: &App, target: Target) { Target::New { .. } => None, Target::Update { ref event } => Some(event.clone()), Target::UpdateRepetition { ref event, .. } => Some(event.clone()), + Target::UpdateFromOccurence { ref event, .. } => Some(event.clone()), }; let title = if event.is_some() { @@ -91,6 +106,7 @@ pub async fn show(app: &App, target: Target) { let columns = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) + .spacing(10) .build(); lines.append(&columns); @@ -101,37 +117,42 @@ pub async fn show(app: &App, target: Target) { .build(); columns.append(&column1); - let name = event.as_ref().map(|e| entry(&e.name)).unwrap_or_default(); - column1.append(&label("Événement")); + let name = event + .as_ref() + .map(|e| utils::entry(&e.name)) + .unwrap_or_default(); + column1.append(&utils::label("Événement")); column1.append(&name); let date = match target { Target::New { date } => date, Target::Update { ref event } => event.date, Target::UpdateRepetition { date, .. } => date, + Target::UpdateFromOccurence { date, .. } => date, }; - let date = entry(&date.format(event::DATE_FORMAT).to_string()); - column1.append(&label("Jour")); + let date = utils::entry(&date.format(event::DATE_FORMAT).to_string()); + column1.append(&utils::label("Jour")); column1.append(&date); let start = event .as_ref() - .map(|e| time_entry(e.start)) - .unwrap_or_else(|| entry("")); - column1.append(&label("Début")); + .map(|e| utils::time_entry(e.start)) + .unwrap_or_else(|| utils::entry("")); + column1.append(&utils::label("Début")); column1.append(&start); let end = event .as_ref() - .map(|e| time_entry(e.end)) - .unwrap_or_else(|| entry("")); - column1.append(&label("Fin")); + .map(|e| utils::time_entry(e.end)) + .unwrap_or_else(|| utils::entry("")); + column1.append(&utils::label("Fin")); column1.append(&end); // Second column let repetition = match target { Target::Update { ref event } => event.repetition.as_ref(), + Target::UpdateFromOccurence { ref event, .. } => event.repetition.as_ref(), _ => None, }; let repetition_model = repetition::view(repetition); @@ -143,6 +164,7 @@ pub async fn show(app: &App, target: Target) { Target::New { .. } => "Créer", Target::Update { .. } => "Modifier", Target::UpdateRepetition { .. } => "Modifier l’occurence", + Target::UpdateFromOccurence { .. } => "Modifier à partir de l’occurence", }; let button = gtk::Button::builder() @@ -153,7 +175,13 @@ pub async fn show(app: &App, target: Target) { let conn = app.conn.clone(); let tx = app.tx.clone(); button.connect_clicked(glib::clone!(@weak dialog, @strong target, @strong event => move |_| { - match repetition::validate(&repetition_model) { + let removed_occurences = match &target { + Target::Update { event, .. } => { + event.repetition.as_ref().map(|r| r.removed_occurences.clone()).unwrap_or_default() + }, + _ => HashSet::new(), + }; + match repetition::validate(&repetition_model, removed_occurences) { Ok(repetition) => { let id = match &target { Target::Update {event} => event.id, @@ -200,6 +228,24 @@ pub async fn show(app: &App, target: Target) { Err(err) => eprintln!("Error when updating repetition: {}", err) } } + Target::UpdateFromOccurence { date, event } => { + match update_repetition_until(&conn, *date - Duration::days(1), event) { + Ok(updated) => { + match db::insert(&conn, &new) { + Ok(_) => { + update::send(tx.clone(), Msg::UpdateRepeatedFrom { + old: event.clone(), + updated, + new + }); + dialog.close() + }, + Err(err) => eprintln!("Error when inserting event: {}", err) + } + }, + Err(err) => eprintln!("Error when updating event: {}", err) + } + } } } None => eprintln!("Event is not valid.") @@ -212,6 +258,7 @@ pub async fn show(app: &App, target: Target) { if let Some(event) = event { let label = match target { Target::Update { .. } => "Supprimer", + Target::UpdateFromOccurence { .. } => "Supprimer à partir de l’occurence", _ => "Supprimer l’occurence", }; let button = gtk::Button::builder().label(label).build(); @@ -231,6 +278,15 @@ pub async fn show(app: &App, target: Target) { } } } + Target::UpdateFromOccurence { date, .. } => { + match update_repetition_until(&conn, date - Duration::days(1), &event) { + Ok(updated) => { + update::send(tx.clone(), Msg::UpdateEvent { old: event.clone(), new: updated }); + dialog.close() + }, + Err(err) => eprintln!("Error when updating event: {}", err) + } + } _ => { let operation = db::delete(&conn, &event.id); if operation.is_ok() { @@ -273,22 +329,13 @@ fn delete_repetition_occurence( } } -fn time_entry(time: Option) -> gtk::Entry { - entry( - &time - .map(event::pprint_time) - .unwrap_or_else(|| "".to_string()), - ) -} - -fn entry(text: &str) -> gtk::Entry { - gtk::Entry::builder().text(text).margin_bottom(10).build() -} - -fn label(text: &str) -> gtk::Label { - gtk::Label::builder() - .label(text) - .halign(gtk::Align::Start) - .margin_bottom(5) - .build() +fn update_repetition_until(conn: &Connection, date: NaiveDate, event: &Event) -> Result { + let mut with_repetition_until = event.clone(); + with_repetition_until.repetition = event.repetition.as_ref().map(|r| Repetition { + frequency: r.frequency.clone(), + removed_occurences: r.removed_occurences.clone(), + until: Some(date), + }); + db::update(conn, &with_repetition_until)?; + Ok(with_repetition_until) } diff --git a/src/gui/form/repetition.rs b/src/gui/form/repetition.rs index 1d36765..4da65ac 100644 --- a/src/gui/form/repetition.rs +++ b/src/gui/form/repetition.rs @@ -1,13 +1,15 @@ use gtk4 as gtk; -use chrono::{Weekday, Weekday::*}; +use chrono::{NaiveDate, Weekday, Weekday::*}; use gtk::prelude::*; use std::collections::HashSet; +use crate::gui::form::utils; use crate::model::{ - repetition, + event, repetition, repetition::{DayOfMonth, Frequency, Repetition}, }; +use crate::validation; static WEEKDAYS_STR: [&str; 7] = [ "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche", @@ -25,6 +27,7 @@ pub struct Model { first_day_radio: gtk::CheckButton, first_day_dropdown: gtk::DropDown, yearly_radio: gtk::CheckButton, + until: gtk::Entry, } pub fn view(repetition: Option<&Repetition>) -> Model { @@ -52,7 +55,7 @@ pub fn view(repetition: Option<&Repetition>) -> Model { &no_radio, !default.is_empty(), &day_interval_entry, - "Interval de jours", + "Intervalle de jours", ); view.append(&day_interval_box); @@ -84,9 +87,18 @@ pub fn view(repetition: Option<&Repetition>) -> Model { .group(&no_radio) .label("Annuel") .active(frequency == Some(Frequency::Yearly)) + .margin_bottom(10) .build(); view.append(&yearly_radio); + let until = repetition + .as_ref() + .and_then(|r| r.until) + .map(|u| utils::entry(&u.format(event::DATE_FORMAT).to_string())) + .unwrap_or_default(); + view.append(&utils::label("Répéter jusqu’au")); + view.append(&until); + Model { view, no_radio, @@ -97,6 +109,7 @@ pub fn view(repetition: Option<&Repetition>) -> Model { first_day_radio, first_day_dropdown, yearly_radio, + until, } } @@ -126,7 +139,10 @@ fn label(text: &str) -> gtk::Label { .build() } -pub fn validate(model: &Model) -> Result, String> { +pub fn validate( + model: &Model, + removed_occurences: HashSet, +) -> Result, String> { let frequency = if model.no_radio.is_active() { Ok(None) } else if model.day_interval_radio.is_active() { @@ -148,8 +164,22 @@ pub fn validate(model: &Model) -> Result, String> { Err("Aucune option n’a été sélectionnée".to_string()) }?; + // Check until + let until = (if frequency.is_some() { + match validation::non_empty(model.until.buffer().text()) { + Some(until) => match NaiveDate::parse_from_str(&until, event::DATE_FORMAT) { + Ok(until) => Ok(Some(until)), + Err(_) => Err(format!("Can’t parse date from {}", until)), + }, + None => Ok(None), + } + } else { + Ok(None) + })?; + Ok(frequency.map(|frequency| Repetition { frequency, - removed_occurences: HashSet::new(), + removed_occurences, + until, })) } diff --git a/src/gui/form/utils.rs b/src/gui/form/utils.rs new file mode 100644 index 0000000..5cf59e3 --- /dev/null +++ b/src/gui/form/utils.rs @@ -0,0 +1,21 @@ +use gtk4 as gtk; + +use chrono::NaiveTime; + +use crate::model::time; + +pub fn time_entry(time: Option) -> gtk::Entry { + entry(&time.map(time::pprint).unwrap_or_else(|| "".to_string())) +} + +pub fn entry(text: &str) -> gtk::Entry { + gtk::Entry::builder().text(text).margin_bottom(10).build() +} + +pub fn label(text: &str) -> gtk::Label { + gtk::Label::builder() + .label(text) + .halign(gtk::Align::Start) + .margin_bottom(5) + .build() +} diff --git a/src/gui/update.rs b/src/gui/update.rs index 372fb24..bd4e7a9 100644 --- a/src/gui/update.rs +++ b/src/gui/update.rs @@ -3,6 +3,7 @@ use chrono::{Duration, NaiveDate}; use gtk4::prelude::GridExt; use std::collections::HashSet; use std::iter::FromIterator; +use uuid::Uuid; use crate::{ db, @@ -21,15 +22,19 @@ pub enum Msg { date: NaiveDate, }, ShowRepetitionDialog { + event_id: Uuid, date: NaiveDate, - event: Event, }, ShowUpdateForm { - event: Event, + event_id: Uuid, }, ShowUpdateRepetitionForm { + event_id: Uuid, + date: NaiveDate, + }, + ShowUpdateFromOccurence { + event_id: Uuid, date: NaiveDate, - event: Event, }, AddEvent { new: Event, @@ -44,6 +49,11 @@ pub enum Msg { date: NaiveDate, new: Event, }, + UpdateRepeatedFrom { + old: Event, + updated: Event, + new: Event, + }, DeleteEvent { event: Event, }, @@ -60,12 +70,62 @@ pub async fn event_handler(rx: Receiver, mut app: App) { while let Ok(msg) = rx.recv().await { match msg { Msg::ShowAddForm { date } => form::show(&app, form::Target::New { date }).await, - Msg::ShowRepetitionDialog { date, event } => { - form::repetition_dialog(&app, date, event).await + Msg::ShowRepetitionDialog { date, event_id } => { + match app.recurring_events.iter().position(|e| e.id == event_id) { + Some(index) => { + form::repetition_dialog(&app, date, &app.recurring_events[index]).await + } + None => eprintln!("Event not found with id: {}", event_id), + } } - Msg::ShowUpdateForm { event } => form::show(&app, form::Target::Update { event }).await, - Msg::ShowUpdateRepetitionForm { date, event } => { - form::show(&app, form::Target::UpdateRepetition { event, date }).await + Msg::ShowUpdateForm { event_id } => { + match app.recurring_events.iter().position(|e| e.id == event_id) { + Some(index) => { + form::show( + &app, + form::Target::Update { + event: app.recurring_events[index].clone(), + }, + ) + .await + } + None => match app.events.iter().position(|e| e.id == event_id) { + Some(index) => { + form::show( + &app, + form::Target::Update { + event: app.events[index].clone(), + }, + ) + .await + } + None => eprintln!("Event not found with id: {}", event_id), + }, + } + } + Msg::ShowUpdateRepetitionForm { event_id, date } => { + match app.recurring_events.iter().position(|e| e.id == event_id) { + Some(index) => { + form::show( + &app, + form::Target::UpdateRepetition { + date, + event: app.recurring_events[index].clone(), + }, + ) + .await + } + None => eprintln!("Event not found with id: {}", event_id), + } + } + Msg::ShowUpdateFromOccurence { event_id, date } => { + match app.recurring_events.iter().position(|e| e.id == event_id) { + Some(index) => { + let event = app.recurring_events[index].clone(); + form::show(&app, form::Target::UpdateFromOccurence { date, event }).await + } + None => eprintln!("Event not found with id: {}", event_id), + } } Msg::AddEvent { new } => { let refresh_dates = add(&mut app, &new); @@ -87,6 +147,12 @@ pub async fn event_handler(rx: Receiver, mut app: App) { refresh_dates.insert(date); refresh(&app, &refresh_dates) } + Msg::UpdateRepeatedFrom { old, updated, new } => { + let mut refresh_dates = remove(&mut app, &old); + refresh_dates.extend(add(&mut app, &updated)); + refresh_dates.extend(add(&mut app, &new)); + refresh(&app, &refresh_dates); + } Msg::DeleteEvent { event } => { let refresh_dates = remove(&mut app, &event); refresh(&app, &refresh_dates) @@ -158,6 +224,7 @@ fn remove(app: &mut App, event: &Event) -> HashSet { } /// Remove event repetition +/// TODO: Completely remove the event if it’s the last remaining occurence fn remove_occurence(app: &mut App, event: &Event, occurence: usize) { match app.recurring_events.iter().position(|e| e.id == event.id) { Some(index) => { -- cgit v1.2.3