aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/app')
-rw-r--r--src/app/app.rs76
-rw-r--r--src/app/calendar.rs189
-rw-r--r--src/app/form/mod.rs124
-rw-r--r--src/app/form/repetition.rs151
-rw-r--r--src/app/mod.rs38
-rw-r--r--src/app/style.css49
-rw-r--r--src/app/update.rs105
-rw-r--r--src/app/utils.rs9
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: &gtk::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: &gtk::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: &gtk::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);
-}