From 2d80413609130f1c121dcae39a150a27dd9f02ea Mon Sep 17 00:00:00 2001 From: Joris Date: Sat, 26 Feb 2022 18:35:24 +0100 Subject: Show repeated events --- src/app/app.rs | 21 +++++++++-- src/app/calendar.rs | 44 ++++++++++++++-------- src/app/form/mod.rs | 4 +- src/app/form/repetition.rs | 34 ++++++++--------- src/app/update.rs | 91 ++++++++++++++++++++++++++++++++++------------ 5 files changed, 134 insertions(+), 60 deletions(-) (limited to 'src/app') diff --git a/src/app/app.rs b/src/app/app.rs index b1ee395..58240af 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -1,7 +1,7 @@ use gtk4 as gtk; use async_channel::Sender; -use chrono::{Datelike, NaiveDate, Weekday}; +use chrono::{Datelike, Duration, NaiveDate, Weekday}; use gtk::glib::signal::Inhibit; use gtk::prelude::*; use rusqlite::Connection; @@ -16,8 +16,10 @@ pub struct App { pub window: Rc, pub grid: gtk::Grid, pub events: Vec, + pub repeated_events: Vec, pub today: NaiveDate, pub start_date: NaiveDate, + pub end_date: NaiveDate, pub tx: Sender, } @@ -36,8 +38,19 @@ impl App { let today = chrono::offset::Local::today().naive_utc(); let start_date = NaiveDate::from_isoywd(today.year(), today.iso_week().week(), Weekday::Mon); - let events = db::list(&conn).unwrap_or(vec![]); - let grid = calendar::create(tx.clone(), &today, &start_date, &events); + let end_date = start_date + Duration::days(7 * 4 - 1); + + let events = db::list_non_repeated_between(&conn, start_date, end_date).unwrap_or(vec![]); + let repeated_events = db::list_repeated(&conn).unwrap_or(vec![]); + + let grid = calendar::create( + tx.clone(), + today, + start_date, + end_date, + &events, + &repeated_events, + ); window.set_child(Some(&grid)); @@ -53,8 +66,10 @@ impl App { window, grid, events, + repeated_events, today, start_date, + end_date, tx, } } diff --git a/src/app/calendar.rs b/src/app/calendar.rs index fa4ebe6..11eb893 100644 --- a/src/app/calendar.rs +++ b/src/app/calendar.rs @@ -4,8 +4,9 @@ use async_channel::Sender; use chrono::{Datelike, NaiveDate}; use gtk::glib; use gtk::prelude::*; +use std::collections::HashMap; -use crate::{app::update, app::update::Msg, app::App, model::event::Event}; +use crate::{app::update, app::update::Msg, app::App, model::event, model::event::Event}; static DAYS: [&str; 7] = ["LUN", "MAR", "MER", "JEU", "VEN", "SAM", "DIM"]; static MONTHES: [&str; 12] = [ @@ -14,9 +15,11 @@ static MONTHES: [&str; 12] = [ pub fn create( tx: Sender, - today: &NaiveDate, - start_date: &NaiveDate, + today: NaiveDate, + start_date: NaiveDate, + end_date: NaiveDate, events: &Vec, + repeated_events: &Vec, ) -> gtk::Grid { let grid = gtk::Grid::builder().build(); @@ -24,7 +27,8 @@ pub fn create( grid.attach(&day_title(col), col, 0, 1, 1); } - attach_days(tx, &grid, &start_date, &today, &events); + let repetitions = event::repetitions_between(repeated_events, start_date, end_date); + attach_days(tx, &grid, start_date, today, events, &repetitions); grid } @@ -32,27 +36,34 @@ pub fn create( fn attach_days( tx: Sender, grid: >k::Grid, - start_date: &NaiveDate, - today: &NaiveDate, + start_date: NaiveDate, + today: NaiveDate, events: &Vec, + repetitions: &HashMap>, ) { - let mut d = *start_date; + let mut d = start_date; for row in 1..5 { for col in 0..7 { - grid.attach(&day_entry(tx.clone(), &d, &today, &events), col, row, 1, 1); + grid.attach( + &day_entry(tx.clone(), d, today, events, repetitions), + col, + row, + 1, + 1, + ); d = d.succ(); } } } -pub fn refresh_date(app: &App, date: NaiveDate) { +pub fn refresh_date(app: &App, date: NaiveDate, repetitions: &HashMap>) { let d = date.signed_duration_since(app.start_date).num_days(); let col = (d % 7) as i32; let row = 1 + (d / 7) as i32; app.grid.attach( - &day_entry(app.tx.clone(), &date, &app.today, &app.events), + &day_entry(app.tx.clone(), date, app.today, &app.events, repetitions), col, row, 1, @@ -76,9 +87,10 @@ fn day_title(col: i32) -> gtk::Box { pub fn day_entry( tx: Sender, - date: &NaiveDate, - today: &NaiveDate, + date: NaiveDate, + today: NaiveDate, events: &Vec, + repetitions: &HashMap>, ) -> gtk::ScrolledWindow { let vbox = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) @@ -87,7 +99,7 @@ pub fn day_entry( vbox.add_css_class("g-Calendar__Day"); let gesture = gtk::GestureClick::new(); - gesture.connect_pressed(glib::clone!(@strong date, @strong tx => move |_, n, _, _| { + gesture.connect_pressed(glib::clone!(@strong tx => move |_, n, _, _| { if n == 2 { update::send(tx.clone(), Msg::ShowAddForm { date }); } @@ -102,8 +114,10 @@ pub fn day_entry( let mut events = events .iter() - .filter(|e| e.date == *date) + .filter(|e| e.date == date) .collect::>(); + let repeated_events = repetitions.get(&date).map(|e| e.clone()).unwrap_or(vec![]); + events.extend(repeated_events.iter()); events.sort_by_key(|e| e.start); if !events.is_empty() { @@ -120,7 +134,7 @@ pub fn day_entry( scrolled_window } -fn day_label(date: &NaiveDate) -> gtk::Label { +fn day_label(date: NaiveDate) -> gtk::Label { let label = gtk::Label::builder() .label(&format!( "{} {}", diff --git a/src/app/form/mod.rs b/src/app/form/mod.rs index 5c60bc5..9cb6ba7 100644 --- a/src/app/form/mod.rs +++ b/src/app/form/mod.rs @@ -85,10 +85,10 @@ pub async fn show(app: &App, event: Event, is_new: bool) { update::send(tx.clone(), msg); dialog.close() }, - Err(err) => println!("Error when upserting event: {err}") + Err(err) => eprintln!("Error when upserting event: {err}") } }, - None => println!("Event is not valid: {event:?}") + None => eprintln!("Event is not valid: {event:?}") } })); diff --git a/src/app/form/repetition.rs b/src/app/form/repetition.rs index ac56479..87c8d84 100644 --- a/src/app/form/repetition.rs +++ b/src/app/form/repetition.rs @@ -7,7 +7,7 @@ use crate::{ model::event::Event, model::{ repetition, - repetition::{MonthFrequency, Repetition}, + repetition::{DayOfMonth, Repetition}, }, }; @@ -19,14 +19,14 @@ static WEEKDAYS: [Weekday; 7] = [Mon, Tue, Wed, Thu, Fri, Sat, Sun]; pub struct Model { pub view: gtk::Box, - pub no_radio: gtk::CheckButton, - pub day_interval_radio: gtk::CheckButton, - pub day_interval_entry: gtk::Entry, - pub monthly_radio: gtk::CheckButton, - pub monthly_entry: gtk::Entry, - pub first_day_radio: gtk::CheckButton, - pub first_day_dropdown: gtk::DropDown, - pub yearly_radio: gtk::CheckButton, + no_radio: gtk::CheckButton, + day_interval_radio: gtk::CheckButton, + day_interval_entry: gtk::Entry, + monthly_radio: gtk::CheckButton, + monthly_entry: gtk::Entry, + first_day_radio: gtk::CheckButton, + first_day_dropdown: gtk::DropDown, + yearly_radio: gtk::CheckButton, } pub fn view(event: &Event) -> Model { @@ -44,7 +44,7 @@ pub fn view(event: &Event) -> Model { view.append(&no_radio); let default = match event.repetition { - Some(Repetition::Daily { frequency }) => frequency.to_string(), + Some(Repetition::Daily { period }) => period.to_string(), _ => "".to_string(), }; let day_interval_entry = gtk::Entry::builder().text(&default).build(); @@ -58,7 +58,7 @@ pub fn view(event: &Event) -> Model { let default = match event.repetition { Some(Repetition::Monthly { - frequency: MonthFrequency::Day { day }, + day: DayOfMonth::Day { day }, }) => day.to_string(), _ => "".to_string(), }; @@ -69,8 +69,8 @@ pub fn view(event: &Event) -> Model { let (active, default) = match event.repetition { Some(Repetition::Monthly { - frequency: MonthFrequency::FirstDay { day }, - }) => (true, day), + day: DayOfMonth::Weekday { weekday }, + }) => (true, weekday), _ => (false, Mon), }; let first_day_dropdown = gtk::DropDown::from_strings(&WEEKDAYS_STR); @@ -131,17 +131,17 @@ pub fn validate(model: &Model) -> Option { None } else if model.day_interval_radio.is_active() { repetition::validate_day(&model.day_interval_entry.buffer().text()) - .map(|d| Repetition::Daily { frequency: d }) + .map(|d| Repetition::Daily { period: d }) } else if model.monthly_radio.is_active() { repetition::validate_day(&model.monthly_entry.buffer().text()).map(|d| { Repetition::Monthly { - frequency: MonthFrequency::Day { day: d }, + day: DayOfMonth::Day { day: d }, } }) } else if model.first_day_radio.is_active() { - let day = WEEKDAYS[model.first_day_dropdown.selected() as usize]; + let weekday = WEEKDAYS[model.first_day_dropdown.selected() as usize]; Some(Repetition::Monthly { - frequency: MonthFrequency::FirstDay { day }, + day: DayOfMonth::Weekday { weekday }, }) } else if model.yearly_radio.is_active() { Some(Repetition::Yearly) diff --git a/src/app/update.rs b/src/app/update.rs index 4e21050..4ef1eb1 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -1,5 +1,6 @@ use async_channel::{Receiver, Sender}; use chrono::NaiveDate; +use std::collections::HashSet; use crate::{ app::{calendar, form, utils, App}, @@ -30,31 +31,75 @@ pub async fn event_handler(rx: Receiver, mut app: App) { form::show(&app, event, false).await; } Msg::AddEvent { new } => { - let date = new.date.clone(); - app.events.push(new); - calendar::refresh_date(&app, date); + let refresh_dates = add(&mut app, &new); + refresh(&app, &refresh_dates) } Msg::UpdateEvent { old, new } => { - let new_date = new.date.clone(); - match app.events.iter().position(|e| e.id == new.id) { - Some(index) => { - app.events.remove(index); - app.events.push(new); - calendar::refresh_date(&app, new_date); - if old.date != new_date { - calendar::refresh_date(&app, old.date.clone()) - } - } - None => println!("Event not found when updating from {:?} to {:?}", old, new), - } - } - Msg::DeleteEvent { event } => match app.events.iter().position(|e| e.id == event.id) { - Some(index) => { - app.events.remove(index); - calendar::refresh_date(&app, event.date); - } - None => println!("Event not found when trying to delete {:?}", event), - }, + let refresh_dates_1 = remove(&mut app, &old); + let refresh_dates_2 = add(&mut app, &new); + refresh(&app, &refresh_dates_1.union(&refresh_dates_2).map(|d| *d).collect::>()) + } + Msg::DeleteEvent { event } => { + let refresh_dates = remove(&mut app, &event); + refresh(&app, &refresh_dates) + } + } + } +} + +/// Remove event and return dates that should be refreshed. +fn remove(app: &mut App, event: &Event) -> HashSet { + if event.repetition.is_some() { + match app.repeated_events.iter().position(|e| e.id == event.id) { + Some(index) => { + app.repeated_events.remove(index); + let mut dates = repetition_dates(&app, event); + dates.insert(event.date); + dates + } + None => { + eprintln!("Event not found when trying to delete {:?}", event); + HashSet::new() + } + } + } else { + match app.events.iter().position(|e| e.id == event.id) { + Some(index) => { + app.events.remove(index); + HashSet::from([event.date]) + } + None => { + eprintln!("Event not found when trying to delete {:?}", event); + HashSet::new() + } } } } + +/// Add event and return dates that should be refreshed. +fn add(app: &mut App, event: &Event) -> HashSet { + if event.repetition.is_some() { + app.repeated_events.push(event.clone()); + let mut dates = repetition_dates(&app, event); + dates.insert(event.date); + dates + } else { + app.events.push(event.clone()); + HashSet::from([event.date]) + } +} + +/// Repetition dates of a repetead event. +fn repetition_dates(app: &App, event: &Event) -> HashSet { + let event_reps = event::repetitions_between(&vec!(event.clone()), app.start_date, app.end_date); + HashSet::from_iter(event_reps.keys().map(|d| *d)) +} + +/// Refresh app for the given dates. +fn refresh(app: &App, dates: &HashSet) { + let repetitions = event::repetitions_between(&app.repeated_events, app.start_date, app.end_date); + + for date in dates { + calendar::refresh_date(app, *date, &repetitions) + } +} -- cgit v1.2.3