From 11a585b3dbe620dcc8772e713b22f1d9ba80d598 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Sun, 27 Apr 2025 19:25:11 +0200 Subject: move files around --- server/src/ui/layout.rs | 182 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 server/src/ui/layout.rs (limited to 'server/src/ui/layout.rs') diff --git a/server/src/ui/layout.rs b/server/src/ui/layout.rs new file mode 100644 index 0000000..0e8d7b9 --- /dev/null +++ b/server/src/ui/layout.rs @@ -0,0 +1,182 @@ +/* + 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::{ + locale::lang_from_request, + logic::session::Session, + ui::{ + account::{ + rocket_uri_macro_r_account_login, rocket_uri_macro_r_account_logout, + rocket_uri_macro_r_account_register, 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::{raw, 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 { @raw(tr(*lang, "nav.username").replace("{name}", &format!("{}", escape(&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() + } +} -- cgit v1.2.3-70-g09d2