1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
/*
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 <metamuffin.org>
*/
use crate::{
routes::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::CONF;
use jellycommon::user::Theme;
use jellycommon::NodeID;
use jellyimport::is_importing;
use markup::{DynRender, Render};
use rocket::{
http::ContentType,
response::{self, Responder},
Request, Response,
};
use std::{io::Cursor, sync::LazyLock};
static LOGO_ENABLED: LazyLock<bool> = LazyLock::new(|| CONF.asset_path.join("logo.svg").exists());
markup::define! {
Layout<'a, Main: Render>(title: String, main: Main, class: &'a str, session: Option<Session>) {
@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"))] { "My Library" } " "
a.library[href=uri!(r_all_items())] { "All Items" } " "
a.library[href=uri!(r_search(None::<&'static str>, None::<usize>))] { "Search" } " "
a.library[href=uri!(r_stats())] { "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 {"Administration"} } " "
}
a.settings.hybrid_button[href=uri!(r_account_settings())] { p {"Settings"} } " "
a.logout.hybrid_button[href=uri!(r_account_logout())] { p {"Log out"} }
} else {
a.register.hybrid_button[href=uri!(r_account_register())] { p {"Register"} } " "
a.login.hybrid_button[href=uri!(r_account_login())] { p {"Log in"} }
}
}
}
#main { @main }
footer {
p { @CONF.brand " - " @CONF.slogan " | powered by " a[href="https://codeberg.org/metamuffin/jellything"]{"Jellything"} }
}
}
}
}
FlashDisplay(flash: Option<Result<String, String>>) {
@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<markup::DynRender<'a>>;
pub struct LayoutPage<T> {
pub title: String,
pub class: Option<&'static str>,
pub content: T,
}
impl Default for LayoutPage<DynRender<'_>> {
fn default() -> Self {
Self {
class: None,
content: markup::new!(),
title: String::new(),
}
}
}
impl<'r, Main: Render> Responder<'r, 'static> for LayoutPage<Main> {
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 session = block_on(req.guard::<Option<Session>>()).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,
}
.render(&mut out)
.unwrap();
Response::build()
.header(ContentType::HTML)
.streamed_body(Cursor::new(out))
.ok()
}
}
|