diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/form/mod.rs (renamed from src/app/form.rs) | 55 | ||||
| -rw-r--r-- | src/app/form/repetition.rs | 151 | ||||
| -rw-r--r-- | src/app/style.css | 4 | ||||
| -rw-r--r-- | src/app/update.rs | 14 | ||||
| -rw-r--r-- | src/db/migrations/1-init.sql | 1 | ||||
| -rw-r--r-- | src/db/mod.rs | 22 | ||||
| -rw-r--r-- | src/model/event.rs | 14 | ||||
| -rw-r--r-- | src/model/mod.rs | 1 | ||||
| -rw-r--r-- | src/model/repetition.rs | 26 | 
9 files changed, 258 insertions, 30 deletions
| diff --git a/src/app/form.rs b/src/app/form/mod.rs index 7f75db0..5c60bc5 100644 --- a/src/app/form.rs +++ b/src/app/form/mod.rs @@ -1,3 +1,5 @@ +mod repetition; +  use gtk4 as gtk;  use gtk::glib; @@ -19,19 +21,32 @@ pub async fn show(app: &App, event: Event, is_new: bool) {      let content_area = dialog.content_area(); -    let vbox = gtk::Box::builder() +    let lines = gtk::Box::builder() +        .orientation(gtk::Orientation::Vertical) +        .build(); +    content_area.append(&lines); + +    let columns = gtk::Box::builder() +        .orientation(gtk::Orientation::Horizontal) +        .build(); +    columns.add_css_class("g-Form__Columns"); +    lines.append(&columns); + +    // First column + +    let column1 = gtk::Box::builder()          .orientation(gtk::Orientation::Vertical)          .build(); -    vbox.add_css_class("g-Form__Inputs"); -    content_area.append(&vbox); +    column1.add_css_class("g-Form__Inputs"); +    columns.append(&column1);      let name = entry(&event.name); -    vbox.append(&label("Événement")); -    vbox.append(&name); +    column1.append(&label("Événement")); +    column1.append(&name);      let date = entry(&event.date.format(event::DATE_FORMAT).to_string()); -    vbox.append(&label("Jour")); -    vbox.append(&date); +    column1.append(&label("Jour")); +    column1.append(&date);      let start = entry(          &event @@ -39,22 +54,30 @@ pub async fn show(app: &App, event: Event, is_new: bool) {              .map(event::pprint_time)              .unwrap_or("".to_string()),      ); -    vbox.append(&label("Début")); -    vbox.append(&start); +    column1.append(&label("Début")); +    column1.append(&start);      let end = entry(&event.end.map(event::pprint_time).unwrap_or("".to_string())); -    vbox.append(&label("Fin")); -    vbox.append(&end); +    column1.append(&label("Fin")); +    column1.append(&end); + +    // Second column + +    let repetition_model = repetition::view(&event); +    columns.append(&repetition_model.view); + +    // Buttons      let button = gtk::Button::builder()          .label(if is_new { "Créer" } else { "Modifier" })          .margin_bottom(10)          .build(); -    vbox.append(&button); +    lines.append(&button);      let conn = app.conn.clone();      let tx = app.tx.clone();      button.connect_clicked(glib::clone!(@weak dialog, @strong event => move |_| { -        match event::validate(event.id, date.buffer().text(), name.buffer().text(), start.buffer().text(), end.buffer().text()) { +        let repetition = repetition::validate(&repetition_model).clone(); +        match event::validate(event.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(_) => { @@ -62,16 +85,16 @@ pub async fn show(app: &App, event: Event, is_new: bool) {                          update::send(tx.clone(), msg);                          dialog.close()                      }, -                    Err(_) => () +                    Err(err) => println!("Error when upserting event: {err}")                  }              }, -            None => () +            None => println!("Event is not valid: {event:?}")          }      }));      if !is_new {          let button = gtk::Button::builder().label("Supprimer").build(); -        vbox.append(&button); +        lines.append(&button);          let conn = app.conn.clone();          let tx = app.tx.clone();          button.connect_clicked(glib::clone!(@weak dialog => move |_| { diff --git a/src/app/form/repetition.rs b/src/app/form/repetition.rs new file mode 100644 index 0000000..ac56479 --- /dev/null +++ b/src/app/form/repetition.rs @@ -0,0 +1,151 @@ +use gtk4 as gtk; + +use chrono::{Weekday, Weekday::*}; +use gtk::prelude::*; + +use crate::{ +    model::event::Event, +    model::{ +        repetition, +        repetition::{MonthFrequency, Repetition}, +    }, +}; + +static WEEKDAYS_STR: [&str; 7] = [ +    "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche", +]; + +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, +} + +pub fn view(event: &Event) -> Model { +    let view = gtk::Box::builder() +        .orientation(gtk::Orientation::Vertical) +        .build(); +    view.add_css_class("g-Form__Inputs"); + +    view.append(&label("Répétition")); + +    let no_radio = gtk::CheckButton::builder() +        .label("Non") +        .active(event.repetition.is_none()) +        .build(); +    view.append(&no_radio); + +    let default = match event.repetition { +        Some(Repetition::Daily { frequency }) => frequency.to_string(), +        _ => "".to_string(), +    }; +    let day_interval_entry = gtk::Entry::builder().text(&default).build(); +    let (day_interval_box, day_interval_radio) = radio_input( +        &no_radio, +        !default.is_empty(), +        &day_interval_entry, +        "Interval de jours", +    ); +    view.append(&day_interval_box); + +    let default = match event.repetition { +        Some(Repetition::Monthly { +            frequency: MonthFrequency::Day { day }, +        }) => day.to_string(), +        _ => "".to_string(), +    }; +    let monthly_entry = gtk::Entry::builder().text(&default).build(); +    let (monthly_box, monthly_radio) = +        radio_input(&no_radio, !default.is_empty(), &monthly_entry, "Mensuel"); +    view.append(&monthly_box); + +    let (active, default) = match event.repetition { +        Some(Repetition::Monthly { +            frequency: MonthFrequency::FirstDay { day }, +        }) => (true, day), +        _ => (false, Mon), +    }; +    let first_day_dropdown = gtk::DropDown::from_strings(&WEEKDAYS_STR); +    first_day_dropdown +        .set_selected(WEEKDAYS.iter().position(|d| d == &default).unwrap_or(0) as u32); +    let (first_day_of_month_box, first_day_radio) = +        radio_input(&no_radio, active, &first_day_dropdown, "1er jour du mois"); +    view.append(&first_day_of_month_box); + +    let yearly_radio = gtk::CheckButton::builder() +        .group(&no_radio) +        .label("Annuel") +        .active(event.repetition == Some(Repetition::Yearly)) +        .build(); +    view.append(&yearly_radio); + +    Model { +        view, +        no_radio, +        day_interval_radio, +        day_interval_entry, +        monthly_radio, +        monthly_entry, +        first_day_radio, +        first_day_dropdown, +        yearly_radio, +    } +} + +fn radio_input( +    radio_group: &impl IsA<gtk::CheckButton>, +    active: bool, +    input: &impl IsA<gtk::Widget>, +    text: &str, +) -> (gtk::Box, gtk::CheckButton) { +    let radio_box = gtk::Box::builder().build(); +    let radio = gtk::CheckButton::builder() +        .group(radio_group) +        .label(text) +        .active(active) +        .build(); +    radio_box.append(&radio); +    input.add_css_class("g-Form__RadioInput"); +    radio_box.append(input); +    (radio_box, radio) +} + +fn label(text: &str) -> gtk::Label { +    gtk::Label::builder() +        .label(text) +        .halign(gtk::Align::Start) +        .margin_bottom(5) +        .build() +} + +pub fn validate(model: &Model) -> Option<Repetition> { +    if model.no_radio.is_active() { +        None +    } else if model.day_interval_radio.is_active() { +        repetition::validate_day(&model.day_interval_entry.buffer().text()) +            .map(|d| Repetition::Daily { frequency: 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 }, +            } +        }) +    } else if model.first_day_radio.is_active() { +        let day = WEEKDAYS[model.first_day_dropdown.selected() as usize]; +        Some(Repetition::Monthly { +            frequency: MonthFrequency::FirstDay { day }, +        }) +    } else if model.yearly_radio.is_active() { +        Some(Repetition::Yearly) +    } else { +        None +    } +} diff --git a/src/app/style.css b/src/app/style.css index 5cd1394..4828e41 100644 --- a/src/app/style.css +++ b/src/app/style.css @@ -43,3 +43,7 @@  .g-Form__Input {    text-align: left;  } + +.g-Form__RadioInput { +  width: 20px; +} diff --git a/src/app/update.rs b/src/app/update.rs index baf4651..4e21050 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -48,15 +48,13 @@ pub async fn event_handler(rx: Receiver<Msg>, mut app: App) {                      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), +            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), +            },          }      }  } diff --git a/src/db/migrations/1-init.sql b/src/db/migrations/1-init.sql index 39b845b..a7db8b8 100644 --- a/src/db/migrations/1-init.sql +++ b/src/db/migrations/1-init.sql @@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS "events" (    "start" VARCHAR NULL,    "end" VARCHAR NULL,    "name" VARCHAR NOT NULL, +  "repetition" VARCHAR NULL,    "created" TIMESTAMP NOT NULL,    "updated" TIMESTAMP NOT NULL  ); diff --git a/src/db/mod.rs b/src/db/mod.rs index 0dd4ddf..2cac0d2 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -13,18 +13,28 @@ pub fn init() -> Result<Connection> {  }  pub fn insert(conn: &Connection, event: &Event) -> Result<()> { +    let repetition = match &event.repetition { +        Some(r) => Some(serde_json::to_string(&r)?), +        None => None, +    }; +      conn.execute( -        "INSERT INTO events (id, date, start, end, name, created, updated) VALUES (?, ?, ?, ?, ?, datetime(), datetime())", -        params![event.id.to_hyphenated().to_string(), event.date, event.start, event.end, event.name] +        "INSERT INTO events (id, date, start, end, name, repetition, created, updated) VALUES (?, ?, ?, ?, ?, ?, datetime(), datetime())", +        params![event.id.to_hyphenated().to_string(), event.date, event.start, event.end, event.name, repetition]      )?;      Ok(())  }  pub fn update(conn: &Connection, event: &Event) -> Result<()> { +    let repetition = match &event.repetition { +        Some(r) => Some(serde_json::to_string(&r)?), +        None => None, +    }; +      conn.execute( -        "UPDATE events SET date = ?, start = ?, end = ?, name = ?, updated = datetime() where id = ?", -        params![event.date, event.start, event.end, event.name, event.id.to_hyphenated().to_string()] +        "UPDATE events SET date = ?, start = ?, end = ?, name = ?, repetition = ?, updated = datetime() where id = ?", +        params![event.date, event.start, event.end, event.name, repetition, event.id.to_hyphenated().to_string()]      )?;      Ok(()) @@ -41,16 +51,18 @@ pub fn delete(conn: &Connection, id: &Uuid) -> Result<()> {  // TODO: Don’t use unwrap  pub fn list(conn: &Connection) -> Result<Vec<Event>> { -    let mut stmt = conn.prepare("SELECT id, date, start, end, name FROM events")?; +    let mut stmt = conn.prepare("SELECT id, date, start, end, name, repeated FROM events")?;      let iter = stmt.query_map([], |row| {          let uuid: String = row.get(0)?; +        let repetition: Option<String> = row.get(5)?;          Ok(Event {              id: Uuid::parse_str(&uuid).unwrap(),              date: row.get(1)?,              start: row.get(2)?,              end: row.get(3)?,              name: row.get(4)?, +            repetition: repetition.and_then(|r: String| serde_json::from_str(&r).ok()),          })      })?; diff --git a/src/model/event.rs b/src/model/event.rs index 7ab0244..3765fec 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -2,6 +2,8 @@ use chrono::Timelike;  use chrono::{NaiveDate, NaiveTime};  use uuid::Uuid; +use crate::model::repetition::Repetition; +  pub static DATE_FORMAT: &str = "%d/%m/%Y";  #[derive(Debug, Clone)] @@ -11,6 +13,7 @@ pub struct Event {      pub start: Option<NaiveTime>,      pub end: Option<NaiveTime>,      pub name: String, +    pub repetition: Option<Repetition>,  }  pub fn init(date: NaiveDate) -> Event { @@ -20,6 +23,7 @@ pub fn init(date: NaiveDate) -> Event {          start: None,          end: None,          name: "".to_string(), +        repetition: None,      }  } @@ -62,7 +66,14 @@ fn parse_time(t: &str) -> Option<NaiveTime> {  // Validation -pub fn validate(id: Uuid, date: String, name: String, start: String, end: String) -> Option<Event> { +pub fn validate( +    id: Uuid, +    date: String, +    name: String, +    start: String, +    end: String, +    repetition: Option<Repetition>, +) -> Option<Event> {      let start = validate_time(start)?;      let end = validate_time(end)?; @@ -77,6 +88,7 @@ pub fn validate(id: Uuid, date: String, name: String, start: String, end: String          name: validate_name(name)?,          start,          end, +        repetition,      })  } diff --git a/src/model/mod.rs b/src/model/mod.rs index 53f1126..c1beb62 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1 +1,2 @@  pub mod event; +pub mod repetition; diff --git a/src/model/repetition.rs b/src/model/repetition.rs new file mode 100644 index 0000000..80387d9 --- /dev/null +++ b/src/model/repetition.rs @@ -0,0 +1,26 @@ +use chrono::Weekday; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum Repetition { +    Daily { frequency: u8 }, +    Monthly { frequency: MonthFrequency }, +    Yearly, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum MonthFrequency { +    Day { day: u8 }, +    FirstDay { day: Weekday }, +} + +// Validation + +pub fn validate_day(str: &str) -> Option<u8> { +    let n = str.parse::<u8>().ok()?; +    if n >= 1 && n <= 31 { +        Some(n) +    } else { +        None +    } +} | 
