diff options
Diffstat (limited to 'src/gui/form')
| -rw-r--r-- | src/gui/form/mod.rs | 238 | ||||
| -rw-r--r-- | src/gui/form/repetition.rs | 47 | 
2 files changed, 227 insertions, 58 deletions
| diff --git a/src/gui/form/mod.rs b/src/gui/form/mod.rs index 57ccac7..68e6539 100644 --- a/src/gui/form/mod.rs +++ b/src/gui/form/mod.rs @@ -2,8 +2,13 @@ mod repetition;  use gtk4 as gtk; +use anyhow::Result; +use chrono::{NaiveDate, NaiveTime};  use gtk::glib;  use gtk::prelude::*; +use rusqlite::Connection; +use thiserror::Error; +use uuid::Uuid;  use crate::{      db, @@ -11,12 +16,70 @@ use crate::{      model::{event, event::Event},  }; -pub async fn show(app: &App, event: Event, is_new: bool) { +pub async fn repetition_dialog(app: &App, date: NaiveDate, event: Event) {      let dialog = gtk::Dialog::builder()          .transient_for(&*app.window)          .modal(true) -        .title(if is_new { "Ajouter" } else { "Modifier" }) -        .css_classes(vec!["g-Form".to_string()]) +        .title("Modifier") +        .css_classes(vec!["g-Dialog".to_string()]) +        .build(); + +    let content_area = dialog.content_area(); + +    let lines = gtk::Box::builder() +        .orientation(gtk::Orientation::Vertical) +        .build(); +    content_area.append(&lines); + +    let button = gtk::Button::builder() +        .label("Cette occurence") +        .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::ShowUpdateRepetitionForm { date, event: event.clone() }); +        dialog.close() +    })); + +    let button = gtk::Button::builder() +        .label("Toutes les occurences") +        .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() }); +        dialog.close() +    })); + +    dialog.run_future().await; +} + +#[derive(Clone)] +pub enum Target { +    New { date: NaiveDate }, +    Update { event: Event }, +    UpdateRepetition { event: Event, date: NaiveDate }, +} + +pub async fn show(app: &App, target: Target) { +    let event = match target { +        Target::New { .. } => None, +        Target::Update { ref event } => Some(event.clone()), +        Target::UpdateRepetition { ref event, .. } => Some(event.clone()), +    }; + +    let title = if event.is_some() { +        "Modifier" +    } else { +        "Ajouter" +    }; + +    let dialog = gtk::Dialog::builder() +        .transient_for(&*app.window) +        .modal(true) +        .title(title) +        .css_classes(vec!["g-Dialog".to_string()])          .build();      let content_area = dialog.content_area(); @@ -29,7 +92,6 @@ pub async fn show(app: &App, event: Event, is_new: bool) {      let columns = gtk::Box::builder()          .orientation(gtk::Orientation::Horizontal)          .build(); -    columns.add_css_class("g-Form__Columns");      lines.append(&columns);      // First column @@ -37,79 +99,145 @@ pub async fn show(app: &App, event: Event, is_new: bool) {      let column1 = gtk::Box::builder()          .orientation(gtk::Orientation::Vertical)          .build(); -    column1.add_css_class("g-Form__Inputs");      columns.append(&column1); -    let name = entry(&event.name); +    let name = event.as_ref().map(|e| entry(&e.name)).unwrap_or_default();      column1.append(&label("Événement"));      column1.append(&name); -    let date = entry(&event.date.format(event::DATE_FORMAT).to_string()); +    let date = match target { +        Target::New { date } => date, +        Target::Update { ref event } => event.date, +        Target::UpdateRepetition { date, .. } => date, +    }; +    let date = entry(&date.format(event::DATE_FORMAT).to_string());      column1.append(&label("Jour"));      column1.append(&date); -    let start = entry( -        &event -            .start -            .map(event::pprint_time) -            .unwrap_or_else(|| "".to_string()), -    ); +    let start = event +        .as_ref() +        .map(|e| time_entry(e.start)) +        .unwrap_or_else(|| entry(""));      column1.append(&label("Début"));      column1.append(&start); -    let end = entry( -        &event -            .end -            .map(event::pprint_time) -            .unwrap_or_else(|| "".to_string()), -    ); +    let end = event +        .as_ref() +        .map(|e| time_entry(e.end)) +        .unwrap_or_else(|| entry(""));      column1.append(&label("Fin"));      column1.append(&end);      // Second column -    let repetition_model = repetition::view(&event); +    let repetition = match target { +        Target::Update { ref event } => event.repetition.as_ref(), +        _ => None, +    }; +    let repetition_model = repetition::view(repetition);      columns.append(&repetition_model.view);      // Buttons +    let button_title = match target { +        Target::New { .. } => "Créer", +        Target::Update { .. } => "Modifier", +        Target::UpdateRepetition { .. } => "Modifier l’occurence", +    }; +      let button = gtk::Button::builder() -        .label(if is_new { "Créer" } else { "Modifier" }) +        .label(button_title)          .margin_bottom(10)          .build();      lines.append(&button);      let conn = app.conn.clone();      let tx = app.tx.clone(); -    button.connect_clicked(glib::clone!(@weak dialog, @strong event => move |_| { +    button.connect_clicked(glib::clone!(@weak dialog, @strong target, @strong event => move |_| {          match repetition::validate(&repetition_model) {              Ok(repetition) => { -                match event::validate(event.id, date.buffer().text(), name.buffer().text(), start.buffer().text(), end.buffer().text(), repetition) { +                let id = match &target { +                    Target::Update {event} => event.id, +                    _ => Uuid::new_v4(), +                }; +                match event::validate(id, date.buffer().text(), name.buffer().text(), start.buffer().text(), end.buffer().text(), repetition) {                      Some(new) => { -                        match if is_new { db::insert(&conn, &new) } else { db::update(&conn, &new) } { -                            Ok(_) => { -                                let msg = if is_new { Msg::AddEvent { new } } else { Msg::UpdateEvent { old: event.clone(), new } }; -                                update::send(tx.clone(), msg); -                                dialog.close() -                            }, -                            Err(err) => eprintln!("Error when upserting event: {err}") +                        match &target { +                            Target::New {..} => { +                                match db::insert(&conn, &new) { +                                    Ok(_) => { +                                        update::send(tx.clone(), Msg::AddEvent { new }); +                                        dialog.close() +                                    }, +                                    Err(err) => eprintln!("Error when inserting event: {}", err) +                                } +                            } +                            Target::Update {event} => { +                                match db::update(&conn, &new) { +                                    Ok(_) => { +                                        update::send(tx.clone(), Msg::UpdateEvent { old: event.clone(), new }); +                                        dialog.close() +                                    }, +                                    Err(err) => eprintln!("Error when updating event: {}", err) +                                } +                            } +                            Target::UpdateRepetition { event, date } => { +                                // TODO: improve intermediate error state +                                match delete_repetition_occurence(&conn, event, *date) { +                                    Ok(occurence) => { +                                        match db::insert(&conn, &new) { +                                            Ok(_) => { +                                                update::send(tx.clone(), Msg::UpdateEventOccurence { +                                                    event: event.clone(), +                                                    occurence, +                                                    date: *date, +                                                    new +                                                }) +                                            } +                                            Err(err) => eprintln!("Error when updating repetition: {}", err) +                                        }; +                                        dialog.close() +                                    }, +                                    Err(err) => eprintln!("Error when updating repetition: {}", err) +                                } +                            }                          }                      } -                    None => eprintln!("Event is not valid: {event:?}") +                    None => eprintln!("Event is not valid.")                  }              }, -            Err(message) => eprintln!("{message}") +            Err(message) => eprintln!("{}", message)          }      })); -    if !is_new { -        let button = gtk::Button::builder().label("Supprimer").build(); +    if let Some(event) = event { +        let label = match target { +            Target::Update { .. } => "Supprimer", +            _ => "Supprimer l’occurence", +        }; +        let button = gtk::Button::builder().label(label).build();          lines.append(&button);          let conn = app.conn.clone();          let tx = app.tx.clone();          button.connect_clicked(glib::clone!(@weak dialog => move |_| { -            if db::delete(&conn, &event.id).is_ok() { -                update::send(tx.clone(), Msg::DeleteEvent { event: event.clone() }); -                dialog.close() +            match target { +                Target::UpdateRepetition { date, .. } => { +                    match delete_repetition_occurence(&conn, &event, date) { +                        Ok(occurence) => { +                            update::send(tx.clone(), Msg::DeleteOccurence { event: event.clone(), date, occurence }); +                            dialog.close() +                        } +                        Err(err) => { +                            eprintln!("{:?}", err); +                        } +                    } +                } +                _ => { +                    let operation = db::delete(&conn, &event.id); +                    if operation.is_ok() { +                        update::send(tx.clone(), Msg::DeleteEvent { event: event.clone() }); +                        dialog.close() +                    } +                }              }          }));      } @@ -117,6 +245,42 @@ pub async fn show(app: &App, event: Event, is_new: bool) {      dialog.run_future().await;  } +#[derive(Error, Debug)] +enum DeleteError { +    #[error("Repetition not found")] +    RepetitionNotFound, +    #[error("Occurence not found")] +    OccurenceNotFound, +} + +fn delete_repetition_occurence( +    conn: &Connection, +    event: &Event, +    occurence_date: NaiveDate, +) -> Result<usize> { +    if let Some(ref repetition) = event.repetition { +        if let Some(occurence) = repetition.occurence_index(event.date, occurence_date) { +            let mut event = event.clone(); +            let mut repetition = repetition.clone(); +            repetition.removed_occurences.insert(occurence); +            event.repetition = Some(repetition); +            db::update(conn, &event).map(|_| occurence) +        } else { +            Err(anyhow::Error::new(DeleteError::OccurenceNotFound)) +        } +    } else { +        Err(anyhow::Error::new(DeleteError::RepetitionNotFound)) +    } +} + +fn time_entry(time: Option<NaiveTime>) -> 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()  } diff --git a/src/gui/form/repetition.rs b/src/gui/form/repetition.rs index accb091..1d36765 100644 --- a/src/gui/form/repetition.rs +++ b/src/gui/form/repetition.rs @@ -2,13 +2,11 @@ use gtk4 as gtk;  use chrono::{Weekday, Weekday::*};  use gtk::prelude::*; +use std::collections::HashSet; -use crate::{ -    model::event::Event, -    model::{ -        repetition, -        repetition::{DayOfMonth, Repetition}, -    }, +use crate::model::{ +    repetition, +    repetition::{DayOfMonth, Frequency, Repetition},  };  static WEEKDAYS_STR: [&str; 7] = [ @@ -29,7 +27,7 @@ pub struct Model {      yearly_radio: gtk::CheckButton,  } -pub fn view(event: &Event) -> Model { +pub fn view(repetition: Option<&Repetition>) -> Model {      let view = gtk::Box::builder()          .orientation(gtk::Orientation::Vertical)          .build(); @@ -39,12 +37,14 @@ pub fn view(event: &Event) -> Model {      let no_radio = gtk::CheckButton::builder()          .label("Non") -        .active(event.repetition.is_none()) +        .active(repetition.is_none())          .build();      view.append(&no_radio); -    let default = match event.repetition { -        Some(Repetition::Daily { period }) => period.to_string(), +    let frequency = repetition.as_ref().map(|r| r.frequency.clone()); + +    let default = match frequency { +        Some(Frequency::Daily { period }) => period.to_string(),          _ => "".to_string(),      };      let day_interval_entry = gtk::Entry::builder().text(&default).build(); @@ -56,8 +56,8 @@ pub fn view(event: &Event) -> Model {      );      view.append(&day_interval_box); -    let default = match event.repetition { -        Some(Repetition::Monthly { +    let default = match frequency { +        Some(Frequency::Monthly {              day: DayOfMonth::Day { day },          }) => day.to_string(),          _ => "".to_string(), @@ -67,8 +67,8 @@ pub fn view(event: &Event) -> Model {          radio_input(&no_radio, !default.is_empty(), &monthly_entry, "Mensuel");      view.append(&monthly_box); -    let (active, default) = match event.repetition { -        Some(Repetition::Monthly { +    let (active, default) = match frequency { +        Some(Frequency::Monthly {              day: DayOfMonth::Weekday { weekday },          }) => (true, weekday),          _ => (false, Mon), @@ -83,7 +83,7 @@ pub fn view(event: &Event) -> Model {      let yearly_radio = gtk::CheckButton::builder()          .group(&no_radio)          .label("Annuel") -        .active(event.repetition == Some(Repetition::Yearly)) +        .active(frequency == Some(Frequency::Yearly))          .build();      view.append(&yearly_radio); @@ -127,24 +127,29 @@ fn label(text: &str) -> gtk::Label {  }  pub fn validate(model: &Model) -> Result<Option<Repetition>, String> { -    if model.no_radio.is_active() { +    let frequency = if model.no_radio.is_active() {          Ok(None)      } else if model.day_interval_radio.is_active() {          let period = repetition::validate_period(&model.day_interval_entry.buffer().text())?; -        Ok(Some(Repetition::Daily { period })) +        Ok(Some(Frequency::Daily { period }))      } else if model.monthly_radio.is_active() {          let day = repetition::validate_day(&model.monthly_entry.buffer().text())?; -        Ok(Some(Repetition::Monthly { +        Ok(Some(Frequency::Monthly {              day: DayOfMonth::Day { day },          }))      } else if model.first_day_radio.is_active() {          let weekday = WEEKDAYS[model.first_day_dropdown.selected() as usize]; -        Ok(Some(Repetition::Monthly { +        Ok(Some(Frequency::Monthly {              day: DayOfMonth::Weekday { weekday },          }))      } else if model.yearly_radio.is_active() { -        Ok(Some(Repetition::Yearly)) +        Ok(Some(Frequency::Yearly))      } else {          Err("Aucune option n’a été sélectionnée".to_string()) -    } +    }?; + +    Ok(frequency.map(|frequency| Repetition { +        frequency, +        removed_occurences: HashSet::new(), +    }))  } | 
