mod repetition; mod utils; use gtk4 as gtk; use anyhow::Result; 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::{category::Category, event, event::Event, repetition::Repetition}, }; pub async fn repetition_dialog(app: &App, date: NaiveDate, event: &Event) { let dialog = gtk::Dialog::builder() .transient_for(&*app.window) .modal(true) .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_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_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() } )); dialog.run_future().await; } #[derive(Clone)] 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) { let event = match 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() { "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(); let lines = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .build(); content_area.append(&lines); let columns = gtk::Box::builder() .orientation(gtk::Orientation::Horizontal) .spacing(10) .build(); lines.append(&columns); // First column let column1 = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) .build(); columns.append(&column1); 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 = utils::entry(&date.format(event::DATE_FORMAT).to_string()); column1.append(&utils::label("Jour")); column1.append(&date); let start = event .as_ref() .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| utils::time_entry(e.end)) .unwrap_or_else(|| utils::entry("")); column1.append(&utils::label("Fin")); column1.append(&end); let dropdown_categories = get_dropdown_categories(&app.categories); let category_dropdown = gtk::DropDown::from_strings( &dropdown_categories .iter() .map(|s| s.as_str()) .collect::>(), ); category_dropdown.set_margin_bottom(10); let selected = get_selected_category(&event, &app.categories).unwrap_or_else(|| "".to_string()); category_dropdown.set_selected( dropdown_categories .iter() .position(|d| d == &selected) .unwrap_or(0) as u32, ); column1.append(&utils::label("Catégorie")); column1.append(&category_dropdown); // 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); columns.append(&repetition_model.view); // Buttons let button_title = match 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() .label(button_title) .margin_bottom(10) .build(); lines.append(&button); let conn = app.conn.clone(); let tx = app.tx.clone(); let categories = app.categories.clone(); button.connect_clicked(glib::clone!( #[weak] dialog, #[strong] target, #[strong] categories, move |_| { 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, _ => Uuid::new_v4(), }; // Find category id from selected id let category = categories .iter() .find(|c| { c.name == dropdown_categories[category_dropdown.selected() as usize] }) .map(|c| c.id); match event::validate( id, date.buffer().text().to_string(), name.buffer().text().to_string(), start.buffer().text().to_string(), end.buffer().text().to_string(), repetition, category, ) { Some(new) => { match &target { Target::New { .. } => match db::events::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::events::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::events::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) } } } Target::UpdateFromOccurence { date, event } => { match update_repetition_until( &conn, *date - Duration::days(1), event, ) { Ok(updated) => match db::events::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."), } } Err(message) => eprintln!("{}", message), } } )); 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(); lines.append(&button); let conn = app.conn.clone(); let tx = app.tx.clone(); button.connect_clicked(glib::clone!( #[weak] dialog, move |_| { 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); } } } 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::events::delete(&conn, &event.id); if operation.is_ok() { update::send( tx.clone(), Msg::DeleteEvent { event: event.clone(), }, ); dialog.close() } } } } )); } dialog.run_future().await; } fn get_dropdown_categories(categories: &[Category]) -> Vec { let mut xs = categories .iter() .map(|c| c.name.clone()) .collect::>(); xs.push("".to_string()); xs.sort(); xs } fn get_selected_category(event: &Option, categories: &[Category]) -> Option { let id = event.as_ref()?.category?; let category = categories.iter().find(|c| c.id == id)?; Some(category.name.clone()) } #[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 { 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::events::update(conn, &event).map(|_| occurence) } else { Err(anyhow::Error::new(DeleteError::OccurenceNotFound)) } } else { Err(anyhow::Error::new(DeleteError::RepetitionNotFound)) } } 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::events::update(conn, &with_repetition_until)?; Ok(with_repetition_until) }