diff options
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/app.rs | 76 | ||||
| -rw-r--r-- | src/app/calendar.rs | 189 | ||||
| -rw-r--r-- | src/app/form/mod.rs | 124 | ||||
| -rw-r--r-- | src/app/form/repetition.rs | 151 | ||||
| -rw-r--r-- | src/app/mod.rs | 38 | ||||
| -rw-r--r-- | src/app/style.css | 49 | ||||
| -rw-r--r-- | src/app/update.rs | 105 | ||||
| -rw-r--r-- | src/app/utils.rs | 9 | 
8 files changed, 0 insertions, 741 deletions
| diff --git a/src/app/app.rs b/src/app/app.rs deleted file mode 100644 index 58240af..0000000 --- a/src/app/app.rs +++ /dev/null @@ -1,76 +0,0 @@ -use gtk4 as gtk; - -use async_channel::Sender; -use chrono::{Datelike, Duration, NaiveDate, Weekday}; -use gtk::glib::signal::Inhibit; -use gtk::prelude::*; -use rusqlite::Connection; -use std::rc::Rc; - -use crate::app::calendar; -use crate::app::update::Msg; -use crate::{db, model::event::Event}; - -pub struct App { -    pub conn: Rc<Connection>, -    pub window: Rc<gtk::ApplicationWindow>, -    pub grid: gtk::Grid, -    pub events: Vec<Event>, -    pub repeated_events: Vec<Event>, -    pub today: NaiveDate, -    pub start_date: NaiveDate, -    pub end_date: NaiveDate, -    pub tx: Sender<Msg>, -} - -impl App { -    pub fn new(conn: Rc<Connection>, app: >k::Application, tx: Sender<Msg>) -> Self { -        let window = Rc::new( -            gtk::ApplicationWindow::builder() -                .application(app) -                .title("Calendar") -                .default_width(800) -                .default_height(600) -                .visible(true) -                .build(), -        ); - -        let today = chrono::offset::Local::today().naive_utc(); -        let start_date = -            NaiveDate::from_isoywd(today.year(), today.iso_week().week(), Weekday::Mon); -        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)); - -        window.connect_close_request(move |window| { -            if let Some(application) = window.application() { -                application.remove_window(window); -            } -            Inhibit(false) -        }); - -        Self { -            conn, -            window, -            grid, -            events, -            repeated_events, -            today, -            start_date, -            end_date, -            tx, -        } -    } -} diff --git a/src/app/calendar.rs b/src/app/calendar.rs deleted file mode 100644 index 11eb893..0000000 --- a/src/app/calendar.rs +++ /dev/null @@ -1,189 +0,0 @@ -use gtk4 as gtk; - -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, model::event::Event}; - -static DAYS: [&str; 7] = ["LUN", "MAR", "MER", "JEU", "VEN", "SAM", "DIM"]; -static MONTHES: [&str; 12] = [ -    "Jan", "Fév", "Mar", "Avr", "Mai", "Juin", "Juil", "Aoû", "Sep", "Oct", "Nov", "Déc", -]; - -pub fn create( -    tx: Sender<Msg>, -    today: NaiveDate, -    start_date: NaiveDate, -    end_date: NaiveDate, -    events: &Vec<Event>, -    repeated_events: &Vec<Event>, -) -> gtk::Grid { -    let grid = gtk::Grid::builder().build(); - -    for col in 0..7 { -        grid.attach(&day_title(col), col, 0, 1, 1); -    } - -    let repetitions = event::repetitions_between(repeated_events, start_date, end_date); -    attach_days(tx, &grid, start_date, today, events, &repetitions); - -    grid -} - -fn attach_days( -    tx: Sender<Msg>, -    grid: >k::Grid, -    start_date: NaiveDate, -    today: NaiveDate, -    events: &Vec<Event>, -    repetitions: &HashMap<NaiveDate, Vec<Event>>, -) { -    let mut d = start_date; -    for row in 1..5 { -        for col in 0..7 { -            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, repetitions: &HashMap<NaiveDate, Vec<Event>>) { -    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, repetitions), -        col, -        row, -        1, -        1, -    ) -} - -fn day_title(col: i32) -> gtk::Box { -    let vbox = gtk::Box::builder() -        .orientation(gtk::Orientation::Vertical) -        .build(); - -    vbox.add_css_class("g-Calendar__DayTitle"); - -    let label = gtk::Label::builder().label(DAYS[col as usize]).build(); - -    vbox.append(&label); - -    vbox -} - -pub fn day_entry( -    tx: Sender<Msg>, -    date: NaiveDate, -    today: NaiveDate, -    events: &Vec<Event>, -    repetitions: &HashMap<NaiveDate, Vec<Event>>, -) -> gtk::ScrolledWindow { -    let vbox = gtk::Box::builder() -        .orientation(gtk::Orientation::Vertical) -        .build(); - -    vbox.add_css_class("g-Calendar__Day"); - -    let gesture = gtk::GestureClick::new(); -    gesture.connect_pressed(glib::clone!(@strong tx => move |_, n, _, _| { -        if n == 2 { -            update::send(tx.clone(), Msg::ShowAddForm { date }); -        } -    })); -    vbox.add_controller(&gesture); - -    if date == today { -        vbox.add_css_class("g-Calendar__Day--Today"); -    } - -    vbox.append(&day_label(date)); - -    let mut events = events -        .iter() -        .filter(|e| e.date == date) -        .collect::<Vec<&Event>>(); -    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() { -        vbox.append(&day_events(tx, events)); -    } - -    let scrolled_window = gtk::ScrolledWindow::builder() -        .hscrollbar_policy(gtk::PolicyType::Never) -        .hexpand(true) -        .vexpand(true) -        .child(&vbox) -        .build(); - -    scrolled_window -} - -fn day_label(date: NaiveDate) -> gtk::Label { -    let label = gtk::Label::builder() -        .label(&format!( -            "{} {}", -            date.day(), -            MONTHES[date.month0() as usize] -        )) -        .halign(gtk::Align::Start) -        .build(); - -    label.add_css_class("g-Calendar__DayNumber"); - -    label -} - -fn day_events(tx: Sender<Msg>, events: Vec<&Event>) -> gtk::Box { -    let vbox = gtk::Box::builder() -        .orientation(gtk::Orientation::Vertical) -        .build(); - -    for event in events { -        let hbox = gtk::Box::builder() -            .orientation(gtk::Orientation::Horizontal) -            .hexpand(true) -            .build(); - -        let gesture = gtk::GestureClick::new(); -        gesture.connect_pressed( -            glib::clone!(@strong event, @strong tx => move |gesture, n, _, _| { -                gesture.set_state(gtk::EventSequenceState::Claimed); -                if n == 2 { -                    update::send(tx.clone(), Msg::ShowUpdateForm { event: event.clone() }); -                } -            }), -        ); -        hbox.add_controller(&gesture); - -        hbox.add_css_class("g-Calendar__DayEvent"); - -        let event_txt = &event.pprint(); -        let label = gtk::Label::builder() -            .label(&event_txt) -            .ellipsize(gtk::pango::EllipsizeMode::End) -            .tooltip_text(&event_txt) -            .halign(gtk::Align::Start) -            .build(); - -        hbox.append(&label); -        vbox.append(&hbox); -    } - -    vbox -} diff --git a/src/app/form/mod.rs b/src/app/form/mod.rs deleted file mode 100644 index 9cb6ba7..0000000 --- a/src/app/form/mod.rs +++ /dev/null @@ -1,124 +0,0 @@ -mod repetition; - -use gtk4 as gtk; - -use gtk::glib; -use gtk::prelude::*; - -use crate::{ -    app::{update, update::Msg, App}, -    db, -    model::{event, event::Event}, -}; - -pub async fn show(app: &App, event: Event, is_new: bool) { -    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()]) -        .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) -        .build(); -    columns.add_css_class("g-Form__Columns"); -    lines.append(&columns); - -    // First column - -    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); -    column1.append(&label("Événement")); -    column1.append(&name); - -    let date = entry(&event.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("".to_string()), -    ); -    column1.append(&label("Début")); -    column1.append(&start); - -    let end = entry(&event.end.map(event::pprint_time).unwrap_or("".to_string())); -    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(); -    lines.append(&button); -    let conn = app.conn.clone(); -    let tx = app.tx.clone(); -    button.connect_clicked(glib::clone!(@weak dialog, @strong event => move |_| { -        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(_) => { -                        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}") -                } -            }, -            None => eprintln!("Event is not valid: {event:?}") -        } -    })); - -    if !is_new { -        let button = gtk::Button::builder().label("Supprimer").build(); -        lines.append(&button); -        let conn = app.conn.clone(); -        let tx = app.tx.clone(); -        button.connect_clicked(glib::clone!(@weak dialog => move |_| { -            match db::delete(&conn, &event.id) { -                Ok(_) => { -                    update::send(tx.clone(), Msg::DeleteEvent { event: event.clone() }); -                    dialog.close() -                }, -                Err(_) => () -            } -        })); -    } - -    dialog.run_future().await; -} - -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() -} diff --git a/src/app/form/repetition.rs b/src/app/form/repetition.rs deleted file mode 100644 index 87c8d84..0000000 --- a/src/app/form/repetition.rs +++ /dev/null @@ -1,151 +0,0 @@ -use gtk4 as gtk; - -use chrono::{Weekday, Weekday::*}; -use gtk::prelude::*; - -use crate::{ -    model::event::Event, -    model::{ -        repetition, -        repetition::{DayOfMonth, 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, -    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 { -    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 { period }) => period.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 { -            day: DayOfMonth::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 { -            day: DayOfMonth::Weekday { weekday }, -        }) => (true, weekday), -        _ => (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 { period: d }) -    } else if model.monthly_radio.is_active() { -        repetition::validate_day(&model.monthly_entry.buffer().text()).map(|d| { -            Repetition::Monthly { -                day: DayOfMonth::Day { day: d }, -            } -        }) -    } else if model.first_day_radio.is_active() { -        let weekday = WEEKDAYS[model.first_day_dropdown.selected() as usize]; -        Some(Repetition::Monthly { -            day: DayOfMonth::Weekday { weekday }, -        }) -    } else if model.yearly_radio.is_active() { -        Some(Repetition::Yearly) -    } else { -        None -    } -} diff --git a/src/app/mod.rs b/src/app/mod.rs deleted file mode 100644 index c9a7f83..0000000 --- a/src/app/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -mod app; -mod calendar; -mod form; -mod update; -mod utils; - -use gtk4 as gtk; - -use gtk::gdk::Display; -use gtk::prelude::*; -use rusqlite::Connection; -use std::rc::Rc; - -use app::App; - -pub fn run(conn: Connection) { -    let conn = Rc::new(conn); -    let app = gtk::Application::new(Some("me.guyonvarch.calendar"), Default::default()); -    app.connect_startup(|_| load_style()); -    app.connect_activate(move |app| build_ui(conn.clone(), app)); -    app.run(); -} - -fn build_ui(conn: Rc<Connection>, app: >k::Application) { -    let (tx, rx) = async_channel::unbounded(); -    let app = App::new(conn.clone(), app, tx.clone()); -    utils::spawn(update::event_handler(rx, app)) -} - -fn load_style() { -    let provider = gtk::CssProvider::new(); -    provider.load_from_data(include_bytes!("style.css")); -    gtk::StyleContext::add_provider_for_display( -        &Display::default().expect("Error initializing gtk css provider."), -        &provider, -        gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, -    ); -} diff --git a/src/app/style.css b/src/app/style.css deleted file mode 100644 index 4828e41..0000000 --- a/src/app/style.css +++ /dev/null @@ -1,49 +0,0 @@ -.g-Calendar__DayTitle { -  border-right: 1px solid #D2D2D2; -  border-bottom: 1px solid #D2D2D2; -  padding: 4px; -  background-color: white; -  font-weight: bold; -} - -.g-Calendar__Day { -  background-color: white; -  border-right: 1px solid #D2D2D2; -  border-bottom: 1px solid #D2D2D2; -  padding: 4px; -} - -.g-Calendar__Day--Today { -  background-color: #FFFCD8; -} - -.g-Calendar__DayNumber { -  font-size: 90%; -  margin-bottom: 4px; -} - -.g-Calendar__DayEvent { -  background-color: #A8C2E0; -  color: white; -  border-radius: 4px; -  padding: 4px; -  margin: 4px; -} - -.g-Calendar__DayEvent:hover { -  background-color: pink; -} - -.g-Form { -  background-color: white; -  color: black; -  padding: 10px; -} - -.g-Form__Input { -  text-align: left; -} - -.g-Form__RadioInput { -  width: 20px; -} diff --git a/src/app/update.rs b/src/app/update.rs deleted file mode 100644 index 4ef1eb1..0000000 --- a/src/app/update.rs +++ /dev/null @@ -1,105 +0,0 @@ -use async_channel::{Receiver, Sender}; -use chrono::NaiveDate; -use std::collections::HashSet; - -use crate::{ -    app::{calendar, form, utils, App}, -    model::{event, event::Event}, -}; - -pub fn send(tx: Sender<Msg>, msg: Msg) { -    utils::spawn(async move { -        let _ = tx.send(msg).await; -    }) -} - -pub enum Msg { -    ShowAddForm { date: NaiveDate }, -    ShowUpdateForm { event: Event }, -    AddEvent { new: Event }, -    UpdateEvent { old: Event, new: Event }, -    DeleteEvent { event: Event }, -} - -pub async fn event_handler(rx: Receiver<Msg>, mut app: App) { -    while let Ok(msg) = rx.recv().await { -        match msg { -            Msg::ShowAddForm { date } => { -                form::show(&app, event::init(date), true).await; -            } -            Msg::ShowUpdateForm { event } => { -                form::show(&app, event, false).await; -            } -            Msg::AddEvent { new } => { -                let refresh_dates = add(&mut app, &new); -                refresh(&app, &refresh_dates) -            } -            Msg::UpdateEvent { old, new } => { -                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::<HashSet<NaiveDate>>()) -            } -            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<NaiveDate> { -    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<NaiveDate> { -    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<NaiveDate> { -    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<NaiveDate>) { -    let repetitions = event::repetitions_between(&app.repeated_events, app.start_date, app.end_date); - -    for date in dates { -        calendar::refresh_date(app, *date, &repetitions) -    } -} diff --git a/src/app/utils.rs b/src/app/utils.rs deleted file mode 100644 index 673b96e..0000000 --- a/src/app/utils.rs +++ /dev/null @@ -1,9 +0,0 @@ -use std::future::Future; - -/// Spawns a task on the default executor, without waiting for it to complete -pub fn spawn<F>(future: F) -where -    F: Future<Output = ()> + 'static, -{ -    gtk4::glib::MainContext::default().spawn_local(future); -} | 
