aboutsummaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2026-02-27 14:40:15 +0100
committermetamuffin <metamuffin@disroot.org>2026-02-27 14:40:15 +0100
commitc05bfcc2775f0e11db6e856bfcf06d0419c35d54 (patch)
treeffd0e9fcf6b476a6198287085a514cfa7940c200 /ui
parent4ba86694e393c61107e27c4127efc0455b329524 (diff)
downloadjellything-c05bfcc2775f0e11db6e856bfcf06d0419c35d54.tar
jellything-c05bfcc2775f0e11db6e856bfcf06d0419c35d54.tar.bz2
jellything-c05bfcc2775f0e11db6e856bfcf06d0419c35d54.tar.zst
ui changed before object slices
Diffstat (limited to 'ui')
-rw-r--r--ui/client-scripts/src/main.ts1
-rw-r--r--ui/client-scripts/src/pagination.ts18
-rw-r--r--ui/src/components/admin.rs37
-rw-r--r--ui/src/components/admin_log.rs6
-rw-r--r--ui/src/components/home.rs38
-rw-r--r--ui/src/components/items.rs22
-rw-r--r--ui/src/components/login.rs15
-rw-r--r--ui/src/components/message.rs6
-rw-r--r--ui/src/components/mod.rs61
-rw-r--r--ui/src/components/node_card.rs18
-rw-r--r--ui/src/components/node_list.rs47
-rw-r--r--ui/src/components/node_page.rs29
-rw-r--r--ui/src/components/props.rs9
-rw-r--r--ui/src/components/stats.rs36
-rw-r--r--ui/src/components/user.rs7
-rw-r--r--ui/src/lib.rs54
-rw-r--r--ui/src/scaffold.rs26
17 files changed, 217 insertions, 213 deletions
diff --git a/ui/client-scripts/src/main.ts b/ui/client-scripts/src/main.ts
index 303ac71..ba60646 100644
--- a/ui/client-scripts/src/main.ts
+++ b/ui/client-scripts/src/main.ts
@@ -10,3 +10,4 @@ import "./backbutton.ts"
import "./dangerbutton.ts"
import "./log_live.ts"
import "./import_live.ts"
+import "./pagination.ts"
diff --git a/ui/client-scripts/src/pagination.ts b/ui/client-scripts/src/pagination.ts
new file mode 100644
index 0000000..380b20e
--- /dev/null
+++ b/ui/client-scripts/src/pagination.ts
@@ -0,0 +1,18 @@
+
+globalThis.addEventListener("DOMContentLoaded", () => {
+ const el = document.querySelector(".next_page") as HTMLElement
+ if (!el) return
+ const cont = document.body.parentElement!
+ console.log(cont);
+
+ cont.addEventListener("scroll", () => {
+ const end = cont.scrollTop + cont.clientHeight
+ console.log(end, cont.scrollHeight);
+
+ if (end + 1000 > el.scrollHeight) {
+ el.textContent = "Loading more..."
+ el.click()
+ }
+
+ })
+})
diff --git a/ui/src/components/admin.rs b/ui/src/components/admin.rs
index 3cb45d6..76dfe6a 100644
--- a/ui/src/components/admin.rs
+++ b/ui/src/components/admin.rs
@@ -4,9 +4,8 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::RenderInfo;
+use crate::{RenderInfo, page};
use jellycommon::{
- jellyobject::Object,
routes::{
u_admin_import, u_admin_import_post, u_admin_log, u_admin_new_user, u_admin_user,
u_admin_user_remove, u_admin_users,
@@ -15,6 +14,16 @@ use jellycommon::{
};
use jellyui_locale::tr;
+page!(AdminDashboard<'_>, |x| tr(x.ri.lang, "admin.dashboard"));
+page!(AdminImport<'_>, |x| tr(x.ri.lang, "admin.import"));
+page!(AdminUserList<'_>, |x| tr(x.ri.lang, "admin.users"));
+page!(AdminUser<'_>, |x| x
+ .user
+ .get(USER_NAME)
+ .unwrap_or("nameless user")
+ .to_string()
+ .into());
+
markup::define!(
AdminDashboard<'a>(ri: &'a RenderInfo<'a>) {
h1 { @tr(ri.lang, "admin.dashboard") }
@@ -22,23 +31,22 @@ markup::define!(
li{a[href=u_admin_log(true)] { @tr(ri.lang, "admin.log.warnonly") }}
li{a[href=u_admin_log(false)] { @tr(ri.lang, "admin.log.full") }}
}
-
a[href=u_admin_import()] { h2 { @tr(ri.lang, "admin.import") }}
a[href=u_admin_users()] { h2 { @tr(ri.lang, "admin.users") }}
}
- AdminImport<'a>(ri: &'a RenderInfo<'a>, data: Object<'a>) {
- @if data.has(ADMIN_IMPORT_BUSY.0) {
+ AdminImport<'a>(ri: &'a RenderInfo<'a>, errors: &'a [&'a str], busy: bool) {
+ @if *busy {
h1 { @tr(ri.lang, "admin.import.running") }
noscript { "Live import progress needs javascript." }
div[id="admin_import"] {}
} else {
h1 { @tr(ri.lang, "admin.import") }
- @if data.has(ADMIN_IMPORT_ERROR.0) {
+ @if !errors.is_empty() {
section.message.error {
details {
- summary { p.error { @tr(ri.lang, "admin.import_errors").replace("{n}", &data.iter(ADMIN_IMPORT_ERROR).count().to_string()) } }
- ol { @for e in data.iter(ADMIN_IMPORT_ERROR) {
+ summary { p.error { @tr(ri.lang, "admin.import_errors").replace("{n}", &errors.len().to_string()) } }
+ ol { @for e in *errors {
li.error { pre.error { @e } }
}}
}
@@ -53,24 +61,19 @@ markup::define!(
}
}
- AdminInfo<'a>(ri: &'a RenderInfo<'a>, data: Object<'a>) {
- @let _ = ri;
- h2 { @data.get(ADMIN_INFO_TITLE) }
- pre { @data.get(ADMIN_INFO_TEXT) }
- }
-
- AdminUserList<'a>(ri: &'a RenderInfo<'a>, data: Object<'a>) {
+ AdminUserList<'a>(ri: &'a RenderInfo<'a>, users: &'a [User<'a>]) {
h1 { @tr(ri.lang, "admin.users") }
form[method="POST", action=u_admin_new_user()] {
label[for="login"] { "Login: " }
input[type="text", id="login", name="login"]; br;
input[type="submit", value="Create new user"];
}
- ul { @for u in data.iter(ADMIN_USER_LIST_ITEM) {
+ ul { @for u in *users {
li { a[href=u_admin_user(u.get(USER_LOGIN).unwrap_or_default())] { @u.get(USER_LOGIN) } }
}}
}
- AdminUser<'a>(ri: &'a RenderInfo<'a>, user: Object<'a>) {
+
+ AdminUser<'a>(ri: &'a RenderInfo<'a>, user: User<'a>) {
h2 { @user.get(USER_NAME).unwrap_or("nameless user") }
p { @tr(ri.lang, "tag.Ulgn") ": " @user.get(USER_LOGIN) }
form[method="POST", action=u_admin_user_remove(user.get(USER_LOGIN).unwrap())] {
diff --git a/ui/src/components/admin_log.rs b/ui/src/components/admin_log.rs
index abec7fe..f521aae 100644
--- a/ui/src/components/admin_log.rs
+++ b/ui/src/components/admin_log.rs
@@ -4,6 +4,7 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
+use crate::{RenderInfo, page};
use jellycommon::{
internal::{LogLevel, LogLine},
routes::u_admin_log,
@@ -11,8 +12,11 @@ use jellycommon::{
use markup::raw;
use std::fmt::Write;
+page!(ServerLogPage<'_>, |_| "Server Log".into());
+
markup::define! {
- ServerLogPage<'a>(warnonly: bool, messages: &'a [String]) {
+ ServerLogPage<'a>(ri: &'a RenderInfo<'a>, warnonly: bool, messages: &'a [String]) {
+ @let _ = ri;
h1 { "Server Log" }
a[href=u_admin_log(!warnonly)] { @if *warnonly { "Show everything" } else { "Show only warnings" }}
code.log[id="log"] {
diff --git a/ui/src/components/home.rs b/ui/src/components/home.rs
new file mode 100644
index 0000000..5ae6a66
--- /dev/null
+++ b/ui/src/components/home.rs
@@ -0,0 +1,38 @@
+/*
+ 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) 2026 metamuffin <metamuffin.org>
+*/
+
+use crate::{
+ RenderInfo,
+ components::node_card::{NodeCard, NodeCardHightlight},
+ page,
+};
+use jellycommon::Nku;
+use jellyui_locale::tr;
+
+pub enum HomeRow {
+ Inline(Vec<Nku<'a>>),
+ Highlight(Nku<'a>),
+}
+
+page!(Home<'_>, |x| tr(x.ri.lang, "home"));
+
+markup::define! {
+ Home<'a>(ri: &'a RenderInfo<'a>, rows: &'a [(&'a str, HomeRow<'a>)]) {
+ @for (title, row) in *rows {
+ h2 { @tr(ri.lang, title) }
+ @match row {
+ HomeRow::Inline(nkus) => {
+ ul.nl.inline { @for nku in nkus {
+ li { @NodeCard { ri, nku } }
+ }}
+ }
+ HomeRow::Highlight(nku) => {
+ @NodeCardHightlight { ri, nku }
+ }
+ }
+ }
+ }
+}
diff --git a/ui/src/components/items.rs b/ui/src/components/items.rs
new file mode 100644
index 0000000..c9e0d41
--- /dev/null
+++ b/ui/src/components/items.rs
@@ -0,0 +1,22 @@
+/*
+ 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) 2026 metamuffin <metamuffin.org>
+*/
+
+use crate::{RenderInfo, components::node_card::NodeCard, page};
+use jellycommon::{Nku, routes::u_items_cont};
+use jellyui_locale::tr;
+
+page!(Items<'_>, |x| tr(x.ri.lang, "items"));
+
+markup::define! {
+ Items<'a>(ri: &'a RenderInfo<'a>, items: &'a [Nku<'a>], cont: Option<String>) {
+ ul.nl.grid { @for nku in *items {
+ li { @NodeCard { ri, nku } }
+ }}
+ @if let Some(cont) = cont {
+ a.next_page[href=u_items_cont(cont)] { button { "Show more" } }
+ }
+ }
+}
diff --git a/ui/src/components/login.rs b/ui/src/components/login.rs
index d291165..7d19a38 100644
--- a/ui/src/components/login.rs
+++ b/ui/src/components/login.rs
@@ -4,19 +4,24 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use jellycommon::{SETPW_PASSWORD, SETPW_USERNAME, jellyobject::Object};
+use crate::{RenderInfo, page};
use jellyui_locale::tr;
-use crate::RenderInfo;
+page!(AccountLogin<'_>, |x| tr(x.ri.lang, "account.login"));
+page!(AccountLogout<'_>, |x| tr(x.ri.lang, "account.logout"));
+page!(AccountSetPassword<'_>, |x| tr(
+ x.ri.lang,
+ "account.login.set_password"
+));
markup::define! {
- AccountSetPassword<'a>(ri: &'a RenderInfo<'a>, data: Object<'a>) {
+ AccountSetPassword<'a>(ri: &'a RenderInfo<'a>, username: &'a str, password: &'a str) {
form.account[method="POST", action=""] {
h1 { @tr(ri.lang, "account.login.set_password") }
p { @tr(ri.lang, "account.login.set_password.par") }
- input[type="hidden", name="username", value=data.get(SETPW_USERNAME)];
- input[type="hidden", name="password", value=data.get(SETPW_PASSWORD)];
+ input[type="hidden", name="username", value=username];
+ input[type="hidden", name="password", value=password];
label[for="password"] { @tr(ri.lang, "account.new_password") }
input[type="password", id="password", name="new_password"]; br;
diff --git a/ui/src/components/message.rs b/ui/src/components/message.rs
index a271d40..b4499ed 100644
--- a/ui/src/components/message.rs
+++ b/ui/src/components/message.rs
@@ -4,14 +4,12 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
use crate::RenderInfo;
-use jellycommon::{MESSAGE_KIND, MESSAGE_TEXT, jellyobject::Object};
use markup::define;
define! {
- Message<'a>(ri: &'a RenderInfo<'a>, message: Object<'a>) {
+ Message<'a>(ri: &'a RenderInfo<'a>, kind: &'a str, text: &'a str) {
@let _ = ri;
- @let text = message.get(MESSAGE_TEXT).unwrap_or_default();
- @match message.get(MESSAGE_KIND).unwrap_or("neutral") {
+ @match *kind {
"success" => { section.message { p.success { @text } } }
"error" => { section.message { p.error { @text } } }
"neutral" | _ => { section.message { p { @text } } }
diff --git a/ui/src/components/mod.rs b/ui/src/components/mod.rs
index d87efab..6c4cc9d 100644
--- a/ui/src/components/mod.rs
+++ b/ui/src/components/mod.rs
@@ -9,66 +9,9 @@ pub mod admin_log;
pub mod login;
pub mod message;
pub mod node_card;
-pub mod node_list;
pub mod node_page;
pub mod props;
pub mod stats;
pub mod user;
-
-use crate::{
- RenderInfo,
- components::{
- admin::{AdminDashboard, AdminImport, AdminInfo, AdminUser, AdminUserList},
- login::{AccountLogin, AccountLogout, AccountSetPassword},
- message::Message,
- node_list::NodeList,
- node_page::{NodePage, Player},
- user::UserSettings,
- },
-};
-use jellycommon::{jellyobject::Object, *};
-use markup::define;
-
-define! {
- View<'a>(ri: &'a RenderInfo<'a>, view: Object<'a>) {
- @if let Some(message) = view.get(VIEW_MESSAGE) {
- @Message { ri, message }
- }
- @if let Some(nku) = view.get(VIEW_NODE_PAGE) {
- @NodePage { ri, nku }
- }
- @if let Some(nku) = view.get(VIEW_PLAYER) {
- @Player { ri, nku }
- }
- @for nl in view.iter(VIEW_NODE_LIST) {
- @NodeList { ri, nl }
- }
- @if let Some(()) = view.get(VIEW_ACCOUNT_LOGIN) {
- @AccountLogin { ri }
- }
- @if let Some(()) = view.get(VIEW_ACCOUNT_LOGOUT) {
- @AccountLogout{ ri }
- }
- @if let Some(data) = view.get(VIEW_ACCOUNT_SET_PASSWORD) {
- @AccountSetPassword { ri, data }
- }
- @if let Some(()) = view.get(VIEW_ADMIN_DASHBOARD) {
- @AdminDashboard { ri }
- }
- @if let Some(data) = view.get(VIEW_ADMIN_IMPORT) {
- @AdminImport { ri, data }
- }
- @if let Some(data) = view.get(VIEW_ADMIN_INFO) {
- @AdminInfo { ri, data }
- }
- @if let Some(user) = view.get(VIEW_USER_SETTINGS) {
- @UserSettings { ri, user }
- }
- @if let Some(data) = view.get(VIEW_ADMIN_USER_LIST) {
- @AdminUserList { ri, data }
- }
- @if let Some(user) = view.get(VIEW_ADMIN_USER) {
- @AdminUser { ri, user }
- }
- }
-}
+pub mod items;
+pub mod home;
diff --git a/ui/src/components/node_card.rs b/ui/src/components/node_card.rs
index d93825b..e1baec1 100644
--- a/ui/src/components/node_card.rs
+++ b/ui/src/components/node_card.rs
@@ -15,8 +15,8 @@ use jellycommon::{
};
markup::define! {
- NodeCard<'a>(ri: &'a RenderInfo<'a>, nku: Object<'a>) {
- @let node = nku.get(NKU_NODE).unwrap_or_default();
+ NodeCard<'a>(ri: &'a RenderInfo<'a>, nku: &'a Nku<'a>) {
+ @let node = nku.node;
@let slug = node.get(NO_SLUG).unwrap_or_default();
div[class=&format!("card {}", aspect_class(node))] {
.poster {
@@ -27,7 +27,7 @@ markup::define! {
@if node.has(NO_TRACK.0) {
a.play.icon[href=u_node_slug_player(&slug)] { "play_arrow" }
}
- @Props { ri, nku: *nku, full: false }
+ @Props { ri, nku, full: false }
}
}
div.title {
@@ -37,14 +37,14 @@ markup::define! {
}
div.subtitle {
span {
- @nku.get(NKU_ROLE).or(node.get(NO_SUBTITLE))
+ @nku.role.or(node.get(NO_SUBTITLE))
}
}
}
}
- NodeCardWide<'a>(ri: &'a RenderInfo<'a>, nku: Object<'a>) {
- @let node = nku.get(NKU_NODE).unwrap_or_default();
+ NodeCardWide<'a>(ri: &'a RenderInfo<'a>, nku: Nku<'a>) {
+ @let node = nku.node;
@let slug = node.get(NO_SLUG).unwrap_or_default();
div[class="card wide"] {
div[class=&format!("poster {}", aspect_class(node))] {
@@ -59,14 +59,14 @@ markup::define! {
}
div.details {
a.title[href=u_node_slug(&slug)] { @node.get(NO_TITLE) }
- @Props { ri, nku: *nku ,full: false }
+ @Props { ri, nku ,full: false }
span.overview { @node.get(NO_DESCRIPTION) }
}
}
}
- NodeCardHightlight<'a>(ri: &'a RenderInfo<'a>, nku: Object<'a>) {
- @let node = nku.get(NKU_NODE).unwrap_or_default();
+ NodeCardHightlight<'a>(ri: &'a RenderInfo<'a>, nku: &'a Nku<'a>) {
+ @let node = nku.node;
@let slug = node.get(NO_SLUG).unwrap_or_default();
@let backdrop = u_image(node.get(NO_PICTURES).unwrap_or_default().get(PICT_BACKDROP).unwrap_or_default(), 2048);
div[class="card highlight", style=format!("background-image: url(\"{backdrop}\")")] {
diff --git a/ui/src/components/node_list.rs b/ui/src/components/node_list.rs
deleted file mode 100644
index df405ea..0000000
--- a/ui/src/components/node_list.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- 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) 2026 metamuffin <metamuffin.org>
-*/
-
-use crate::{
- RenderInfo,
- components::node_card::{NodeCard, NodeCardHightlight, NodeCardWide},
-};
-use jellycommon::{jellyobject::Object, routes::u_items_cont, *};
-use jellyui_locale::tr;
-
-markup::define! {
- NodeList<'a>(ri: &'a RenderInfo<'a>, nl: Object<'a>) {
- @let ds = nl.get(NODELIST_DISPLAYSTYLE).unwrap_or(NLSTYLE_GRID);
- @if let Some(title) = nl.get(NODELIST_TITLE) {
- h2 { @tr(ri.lang, title) }
- }
- @match ds {
- NLSTYLE_GRID => {
- ul.nl.grid { @for nku in nl.iter(NODELIST_ITEM) {
- li { @NodeCard { ri, nku } }
- }}
- }
- NLSTYLE_INLINE => {
- ul.nl.inline { @for nku in nl.iter(NODELIST_ITEM) {
- li { @NodeCard { ri, nku } }
- }}
- }
- NLSTYLE_LIST => {
- ol.nl.list { @for nku in nl.iter(NODELIST_ITEM) {
- li { @NodeCardWide { ri, nku } }
- }}
- }
- NLSTYLE_HIGHLIGHT => {
- @if let Some(nku) = nl.get(NODELIST_ITEM) {
- @NodeCardHightlight { ri, nku }
- }
- }
- _ => {}
- }
- @if let Some(cont) = nl.get(NODELIST_CONTINUATION) {
- a[href=u_items_cont(cont)] { button { "Show more" } }
- }
- }
-}
diff --git a/ui/src/components/node_page.rs b/ui/src/components/node_page.rs
index f40aa73..5cd71ce 100644
--- a/ui/src/components/node_page.rs
+++ b/ui/src/components/node_page.rs
@@ -4,7 +4,7 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::{RenderInfo, components::props::Props};
+use crate::{RenderInfo, components::props::Props, page};
use jellycommon::{
jellyobject::{Object, Tag, TypedTag},
routes::{u_image, u_node_slug_player},
@@ -13,9 +13,24 @@ use jellycommon::{
use jellyui_locale::tr;
use std::marker::PhantomData;
+page!(NodePage<'_>, |x| x
+ .nku
+ .node
+ .get(NO_TITLE)
+ .unwrap_or_default()
+ .to_string()
+ .into());
+page!(Player<'_>, |x| x
+ .nku
+ .node
+ .get(NO_TITLE)
+ .unwrap_or_default()
+ .to_string()
+ .into());
+
markup::define! {
- NodePage<'a>(ri: &'a RenderInfo<'a>, nku: Object<'a>) {
- @let node = nku.get(NKU_NODE).unwrap_or_default();
+ NodePage<'a>(ri: &'a RenderInfo<'a>, nku: Nku<'a>) {
+ @let node = nku.node;
@let slug = node.get(NO_SLUG).unwrap_or_default();
@let pics = node.get(NO_PICTURES).unwrap_or_default();
@if let Some(path) = pics.get(PICT_BACKDROP) {
@@ -59,7 +74,7 @@ markup::define! {
}
}
.details {
- @Props { ri, nku: *nku, full: true }
+ @Props { ri, nku, full: true }
h3 { @node.get(NO_TAGLINE).unwrap_or_default() }
@if let Some(description) = &node.get(NO_DESCRIPTION) {
p { @for line in description.lines() { @line br; } }
@@ -141,13 +156,11 @@ markup::define! {
// }
}
- Player<'a>(ri: &'a RenderInfo<'a>, nku: Object<'a>) {
+ Player<'a>(ri: &'a RenderInfo<'a>, nku: Nku<'a>) {
@let _ = ri;
- @let node = nku.get(NKU_NODE).unwrap_or_default();
- @let pics = node.get(NO_PICTURES).unwrap_or_default();
+ @let pics = nku.node.get(NO_PICTURES).unwrap_or_default();
video[id="player", poster=pics.get(PICT_COVER).map(|p| u_image(p, 2048))] {}
}
-
}
// fn chapter_key_time(c: Object, dur: f64) -> f64 {
diff --git a/ui/src/components/props.rs b/ui/src/components/props.rs
index 5fa9d3e..e672919 100644
--- a/ui/src/components/props.rs
+++ b/ui/src/components/props.rs
@@ -9,16 +9,13 @@ use crate::{
format::{format_count, format_duration},
};
use chrono::DateTime;
-use jellycommon::{
- jellyobject::{Object, TypedTag},
- *,
-};
+use jellycommon::{jellyobject::TypedTag, *};
use jellyui_locale::tr;
use std::marker::PhantomData;
markup::define! {
- Props<'a>(ri: &'a RenderInfo<'a>, nku: Object<'a>, full: bool) {
- @let node = nku.get(NKU_NODE).unwrap_or_default();
+ Props<'a>(ri: &'a RenderInfo<'a>, nku: &'a Nku<'a>, full: bool) {
+ @let node = nku.node;
.props {
@if let Some(dur) = node.get(NO_DURATION) {
p { @format_duration(dur) }
diff --git a/ui/src/components/stats.rs b/ui/src/components/stats.rs
index 698430b..ce06843 100644
--- a/ui/src/components/stats.rs
+++ b/ui/src/components/stats.rs
@@ -8,27 +8,25 @@ use crate::{
RenderInfo,
format::{format_duration, format_duration_long, format_size},
};
-use jellycommon::{jellyobject::Object, *};
+use jellycommon::*;
use jellyui_locale::tr;
use markup::raw;
markup::define! {
- StatText<'a>(ri: &'a RenderInfo<'a>, stat: Object<'a>) {
+ StatText<'a>(ri: &'a RenderInfo<'a>, stats: &'a Stats) {
h1 { @tr(ri.lang, "stats.title") }
p { @raw(tr(ri.lang, "stats.count")
- .replace("{count}", &format!("<b>{}</b>", stat.get(STAT_COUNT).unwrap_or_default()))
+ .replace("{count}", &format!("<b>{}</b>", stats.all.count))
)}
p { @raw(tr(ri.lang, "stats.runtime")
- .replace("{dur}", &format!("<b>{}</b>", format_duration_long(ri.lang, stat.get(STAT_TOTAL_DURATION).unwrap_or_default())))
- .replace("{size}", &format!("<b>{}</b>", format_size(stat.get(STAT_TOTAL_SIZE).unwrap_or_default())))
+ .replace("{dur}", &format!("<b>{}</b>", format_duration_long(ri.lang, stats.all.sum_duration)))
+ .replace("{size}", &format!("<b>{}</b>", format_size(stats.all.sum_size)))
)}
p { @raw(tr(ri.lang, "stats.average")
- .replace("{dur}", &format!("<b>{}</b>", format_duration(stat.get(STAT_TOTAL_DURATION).unwrap_or_default() / stat.get(STAT_COUNT).unwrap_or_default() as f64)))
- .replace("{size}", &format!("<b>{}</b>", format_size(stat.get(STAT_TOTAL_SIZE).unwrap_or_default() / stat.get(STAT_COUNT).unwrap_or_default())))
+ .replace("{dur}", &format!("<b>{}</b>", format_duration(stats.all.sum_duration / stats.all.count as f64)))
+ .replace("{size}", &format!("<b>{}</b>", format_size(stats.all.sum_size / stats.all.count as u64)))
)}
- }
- StatGroup<'a>(ri: &'a RenderInfo<'a>, statgroup: Object<'a>) {
- h2 { @tr(ri.lang, statgroup.get(STATGROUP_TITLE).unwrap_or_default()) }
+ h2 { @tr(ri.lang, "stats.by_kind") }
table.striped {
tr {
th { @tr(ri.lang, "stats.by_kind.kind") }
@@ -40,15 +38,15 @@ markup::define! {
th { @tr(ri.lang, "stats.by_kind.max_size") }
th { @tr(ri.lang, "stats.by_kind.max_runtime") }
}
- @for stat in statgroup.iter(STATGROUP_BIN) { tr {
- td { @tr(ri.lang, stat.get(STAT_NAME).unwrap_or_default()) }
- td { @stat.get(STAT_COUNT).unwrap_or_default() }
- td { @format_size(stat.get(STAT_TOTAL_SIZE).unwrap_or_default()) }
- td { @format_duration(stat.get(STAT_TOTAL_DURATION).unwrap_or_default()) }
- td { @format_size(stat.get(STAT_TOTAL_SIZE).unwrap_or_default() / stat.get(STAT_COUNT).unwrap_or_default()) }
- td { @format_duration(stat.get(STAT_TOTAL_DURATION).unwrap_or_default() / stat.get(STAT_COUNT).unwrap_or_default() as f64) }
- td { @format_size(stat.get(STAT_MAX_SIZE).unwrap_or_default()) }
- td { @format_duration(stat.get(STAT_MAX_DURATION).unwrap_or_default()) }
+ @for (kind, stat) in &stats.by_kind { tr {
+ td { @tr(ri.lang, &format!("tag.kind.{kind}")) }
+ td { @stat.count }
+ td { @format_size(stat.sum_size) }
+ td { @format_duration(stat.sum_duration) }
+ td { @format_size(stat.sum_size / stat.count as u64) }
+ td { @format_duration(stat.sum_duration / stat.count as f64) }
+ td { @format_size(stat.max_size) }
+ td { @format_duration(stat.max_duration) }
}}
}
}
diff --git a/ui/src/components/user.rs b/ui/src/components/user.rs
index 22b296e..9dabffc 100644
--- a/ui/src/components/user.rs
+++ b/ui/src/components/user.rs
@@ -4,16 +4,17 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::RenderInfo;
+use crate::{RenderInfo, page};
use jellycommon::{
- jellyobject::Object,
routes::{u_account_login, u_account_logout, u_account_settings},
*,
};
use jellyui_locale::tr;
+page!(UserSettings<'_>, |x| tr(x.ri.lang, "settings"));
+
markup::define! {
- UserSettings<'a>(ri: &'a RenderInfo<'a>, user: Object<'a>) {
+ UserSettings<'a>(ri: &'a RenderInfo<'a>, user: User<'a>) {
h1 { @tr(ri.lang, "settings") }
h2 { @tr(ri.lang, "settings.account") }
diff --git a/ui/src/lib.rs b/ui/src/lib.rs
index d74df51..93dd38a 100644
--- a/ui/src/lib.rs
+++ b/ui/src/lib.rs
@@ -3,20 +3,19 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-mod components;
+pub mod components;
pub(crate) mod format;
mod scaffold;
+use std::borrow::Cow;
+
pub use jellyui_client_scripts::*;
pub use jellyui_client_style::*;
-
-pub use components::admin_log::ServerLogPage;
-pub use components::admin_log::render_log_line;
pub use jellyui_locale::tr;
-pub use scaffold::Scaffold;
-use crate::components::View;
-use jellycommon::{jellyobject::Object, *};
+use jellycommon::jellyobject::Object;
+use markup::DynRender;
+pub use scaffold::Scaffold;
use serde::{Deserialize, Serialize};
#[rustfmt::skip]
@@ -27,29 +26,34 @@ pub struct Config {
pub logo: bool,
}
+pub trait Page {
+ fn title(&self) -> Cow<'static, str>;
+ fn ri(&self) -> &RenderInfo<'_>;
+ fn render(&self) -> DynRender<'_>;
+}
+
pub struct RenderInfo<'a> {
pub user: Option<Object<'a>>,
+ pub message: Option<(&'a str, &'a str)>,
pub lang: &'a str,
pub status_message: Option<&'a str>,
pub config: &'a Config,
}
-pub fn render_view(ri: RenderInfo<'_>, view: Object<'_>) -> String {
- let theme = ri
- .user
- .and_then(|u| u.get(USER_THEME_PRESET))
- .unwrap_or(THEME_DARK);
- Scaffold {
- ri: &ri,
- main: View { ri: &ri, view },
- title: view.get(VIEW_TITLE).unwrap_or_default(),
- class: match (theme, view.has(VIEW_PLAYER.0)) {
- (THEME_DARK, true) => "theme-dark player",
- (THEME_DARK, false) => "theme-dark",
- (THEME_LIGHT, true) => "theme-light player",
- (THEME_LIGHT, false) => "theme-light",
- _ => "theme-dark",
- },
- }
- .to_string()
+#[macro_export]
+macro_rules! page {
+ ($t:ty, $title:stmt) => {
+ impl $crate::Page for $t {
+ fn title(&self) -> std::borrow::Cow<'static, str> {
+ let x: fn(&$t) -> std::borrow::Cow<'static, str> = {$title};
+ x(self)
+ }
+ fn ri(&self) -> &$crate::RenderInfo<'_> {
+ self.ri
+ }
+ fn render(&self) -> markup::DynRender<'_> {
+ markup::new!(@self)
+ }
+ }
+ };
}
diff --git a/ui/src/scaffold.rs b/ui/src/scaffold.rs
index c563ee4..61e497a 100644
--- a/ui/src/scaffold.rs
+++ b/ui/src/scaffold.rs
@@ -4,9 +4,9 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::RenderInfo;
+use crate::{Page, RenderInfo, components::message::Message};
use jellycommon::{
- USER_THEME_ACCENT,
+ THEME_DARK, USER_THEME_ACCENT, USER_THEME_PRESET,
routes::{
u_account_login, u_account_settings, u_admin_dashboard, u_home, u_items, u_node_slug,
u_search, u_stats,
@@ -14,21 +14,27 @@ use jellycommon::{
user::{USER_ADMIN, USER_NAME},
};
use jellyui_locale::{escape, tr};
-use markup::{Render, raw};
+use markup::raw;
markup::define! {
- Scaffold<'a, Main: Render>(ri: &'a RenderInfo<'a>, title: &'a str, main: Main, class: &'a str) {
+ Scaffold<'a>(page: &'a dyn Page) {
+ @let ri = page.ri();
@markup::doctype()
html {
head {
- title { @title " - " @ri.config.brand }
+ title { @page.title() " - " @ri.config.brand }
meta[name="viewport", content="width=device-width, initial-scale=1.0"];
link[rel="stylesheet", href="/assets/bundle.css"];
script[src="/assets/bundle.js"] {}
}
- body[class=class, style=format!("--accent-hue: {}", ri.user.and_then(|u|u.get(USER_THEME_ACCENT)).unwrap_or(277))] {
+ @let theme = ri.user.and_then(|u| u.get(USER_THEME_PRESET)).unwrap_or(THEME_DARK);
+ @let hue = ri.user.and_then(|u| u.get(USER_THEME_ACCENT)).unwrap_or(277);
+ body[class=format!("theme-{theme}"), style=format!("--accent-hue: {hue}")] {
@Navbar { ri }
- #main { @main }
+ @if let Some((kind, text)) = ri.message {
+ @Message { ri, kind, text }
+ }
+ #main { @page.render() }
footer {
p { @ri.config.brand " - " @ri.config.slogan " | powered by " a[href="https://codeberg.org/metamuffin/jellything"]{"Jellything"} }
}
@@ -50,11 +56,11 @@ markup::define! {
@if let Some(user) = &ri.user {
span { @raw(tr(ri.lang, "nav.username").replace("{name}", &format!("<b class=\"username\">{}</b>", escape(user.get(USER_NAME).unwrap_or("nameless user"))))) } " "
@if user.has(USER_ADMIN.0) {
- a.admin.hybrid_button[href=u_admin_dashboard()] { p {@tr(ri.lang, "nav.admin")} } " "
+ a.admin[href=u_admin_dashboard()] { p {@tr(ri.lang, "nav.admin")} } " "
}
- a.settings.hybrid_button[href=u_account_settings()] { p {@tr(ri.lang, "nav.settings")} } " "
+ a.settings[href=u_account_settings()] { p {@tr(ri.lang, "nav.settings")} } " "
} else {
- a.login.hybrid_button[href=u_account_login()] { p {@tr(ri.lang, "nav.login")} }
+ a.login[href=u_account_login()] { p {@tr(ri.lang, "nav.login")} }
}
}
}