diff --git a/src/database/v1.rs b/src/database/v1.rs index 9ad672e..4416d21 100644 --- a/src/database/v1.rs +++ b/src/database/v1.rs @@ -1,10 +1,9 @@ pub(self) use chrono::NaiveDateTime; -pub(self) use serde_derive::{Serialize, Deserialize}; +pub(self) use serde_derive::{Deserialize, Serialize}; pub(self) use uuid::Uuid; /// Stuff in the default namespace -pub mod global { -} +pub mod global {} pub mod trees { pub mod categories { @@ -42,5 +41,3 @@ pub mod trees { } } } - - diff --git a/src/handlebars_util.rs b/src/handlebars_util.rs index bf3c8b8..95ec77c 100644 --- a/src/handlebars_util.rs +++ b/src/handlebars_util.rs @@ -1,13 +1,26 @@ -use rocket_contrib::templates::Engines; +use chrono::NaiveDateTime; use handlebars::handlebars_helper; +use rocket_contrib::templates::Engines; pub fn register_helpers(engines: &mut Engines) { handlebars_helper!(pretty_datetime: |dt: str| { - let date = dt.trim_end_matches(|c| c != 'T').trim_end_matches('T'); - let time = dt.trim_start_matches(|c| c != 'T').trim_start_matches('T') - .trim_end_matches(|c| c != ':').trim_end_matches(':'); - format!("{} {}", date, time) + let dt: NaiveDateTime = dt.parse().unwrap(); + format!("{}", dt.format("%Y-%m-%d %H:%M")) }); + engines + .handlebars + .register_helper("pretty_datetime", Box::new(pretty_datetime)); - engines.handlebars.register_helper("pretty_datetime", Box::new(pretty_datetime)); + handlebars_helper!(pretty_seconds: |secs: u64| { + let minutes = secs / 60; + let hours = minutes / 60; + if hours > 0 { + format!("{}h {}m", hours, minutes) + } else { + format!("{}m", minutes) + } + }); + engines + .handlebars + .register_helper("pretty_seconds", Box::new(pretty_seconds)); } diff --git a/src/main.rs b/src/main.rs index 38aafcc..688e0a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,13 @@ #![feature(decl_macro)] mod database; +mod handlebars_util; mod routes; mod status_json; -mod handlebars_util; -use std::{io, env}; use dotenv::dotenv; -use rocket_contrib::templates::Template; use rocket_contrib::serve::StaticFiles; +use rocket_contrib::templates::Template; +use std::{env, io}; fn main() -> io::Result<()> { dotenv().ok(); @@ -17,17 +17,19 @@ fn main() -> io::Result<()> { let sled = sled::open(db_path)?; let rocket = rocket::ignite() - .attach(Template::custom(|engines| handlebars_util::register_helpers(engines))) + .attach(Template::custom(|engines| { + handlebars_util::register_helpers(engines) + })) .manage(sled) .mount("/static", StaticFiles::from("static")) .mount( "/", rocket::routes![ - routes::index, - routes::history, - routes::create_category, - routes::activate_category, - routes::deactivate_category, + routes::pages::index, + routes::pages::history, + routes::api::create_category, + routes::api::activate_category, + routes::api::deactivate_category, ], ); @@ -35,4 +37,3 @@ fn main() -> io::Result<()> { Ok(()) } - diff --git a/src/routes.rs b/src/routes.rs deleted file mode 100644 index 62d9084..0000000 --- a/src/routes.rs +++ /dev/null @@ -1,140 +0,0 @@ -use serde_derive::{Serialize, Deserialize}; -use rocket::{State, get, post, uri}; -use uuid::Uuid; -use rocket_contrib::templates::Template; -use crate::database::v1::trees::{categories, past_sessions}; -use crate::status_json::StatusJson; -use rocket::http::Status; -use bincode::{serialize, deserialize}; -use rocket::response::Redirect; -use rocket::request::{FromForm, Form}; -use sled::Transactional; -use chrono::{Duration, Utc}; -use std::collections::HashMap; - -#[derive(FromForm)] -pub struct NewCategory { - name: String, - color: String, -} - -#[post("/create_category", data="")] -pub fn create_category(category: Form, db: State<'_, sled::Db>) -> Result { - let category = category.into_inner(); - - let categories_tree = db.open_tree(categories::NAME)?; - categories_tree.insert(serialize(&Uuid::new_v4())?, serialize(&categories::V { - name: category.name, - color: category.color, - started: None, - })?)?; - - Ok(Redirect::to(uri!(index))) -} - - -#[post("/set_category//active")] -pub fn activate_category(category_uuid: String, db: State<'_, sled::Db>) -> Result { - toggle_category(category_uuid, true, db) -} - -#[post("/set_category//inactive")] -pub fn deactivate_category(category_uuid: String, db: State<'_, sled::Db>) -> Result { - toggle_category(category_uuid, false, db) -} - -pub fn toggle_category(category_uuid: String, set_active: bool, db: State<'_, sled::Db>) -> Result { - let category_uuid = Uuid::parse_str(&category_uuid)?; - let category_uuid_s = sled::IVec::from(serialize(&category_uuid)?); - - let categories_tree = db.open_tree(categories::NAME)?; - let past_sessions_tree = db.open_tree(past_sessions::NAME)?; - - Ok((&categories_tree, &past_sessions_tree) - .transaction(|(tx_categories, tx_past_sessions)| { - match tx_categories.get(&category_uuid_s)? { - None => return Ok(Err(Status::NotFound)), - Some(data) => { - - let mut category: categories::V = dbg!(deserialize(&data).unwrap()); - let now = Utc::now().naive_utc(); - - match (set_active, category.started.take()) { - (false, Some(started)) => { - // only save sessions longer than 5 minutes - let duration = now - started; - if duration > Duration::minutes(5) { - let session_uuid = serialize(&Uuid::new_v4()).unwrap(); - let past_session = past_sessions::V { - category: category_uuid, - started, - ended: now, - }; - tx_past_sessions.insert(session_uuid, serialize(&past_session).unwrap())?; - } - } - (true, None) => { - category.started = Some(now); - } - _ => { - // Category is already in the correct state - return Ok(Ok(Status::Ok.into())); - } - } - - tx_categories.insert(&category_uuid_s, serialize(&category).unwrap())?; - } - } - - Ok(Ok(Status::Ok.into())) - })??) -} - -#[get("/")] -pub fn index(db: State<'_, sled::Db>) -> Result { - #[derive(Debug, Serialize, Deserialize)] - struct TemplateContext { - categories: Vec<(Uuid, categories::V)>, - } - - let categories_tree = db.open_tree(categories::NAME)?; - - let context = TemplateContext { - categories: categories_tree.iter() - .map(|result| result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v))))) - .collect::, _>>()??, - }; - - Ok(Template::render("index", dbg!(&context))) -} - -#[get("/history")] -pub fn history(db: State<'_, sled::Db>) -> Result { - #[derive(Debug, Serialize, Deserialize)] - struct TemplateContext { - sessions: Vec<(categories::V, past_sessions::V)>, - } - - let categories_tree = db.open_tree(categories::NAME)?; - let past_sessions_tree = db.open_tree(past_sessions::NAME)?; - - let categories: HashMap = categories_tree.iter() - .map(|result| result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v))))) - .collect::, _>>()??; - - let past_sessions: HashMap = past_sessions_tree.iter() - .map(|result| result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v))))) - .collect::, _>>()??; - - - let context = TemplateContext { - sessions: past_sessions.into_iter() - .map(|(_, session)| { - let category = categories.get(&session.category).unwrap().clone(); - (category, session) - }) - .collect(), - }; - - Ok(Template::render("history", dbg!(&context))) -} diff --git a/src/routes/api.rs b/src/routes/api.rs new file mode 100644 index 0000000..e60b454 --- /dev/null +++ b/src/routes/api.rs @@ -0,0 +1,103 @@ +use crate::database::v1::trees::{categories, past_sessions}; +use crate::status_json::StatusJson; +use bincode::{deserialize, serialize}; +use chrono::{Duration, Utc}; +use rocket::http::Status; +use rocket::request::{Form, FromForm}; +use rocket::{post, State}; +use sled::Transactional; +use uuid::Uuid; + +#[derive(FromForm)] +pub struct NewCategory { + name: String, + color: String, +} + +#[post("/create_category", data = "")] +pub fn create_category( + category: Form, + db: State<'_, sled::Db>, +) -> Result { + let category = category.into_inner(); + + let categories_tree = db.open_tree(categories::NAME)?; + categories_tree.insert( + serialize(&Uuid::new_v4())?, + serialize(&categories::V { + name: category.name, + color: category.color, + started: None, + })?, + )?; + + Ok(Status::Ok.into()) +} + +#[post("/set_category//active")] +pub fn activate_category( + category_uuid: String, + db: State<'_, sled::Db>, +) -> Result { + toggle_category(category_uuid, true, db) +} + +#[post("/set_category//inactive")] +pub fn deactivate_category( + category_uuid: String, + db: State<'_, sled::Db>, +) -> Result { + toggle_category(category_uuid, false, db) +} + +pub fn toggle_category( + category_uuid: String, + set_active: bool, + db: State<'_, sled::Db>, +) -> Result { + let category_uuid = Uuid::parse_str(&category_uuid)?; + let category_uuid_s = sled::IVec::from(serialize(&category_uuid)?); + + let categories_tree = db.open_tree(categories::NAME)?; + let past_sessions_tree = db.open_tree(past_sessions::NAME)?; + + Ok((&categories_tree, &past_sessions_tree).transaction( + |(tx_categories, tx_past_sessions)| { + match tx_categories.get(&category_uuid_s)? { + None => return Ok(Err(Status::NotFound)), + Some(data) => { + let mut category: categories::V = deserialize(&data).unwrap(); + let now = Utc::now().naive_utc(); + + match (set_active, category.started.take()) { + (false, Some(started)) => { + // only save sessions longer than 5 minutes + let duration = now - started; + if duration > Duration::minutes(5) { + let session_uuid = serialize(&Uuid::new_v4()).unwrap(); + let past_session = past_sessions::V { + category: category_uuid, + started, + ended: now, + }; + tx_past_sessions + .insert(session_uuid, serialize(&past_session).unwrap())?; + } + } + (true, None) => { + category.started = Some(now); + } + _ => { + // Category is already in the correct state + return Ok(Ok(Status::Ok.into())); + } + } + + tx_categories.insert(&category_uuid_s, serialize(&category).unwrap())?; + } + } + + Ok(Ok(Status::Ok.into())) + }, + )??) +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..22bfe40 --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,2 @@ +pub mod api; +pub mod pages; diff --git a/src/routes/pages.rs b/src/routes/pages.rs new file mode 100644 index 0000000..67a71c5 --- /dev/null +++ b/src/routes/pages.rs @@ -0,0 +1,82 @@ +use crate::database::v1::trees::{categories, past_sessions}; +use crate::status_json::StatusJson; +use bincode::deserialize; +use rocket::{get, State}; +use rocket_contrib::templates::Template; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::Duration; +use uuid::Uuid; + +#[get("/")] +pub fn index(db: State<'_, sled::Db>) -> Result { + #[derive(Debug, Serialize, Deserialize)] + struct TemplateContext { + categories: Vec<(Uuid, categories::V)>, + } + + let categories_tree = db.open_tree(categories::NAME)?; + + let context = TemplateContext { + categories: categories_tree + .iter() + .map(|result| { + result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v)))) + }) + .collect::, _>>()??, + }; + + Ok(Template::render("index", &context)) +} + +#[get("/history")] +pub fn history(db: State<'_, sled::Db>) -> Result { + #[derive(Debug, Serialize, Deserialize)] + struct HistoryEntryContext { + category: categories::V, + session: past_sessions::V, + duration: Duration, + } + + #[derive(Debug, Serialize, Deserialize)] + struct TemplateContext { + entries: Vec, + } + + let categories_tree = db.open_tree(categories::NAME)?; + let past_sessions_tree = db.open_tree(past_sessions::NAME)?; + + let categories: HashMap = categories_tree + .iter() + .map(|result| { + result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v)))) + }) + .collect::, _>>()??; + + let past_sessions: HashMap = past_sessions_tree + .iter() + .map(|result| { + result.map(|(k, v)| deserialize(&k).and_then(|k| deserialize(&v).map(|v| (k, v)))) + }) + .collect::, _>>()??; + + let mut context = dbg!(TemplateContext { + entries: past_sessions + .into_iter() + .map(|(_, session)| { + let category = categories.get(&session.category).unwrap().clone(); + HistoryEntryContext { + duration: (session.ended - session.started).to_std().unwrap(), + category, + session, + } + }) + .collect(), + }); + + // Newest entries first + context.entries.sort_by_key(|entry| entry.session.started); + context.entries.reverse(); + + Ok(Template::render("history", &context)) +} diff --git a/src/status_json.rs b/src/status_json.rs index dd5e63f..c92d090 100644 --- a/src/status_json.rs +++ b/src/status_json.rs @@ -59,7 +59,6 @@ impl<'r> Responder<'r> for StatusJson { } } - #[duplicate( status_code T; [ Status::InternalServerError ] [ sled::Error ]; diff --git a/static/styles.css b/static/styles.css index ddd75d6..7ab328c 100644 --- a/static/styles.css +++ b/static/styles.css @@ -1,4 +1,4 @@ -body { +ody { font-family: Ubuntu; } @@ -6,14 +6,14 @@ body { text-align: center; } -ul.category_list { +ul.striped_list { max-width: 40em; list-style-type: none; margin: auto; padding: 0; } -ul.category_list > li:nth-of-type(odd) { +ul.striped_list > li:nth-of-type(odd) { background-color: #f0f0f0; } @@ -80,23 +80,23 @@ ul.category_list > li:nth-of-type(odd) { border-width: 0px 0 0px 50px; } - -.history_list { - -} - .history_entry { - + padding: 0.2em; } .history_entry_category { } -.history_entry_started { +.history_entry_duration { + color: #892be1; +} +.history_entry_started { + color: #cc661e; } .history_entry_ended { - + color: #9f2727; } + diff --git a/templates/history.hbs b/templates/history.hbs index 25d46b3..6d2acc9 100644 --- a/templates/history.hbs +++ b/templates/history.hbs @@ -14,16 +14,18 @@

stl

-
- {{#each sessions}} -
- +
    + {{#each entries}} +
  • + + for + {{pretty_seconds this.duration.secs}} from - {{pretty_datetime this.1.started}} + {{pretty_datetime this.session.started}} to - {{pretty_datetime this.1.ended}} -
+ {{pretty_datetime this.session.ended}} + {{/each}} -
+ diff --git a/templates/index.hbs b/templates/index.hbs index bec994d..e8edd57 100644 --- a/templates/index.hbs +++ b/templates/index.hbs @@ -46,7 +46,7 @@
{{/each}} -
    +
      {{#each categories}}