From bd59a5128c05dcd550e91bbdd0cd9d5996a65586 Mon Sep 17 00:00:00 2001 From: Joris Date: Sun, 9 Jan 2022 09:43:21 +0100 Subject: Persist events to sqlite db --- src/app.rs | 151 ++++++++++++++++++++++++++++++++----------- src/db/migrations/1-init.sql | 9 +++ src/db/mod.rs | 36 +++++++++++ src/main.rs | 59 ++--------------- src/model/event.rs | 45 +++++++------ src/style.css | 10 +++ 6 files changed, 200 insertions(+), 110 deletions(-) create mode 100644 src/db/migrations/1-init.sql create mode 100644 src/db/mod.rs (limited to 'src') diff --git a/src/app.rs b/src/app.rs index 5717c12..0eb2b1e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,15 +1,18 @@ use gtk4 as gtk; use async_channel::{Receiver, Sender}; -use chrono::{Datelike, NaiveDate, Weekday}; +use chrono::{Datelike, NaiveDate, NaiveTime, Weekday}; use gtk::gdk::Display; -use gtk::glib; use gtk::glib::signal::Inhibit; +use gtk::glib; use gtk::prelude::*; +use rusqlite::Connection; use std::future::Future; use std::rc::Rc; -use crate::model::event::{Event, Time}; +use crate::model::event; +use crate::model::event::Event; +use crate::db; /// Spawns a task on the default executor, without waiting for it to complete pub fn spawn(future: F) @@ -35,24 +38,25 @@ static MONTHES: [&str; 12] = [ "Jan", "Fév", "Mar", "Avr", "Mai", "Juin", "Juil", "Aoû", "Sep", "Oct", "Nov", "Déc", ]; -pub fn run(events: Vec) { - let application = gtk::Application::new(Some("me.guyonvarch.calendar"), Default::default()); - application.connect_startup(move |app| build_ui(app, &events)); - application.run(); +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(app: >k::Application, events: &Vec) { - load_style(); +fn build_ui(conn: Rc, app: >k::Application) { let (tx, rx) = async_channel::unbounded(); - let app = App::new(app, tx.clone(), &events); - spawn(event_handler(rx, tx, app)) + let app = App::new(conn.clone(), app, tx.clone()); + spawn(event_handler(conn, rx, tx, app)) } -async fn event_handler(rx: Receiver, tx: Sender, mut app: App) { +async fn event_handler(conn: Rc, rx: Receiver, tx: Sender, mut app: App) { while let Ok(msg) = rx.recv().await { match msg { Msg::ShowAddForm { date } => { - spawn(add_event_dialog(tx.clone(), Rc::clone(&app.window), date)); + add_event_dialog(Rc::clone(&conn), tx.clone(), Rc::clone(&app.window), date).await; } Msg::AddEvent { event } => { let date = event.date.clone(); @@ -85,7 +89,7 @@ struct App { } impl App { - fn new(app: >k::Application, tx: Sender, events: &Vec) -> Self { + fn new(conn: Rc, app: >k::Application, tx: Sender) -> Self { let window = Rc::new( gtk::ApplicationWindow::builder() .application(app) @@ -107,6 +111,8 @@ impl App { let start_date = NaiveDate::from_isoywd(today.year(), today.iso_week().week(), Weekday::Mon); + + let events = db::list(&conn).unwrap_or(vec!()); show_days(tx, &grid, &start_date, &today, &events); window.connect_close_request(move |window| { @@ -196,7 +202,7 @@ fn day_entry( .iter() .filter(|e| e.date == *date) .collect::>(); - events.sort_by_key(|e| e.time); + events.sort_by_key(|e| e.start); if !events.is_empty() { vbox.append(&day_events(events)); @@ -263,34 +269,103 @@ fn day_events(events: Vec<&Event>) -> gtk::Box { vbox } -async fn add_event_dialog(tx: Sender, window: Rc, date: NaiveDate) { +static DATE_FORMAT: &str = "%d/%m/%Y"; + +async fn add_event_dialog(conn: Rc, tx: Sender, window: Rc, date: NaiveDate) { let dialog = gtk::Dialog::builder() .transient_for(&*window) .modal(true) - .title("Ajouter un évènement") + .title("Ajouter") + .css_classes(vec!["g-Form".to_string()]) .build(); let content_area = dialog.content_area(); - let label = gtk::Label::builder().label(&format!("{:?}", date)).build(); - content_area.append(&label); - - let entry = gtk::Entry::builder().build(); - content_area.append(&entry); - - dialog.add_buttons(&[ - ("Annuler", gtk::ResponseType::Cancel), - ("Créer", gtk::ResponseType::Ok), - ]); - - let answer = dialog.run_future().await; - if answer == gtk::ResponseType::Ok { - let event = Event { - date, - time: Time::AllDay, - name: entry.buffer().text(), - }; - - send_message(tx, Msg::AddEvent { event: event }); + + let vbox = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + vbox.add_css_class("g-Form__Inputs"); + content_area.append(&vbox); + + let name = entry(""); + vbox.append(&label("Événement")); + vbox.append(&name); + + let date = entry(&date.format(DATE_FORMAT).to_string()); + vbox.append(&label("Jour")); + vbox.append(&date); + + let start = entry(""); + vbox.append(&label("Début")); + vbox.append(&start); + + let end = entry(""); + vbox.append(&label("Fin")); + vbox.append(&end); + + let button = gtk::Button::with_label("Créer"); + vbox.append(&button); + button.connect_clicked(glib::clone!(@weak dialog => move |_| { + match validate_event(date.buffer().text(), name.buffer().text(), start.buffer().text(), end.buffer().text()) { + Some(event) => { + match db::insert(&conn, &event) { + Ok(_) => { + send_message(tx.clone(), Msg::AddEvent { event: event }); + dialog.close() + }, + Err(_) => () + } + }, + None => () + } + })); + + 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() +} + +fn validate_event(date: String, name: String, start: String, end: String) -> Option { + let start = validate_time(start)?; + let end = validate_time(end)?; + + match (start, end) { + (Some(s), Some(e)) if s > e => None?, + _ => (), + } + + Some(Event { + date: NaiveDate::parse_from_str(&date, DATE_FORMAT).ok()?, + name: validate_name(name)?, + start, + end, + }) +} + +fn validate_time(time: String) -> Option> { + let time = time.trim(); + if time.is_empty() { + Some(None) + } else { + event::parse_time(time).map(|t| Some(t)) + } +} + +fn validate_name(name: String) -> Option { + let name = name.trim(); + if name.is_empty() { + None + } else { + Some(name.to_string()) } - dialog.close(); } diff --git a/src/db/migrations/1-init.sql b/src/db/migrations/1-init.sql new file mode 100644 index 0000000..72fab80 --- /dev/null +++ b/src/db/migrations/1-init.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS "events" ( + "id" INTEGER PRIMARY KEY, + "date" VARCHAR NOT NULL, + "start" VARCHAR NULL, + "end" VARCHAR NULL, + "name" VARCHAR NOT NULL, + "created" TIMESTAMP NOT NULL, + "updated" TIMESTAMP NOT NULL +); diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..3348673 --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,36 @@ +use anyhow::Result; +use rusqlite::{params, Connection}; +use rusqlite_migration::{Migrations, M}; + +use crate::model::event::Event; + +pub fn init() -> Result { + let mut conn = Connection::open("database.db")?; + let migrations = Migrations::new(vec![M::up(include_str!("migrations/1-init.sql"))]); + migrations.to_latest(&mut conn)?; + Ok(conn) +} + +pub fn insert(conn: &Connection, event: &Event) -> Result<()> { + conn.execute( + "INSERT INTO events (date, start, end, name, created, updated) VALUES (?, ?, ?, ?, datetime(), datetime())", + params![event.date, event.start, event.end, event.name] + )?; + + Ok(()) +} + +pub fn list(conn: &Connection) -> Result> { + let mut stmt = conn.prepare("SELECT date, start, end, name FROM events")?; + + let iter = stmt.query_map([], |row| + Ok(Event { + date: row.get(0)?, + start: row.get(1)?, + end: row.get(2)?, + name: row.get(3)?, + }) + )?; + + Ok(iter.map(|r| r.unwrap()).collect()) +} diff --git a/src/main.rs b/src/main.rs index 9dbed82..f30e38e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,59 +1,10 @@ mod app; mod model; +mod db; -use chrono::{NaiveDate, NaiveTime}; +use anyhow::Result; -use crate::model::event::{Event, Time}; - -fn main() { - let events = test_events(); - app::run(events) -} - -fn test_events() -> Vec { - [ - Event { - date: NaiveDate::from_ymd(2021, 11, 29), - time: Time::AllDay, - name: "Début de la semaine".to_string(), - }, - Event { - date: NaiveDate::from_ymd(2021, 12, 4), - time: Time::AllDay, - name: "Fin de la semaine".to_string(), - }, - Event { - date: NaiveDate::from_ymd(2021, 12, 4), - time: Time::Time { - start: NaiveTime::from_hms(15, 0, 0), - end: Some(NaiveTime::from_hms(15, 30, 0)), - }, - name: "Appel".to_string(), - }, - Event { - date: NaiveDate::from_ymd(2021, 12, 4), - time: Time::Time { - start: NaiveTime::from_hms(12, 0, 0), - end: Some(NaiveTime::from_hms(14, 0, 0)), - }, - name: "Repas".to_string(), - }, - Event { - date: NaiveDate::from_ymd(2021, 12, 4), - time: Time::Time { - start: NaiveTime::from_hms(8, 0, 0), - end: None, - }, - name: "Promener le chien".to_string(), - }, - Event { - date: NaiveDate::from_ymd(2021, 12, 4), - time: Time::Time { - start: NaiveTime::from_hms(9, 0, 0), - end: None, - }, - name: "Thé".to_string(), - }, - ] - .to_vec() +fn main() -> Result<()> { + let conn = db::init()?; + Ok(app::run(conn)) } diff --git a/src/model/event.rs b/src/model/event.rs index d1d9775..2650c47 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -1,35 +1,31 @@ use chrono::Timelike; use chrono::{NaiveDate, NaiveTime}; +// #[derive(Debug, Clone, sqlx::FromRow)] #[derive(Debug, Clone)] pub struct Event { pub date: NaiveDate, - pub time: Time, + pub start: Option, + pub end: Option, pub name: String, } impl Event { pub fn pprint(&self) -> String { - match self.time { - Time::AllDay => self.name.clone(), - Time::Time { start, end: None } => format!("{} {}", pprint_time(start), self.name), - Time::Time { - start, - end: Some(e), - } => format!("{}-{} {}", pprint_time(start), pprint_time(e), self.name), - } + let start = self.start.map(pprint_time).unwrap_or_default(); + let end = self + .end + .map(|t| format!("-{}", pprint_time(t))) + .unwrap_or_default(); + let space = if self.start.is_some() || self.end.is_some() { + " " + } else { + "" + }; + format!("{}{}{}{}", start, end, space, self.name) } } -#[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Eq, Ord)] -pub enum Time { - AllDay, - Time { - start: NaiveTime, - end: Option, - }, -} - fn pprint_time(t: NaiveTime) -> String { if t.minute() == 0 { format!("{}h", t.hour()) @@ -37,3 +33,16 @@ fn pprint_time(t: NaiveTime) -> String { format!("{}h{}", t.hour(), t.minute()) } } + +pub fn parse_time(t: &str) -> Option { + match t.split('h').collect::>()[..] { + [hours, minutes] => { + if minutes.trim().is_empty() { + NaiveTime::from_hms_opt(hours.parse().ok()?, 0, 0) + } else { + NaiveTime::from_hms_opt(hours.parse().ok()?, minutes.parse().ok()?, 0) + } + } + _ => None, + } +} diff --git a/src/style.css b/src/style.css index 0a8292f..5cd1394 100644 --- a/src/style.css +++ b/src/style.css @@ -33,3 +33,13 @@ .g-Calendar__DayEvent:hover { background-color: pink; } + +.g-Form { + background-color: white; + color: black; + padding: 10px; +} + +.g-Form__Input { + text-align: left; +} -- cgit v1.2.3