/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ use crate::{ routes::{ locale::lang_from_request, ui::{ account::{ rocket_uri_macro_r_account_login, rocket_uri_macro_r_account_logout, rocket_uri_macro_r_account_register, session::Session, settings::rocket_uri_macro_r_account_settings, }, admin::rocket_uri_macro_r_admin_dashboard, browser::rocket_uri_macro_r_all_items, node::rocket_uri_macro_r_library_node, search::rocket_uri_macro_r_search, stats::rocket_uri_macro_r_stats, }, }, uri, }; use futures::executor::block_on; use jellybase::{ locale::{tr, Language}, CONF, }; use jellycommon::user::Theme; use jellycommon::NodeID; use jellyimport::is_importing; use markup::{DynRender, Render, RenderAttributeValue}; use rocket::{ http::ContentType, response::{self, Responder}, Request, Response, }; use std::{borrow::Cow, io::Cursor, sync::LazyLock}; static LOGO_ENABLED: LazyLock = LazyLock::new(|| CONF.asset_path.join("logo.svg").exists()); pub struct TrString<'a>(Cow<'a, str>); impl Render for TrString<'_> { fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result { self.0.as_str().render(writer) } } impl RenderAttributeValue for TrString<'_> { fn is_none(&self) -> bool { false } fn is_true(&self) -> bool { false } fn is_false(&self) -> bool { false } } pub fn escape(str: &str) -> String { let mut o = String::with_capacity(str.len()); let mut last = 0; for (index, byte) in str.bytes().enumerate() { if let Some(esc) = match byte { b'<' => Some("<"), b'>' => Some(">"), b'&' => Some("&"), b'"' => Some("""), _ => None, } { o += &str[last..index]; o += esc; last = index + 1; } } o += &str[last..]; o } pub fn trs<'a>(lang: &Language, key: &str) -> TrString<'a> { TrString(tr(*lang, key)) } markup::define! { Layout<'a, Main: Render>(title: String, main: Main, class: &'a str, session: Option, lang: Language) { @markup::doctype() html { head { title { @title " - " @CONF.brand } meta[name="viewport", content="width=device-width, initial-scale=1.0"]; link[rel="stylesheet", href="/assets/style.css"]; script[src="/assets/bundle.js"] {} } body[class=class] { nav { h1 { a[href=if session.is_some() {"/home"} else {"/"}] { @if *LOGO_ENABLED { img.logo[src="/assets/logo.svg"]; } else { @CONF.brand } } } " " @if let Some(_) = session { a.library[href=uri!(r_library_node("library"))] { @trs(lang, "nav.root") } " " a.library[href=uri!(r_all_items())] { @trs(lang, "nav.all") } " " a.library[href=uri!(r_search(None::<&'static str>, None::))] { @trs(lang, "nav.search") } " " a.library[href=uri!(r_stats())] { @trs(lang, "nav.stats") } " " } @if is_importing() { span.warn { "Library database is updating..." } } div.account { @if let Some(session) = session { span { "Logged in as " } span.username { @session.user.display_name } " " @if session.user.admin { a.admin.hybrid_button[href=uri!(r_admin_dashboard())] { p {@trs(lang, "nav.admin")} } " " } a.settings.hybrid_button[href=uri!(r_account_settings())] { p {@trs(lang, "nav.settings")} } " " a.logout.hybrid_button[href=uri!(r_account_logout())] { p {@trs(lang, "nav.logout")} } } else { a.register.hybrid_button[href=uri!(r_account_register())] { p {@trs(lang, "nav.register")} } " " a.login.hybrid_button[href=uri!(r_account_login())] { p {@trs(lang, "nav.login")} } } } } #main { @main } footer { p { @CONF.brand " - " @CONF.slogan " | powered by " a[href="https://codeberg.org/metamuffin/jellything"]{"Jellything"} } } } } } FlashDisplay(flash: Option>) { @if let Some(flash) = &flash { @match flash { Ok(mesg) => { section.message { p.success { @mesg } } } Err(err) => { section.message { p.error { @err } } } } } } } pub type DynLayoutPage<'a> = LayoutPage>; pub struct LayoutPage { pub title: String, pub class: Option<&'static str>, pub content: T, } impl Default for LayoutPage> { fn default() -> Self { Self { class: None, content: markup::new!(), title: String::new(), } } } impl<'r, Main: Render> Responder<'r, 'static> for LayoutPage
{ fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { // TODO blocking the event loop here. it seems like there is no other way to // TODO offload this, since the guard references `req` which has a lifetime. // TODO therefore we just block. that is fine since the database is somewhat fast. let lang = lang_from_request(&req); let session = block_on(req.guard::>()).unwrap(); let mut out = String::new(); Layout { main: self.content, title: self.title, class: &format!( "{} theme-{:?}", self.class.unwrap_or(""), session .as_ref() .map(|s| s.user.theme) .unwrap_or(Theme::Dark) ), session, lang, } .render(&mut out) .unwrap(); Response::build() .header(ContentType::HTML) .streamed_body(Cursor::new(out)) .ok() } }