aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJoris2022-01-09 09:43:21 +0100
committerJoris2022-01-09 10:11:29 +0100
commitbd59a5128c05dcd550e91bbdd0cd9d5996a65586 (patch)
tree541f7d49253ad3e7c8dfab480f33a2b10107b0d2 /src
parentce978143f1360e16e85587644055a9f83d11c64c (diff)
Persist events to sqlite db
Diffstat (limited to 'src')
-rw-r--r--src/app.rs151
-rw-r--r--src/db/migrations/1-init.sql9
-rw-r--r--src/db/mod.rs36
-rw-r--r--src/main.rs59
-rw-r--r--src/model/event.rs45
-rw-r--r--src/style.css10
6 files changed, 200 insertions, 110 deletions
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<F>(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<Event>) {
- 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: &gtk::Application, events: &Vec<Event>) {
- load_style();
+fn build_ui(conn: Rc<Connection>, app: &gtk::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<Msg>, tx: Sender<Msg>, mut app: App) {
+async fn event_handler(conn: Rc<Connection>, rx: Receiver<Msg>, tx: Sender<Msg>, 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: &gtk::Application, tx: Sender<Msg>, events: &Vec<Event>) -> Self {
+ fn new(conn: Rc<Connection>, app: &gtk::Application, tx: Sender<Msg>) -> 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::<Vec<&Event>>();
- 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<Msg>, window: Rc<gtk::ApplicationWindow>, date: NaiveDate) {
+static DATE_FORMAT: &str = "%d/%m/%Y";
+
+async fn add_event_dialog(conn: Rc<Connection>, tx: Sender<Msg>, window: Rc<gtk::ApplicationWindow>, 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<Event> {
+ 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<Option<NaiveTime>> {
+ 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<String> {
+ 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<Connection> {
+ 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<Vec<Event>> {
+ 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> {
- [
- 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<NaiveTime>,
+ pub end: Option<NaiveTime>,
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<NaiveTime>,
- },
-}
-
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<NaiveTime> {
+ match t.split('h').collect::<Vec<&str>>()[..] {
+ [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;
+}