use askama::Template; use axum::{ response::{IntoResponse, Response, Html}, http::StatusCode, Extension, }; use diesel::prelude::*; use chrono::{offset::Local, Datelike, Duration, NaiveDate, TimeZone}; use libticker::{ schema::{self, events::dsl::events}, model::Event, config::Config, }; use crate::AppState; fn fix_url(s: &str) -> String { if s.starts_with("http:") || s.starts_with("https:") { s.to_owned() } else { format!("http://{}", s) } } struct DayEvents<'e> { date: NaiveDate, month: &'e str, weekday: &'e str, /// (event, url, color) events: Vec<(&'e Event, Option, &'e str)>, } /// assumes pre-sorted input fn group_by_day<'e>(config: &'e Config, es: &'e [Event]) -> Vec> { let mut results = vec![]; let mut prev_date = None; let mut date_start = 0; for (i, event) in es.iter().enumerate() { if prev_date.is_some() && prev_date != Some(event.dtstart.date()) { if i > date_start { let date = prev_date.unwrap(); results.push(DayEvents { date: date.clone(), month: &config.months[date.month0() as usize], weekday: &config.weekdays[date.weekday().num_days_from_monday() as usize], events: es[date_start..i].iter() .map(|event| { let url = event.url.as_ref().map(|url| fix_url(url)); let color = config.calendars.get(&event.calendar) .map(|calendar| &calendar.color[..]) .unwrap_or("grey"); (event, url, color) }).collect(), }); date_start = i; } } prev_date = Some(event.dtstart.date()); } results } #[derive(Template)] #[template(path = "index.html")] struct IndexTemplate<'a> { days: Vec>, } pub async fn index(Extension(app_state): Extension) -> Response { let config = &app_state.config; let db = app_state.db.lock().unwrap(); let today = Local::now().date_naive().and_hms_opt(0, 0, 0).unwrap(); let limit = Local::now().date_naive().and_hms_opt(0, 0, 0).unwrap() + Duration::days(app_state.config.upcoming_days.into()); let limit_past = Local::now().date_naive().and_hms_opt(0, 0, 0).unwrap() - Duration::days(app_state.config.passed_days.into()); let es = events .filter(schema::events::dtend.ge(&today)) .filter(schema::events::dtstart.ge(&limit_past)) .or_filter(schema::events::dtstart.ge(&today)) .filter(schema::events::dtstart.lt(&limit)) .order_by(schema::events::dtstart.asc()) .then_order_by(schema::events::dtend.desc()) .load::(&*db) .unwrap() .into_iter() .map(|mut e| { if e.location.as_ref().map_or(false, |s| s.is_empty()) { e.location = None; } if e.url.as_ref().map_or(false, |s| s.is_empty()) { e.url = None; } e }) .map(|mut e| { let defaults = config.calendars.get(&e.calendar) .and_then(|calendar| calendar.defaults.as_ref()); let defaults = match defaults { Some(defaults) => defaults, None => return e, }; if e.location.is_none() { e.location = defaults.location.clone(); } if e.url.is_none() { e.url = defaults.url.clone(); } e }).collect::>(); let days = group_by_day(config, &es); let template = IndexTemplate { days, }; match template.render() { Ok(rendered) => Html(rendered).into_response(), Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to render template. Error: {}", e), ).into_response(), } }