aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2026-01-23 04:19:24 +0100
committermetamuffin <metamuffin@disroot.org>2026-01-23 04:19:24 +0100
commit3671a4e07565c86f8071fb2309f463aeaf684ba3 (patch)
tree9a9057c7dcc174ada17a45a195502ff94b2f2946
parent10cdaaa30a6b4a187797434dc8d959780f0e8fbf (diff)
downloadjellything-3671a4e07565c86f8071fb2309f463aeaf684ba3.tar
jellything-3671a4e07565c86f8071fb2309f463aeaf684ba3.tar.bz2
jellything-3671a4e07565c86f8071fb2309f463aeaf684ba3.tar.zst
move ui code around
-rw-r--r--Cargo.lock1
-rw-r--r--common/object/src/registry.rs6
-rw-r--r--common/src/api.rs17
-rw-r--r--common/src/routes.rs4
-rw-r--r--server/Cargo.toml2
-rw-r--r--server/src/compat/jellyfin/mod.rs6
-rw-r--r--server/src/main.rs4
-rw-r--r--ui/src/components/message.rs20
-rw-r--r--ui/src/components/mod.rs22
-rw-r--r--ui/src/components/node_list.rs0
-rw-r--r--ui/src/components/node_page.rs194
-rw-r--r--ui/src/components/props.rs (renamed from ui/src/props.rs)23
-rw-r--r--ui/src/components/stats.rs55
-rw-r--r--ui/src/error.rs27
-rw-r--r--ui/src/format.rs14
-rw-r--r--ui/src/lib.rs88
-rw-r--r--ui/src/locale.rs6
-rw-r--r--ui/src/node_page.rs218
-rw-r--r--ui/src/old/account/mod.rs (renamed from ui/src/account/mod.rs)0
-rw-r--r--ui/src/old/account/settings.rs (renamed from ui/src/account/settings.rs)0
-rw-r--r--ui/src/old/admin/import.rs (renamed from ui/src/admin/import.rs)0
-rw-r--r--ui/src/old/admin/log.rs (renamed from ui/src/admin/log.rs)0
-rw-r--r--ui/src/old/admin/mod.rs (renamed from ui/src/admin/mod.rs)0
-rw-r--r--ui/src/old/admin/user.rs (renamed from ui/src/admin/user.rs)0
-rw-r--r--ui/src/old/filter_sort.rs (renamed from ui/src/filter_sort.rs)0
-rw-r--r--ui/src/old/home.rs (renamed from ui/src/home.rs)0
-rw-r--r--ui/src/old/items.rs (renamed from ui/src/items.rs)0
-rw-r--r--ui/src/old/node_card.rs (renamed from ui/src/node_card.rs)0
-rw-r--r--ui/src/old/search.rs (renamed from ui/src/search.rs)0
-rw-r--r--ui/src/scaffold.rs30
-rw-r--r--ui/src/stats.rs79
31 files changed, 363 insertions, 453 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 413071f..246f3a6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1945,7 +1945,6 @@ dependencies = [
"jellyimport",
"jellystream",
"jellytranscoder",
- "jellyui",
"log",
"rand 0.9.2",
"rocket",
diff --git a/common/object/src/registry.rs b/common/object/src/registry.rs
index 2cd2a1f..d9da2fb 100644
--- a/common/object/src/registry.rs
+++ b/common/object/src/registry.rs
@@ -34,10 +34,10 @@ impl Registry {
pub fn info(&self, tag: Tag) -> Option<&TagInfo> {
self.tags.get(&tag)
}
- pub fn name(&self, tag: Tag) -> String {
+ pub fn name(&self, tag: Tag) -> &str {
match self.tags.get(&tag) {
- Some(inf) => inf.name.to_string(),
- None => format!("unknown_tag_{:04x}", tag.0),
+ Some(inf) => inf.name,
+ None => "unknown",
}
}
}
diff --git a/common/src/api.rs b/common/src/api.rs
index 2123477..ee26020 100644
--- a/common/src/api.rs
+++ b/common/src/api.rs
@@ -14,10 +14,12 @@ fields! {
QUERY_SORT_ASCENDING: () = 2003 "sort_ascending";
VIEW_TITLE: &str = 2005 "title";
- VIEW_MESSAGE: &str = 2010 "message";
+ VIEW_MESSAGE: Object = 2010 "message";
VIEW_NODE_PAGE: Object = 2011 "node_page";
VIEW_NODE_LIST: Object = 2012 "node_list"; // multi
VIEW_PLAYER: u64 = 2028 "player";
+ VIEW_STATGROUP: Object = 2041 "statgroup";
+ VIEW_STATTEXT: Object = 2042 "stattext";
NKU_NODE: Object = 2025 "node";
NKU_UDATA: Object = 2026 "udata";
@@ -26,6 +28,19 @@ fields! {
NODELIST_TITLE: &str = 2007 "title";
NODELIST_DISPLAYSTYLE: &str = 2008 "displaystyle";
NODELIST_ITEM: &str = 2009 "item";
+
+ MESSAGE_KIND: &str = 2029 "kind";
+ MESSAGE_TEXT: &str = 2030 "text";
+
+ STATGROUP_TITLE: &str = 2039 "title";
+ STATGROUP_BIN: Object = 2040 "bin";
+
+ STAT_NAME: &str = 2038 "name";
+ STAT_COUNT: u64 = 2031 "count";
+ STAT_TOTAL_SIZE: u64 = 2032 "total_size";
+ STAT_TOTAL_DURATION: f64 = 2033 "total_duration";
+ STAT_MAX_SIZE: u64 = 2036 "max_size";
+ STAT_MAX_DURATION: f64 = 2037 "max_duration";
}
enums! {
diff --git a/common/src/routes.rs b/common/src/routes.rs
index 48f975a..1d3a8da 100644
--- a/common/src/routes.rs
+++ b/common/src/routes.rs
@@ -18,8 +18,8 @@ pub fn u_node_slug_player(node: &str) -> String {
pub fn u_node_slug_player_time(node: &str, time: f64) -> String {
format!("/n/{node}/player?t={time}")
}
-pub fn u_node_image(node: &str, slot: &str, size: usize) -> String {
- format!("/n/{node}/image/{slot}?size={size}")
+pub fn u_image(path: &str, size: usize) -> String {
+ format!("/image/{path}?size={size}")
}
pub fn u_node_slug_watched(node: &str, state: &str) -> String {
format!("/n/{node}/watched?state={state}")
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 416ff8e..01e918e 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -9,7 +9,7 @@ jellystream = { path = "../stream" }
jellytranscoder = { path = "../transcoder" }
jellyimport = { path = "../import" }
jellycache = { path = "../cache" }
-jellyui = { path = "../ui" }
+# jellyui = { path = "../ui" }
anyhow = { workspace = true }
async-recursion = "1.1.1"
diff --git a/server/src/compat/jellyfin/mod.rs b/server/src/compat/jellyfin/mod.rs
index 7978f3b..db60530 100644
--- a/server/src/compat/jellyfin/mod.rs
+++ b/server/src/compat/jellyfin/mod.rs
@@ -9,7 +9,7 @@ use crate::{helper::A, ui::error::MyResult};
use anyhow::anyhow;
use jellycommon::{
api::{NodeFilterSort, SortOrder, SortProperty},
- routes::{u_asset, u_node_image},
+ routes::{u_asset, u_image},
stream::{StreamContainer, StreamSpec},
user::{NodeUserData, WatchedState},
MediaInfo, Node, NodeID, NodeKind, PictureSlot, SourceTrack, SourceTrackKind, Visibility,
@@ -169,7 +169,7 @@ pub fn r_jellyfin_items_image_primary(
tag: String,
) -> Redirect {
if tag == "poster" {
- Redirect::permanent(u_node_image(
+ Redirect::permanent(u_image(
id,
PictureSlot::Cover,
fillWidth.unwrap_or(1024),
@@ -186,7 +186,7 @@ pub fn r_jellyfin_items_images_backdrop(
id: &str,
maxWidth: Option<usize>,
) -> Redirect {
- Redirect::permanent(u_node_image(
+ Redirect::permanent(u_image(
id,
PictureSlot::Backdrop,
maxWidth.unwrap_or(1024),
diff --git a/server/src/main.rs b/server/src/main.rs
index 058fdc3..8b0c128 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -8,7 +8,6 @@
#![recursion_limit = "4096"]
use config::load_config;
-use jellylogic::{admin::log::enable_logging, login::create_admin_account};
use log::{error, info, warn};
use routes::build_rocket;
use serde::{Deserialize, Serialize};
@@ -23,12 +22,11 @@ pub mod logic;
pub mod routes;
pub mod ui;
-#[rustfmt::skip]
#[derive(Debug, Deserialize, Serialize, Default)]
pub struct Config {
asset_path: PathBuf,
cookie_key: Option<String>,
- tls:bool,
+ tls: bool,
hostname: String,
}
diff --git a/ui/src/components/message.rs b/ui/src/components/message.rs
new file mode 100644
index 0000000..a271d40
--- /dev/null
+++ b/ui/src/components/message.rs
@@ -0,0 +1,20 @@
+/*
+ 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;
+use jellycommon::{MESSAGE_KIND, MESSAGE_TEXT, jellyobject::Object};
+use markup::define;
+
+define! {
+ Message<'a>(ri: &'a RenderInfo<'a>, message: Object<'a>) {
+ @let _ = ri;
+ @let text = message.get(MESSAGE_TEXT).unwrap_or_default();
+ @match message.get(MESSAGE_KIND).unwrap_or("neutral") {
+ "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
new file mode 100644
index 0000000..4d2dd62
--- /dev/null
+++ b/ui/src/components/mod.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>
+*/
+
+pub mod message;
+pub mod node_page;
+pub mod props;
+pub mod stats;
+
+use crate::{RenderInfo, components::message::Message};
+use jellycommon::{VIEW_MESSAGE, 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 }
+ }
+ }
+}
diff --git a/ui/src/components/node_list.rs b/ui/src/components/node_list.rs
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ui/src/components/node_list.rs
diff --git a/ui/src/components/node_page.rs b/ui/src/components/node_page.rs
new file mode 100644
index 0000000..4a594d6
--- /dev/null
+++ b/ui/src/components/node_page.rs
@@ -0,0 +1,194 @@
+/*
+ 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::props::Props, locale::tr};
+use jellycommon::{
+ jellyobject::{Object, Tag, TypedTag},
+ routes::{u_image, u_node_slug_player},
+ *,
+};
+use std::marker::PhantomData;
+
+markup::define! {
+ NodePage<'a>(ri: &'a RenderInfo<'a>, nku: Object<'a>) {
+ @let node = nku.get(NKU_NODE).unwrap_or_default();
+ @let slug = node.get(NO_SLUG).unwrap_or_default();
+ @if let Some(path) = node.get(NO_PICTURES).unwrap_or_default().get(PICT_BACKDROP) {
+ img.backdrop[src=u_image(path, 2048)];
+ }
+ @if let Some(path) = node.get(NO_PICTURES).unwrap_or_default().get(PICT_COVER) {
+ @let cls = format!("bigposter {}", aspect_class(node));
+ div[class=cls] { img[src=u_image(path, 2048), loading="lazy"]; }
+ }
+ .title {
+ h1 { @node.get(NO_TITLE).unwrap_or_default() }
+ // ul.parents { @for (node, _) in *parents { li {
+ // a.component[href=u_node_slug(&node.slug)] { @node.title }
+ // }}}
+ @if node.has(NO_TRACK.0) {
+ a.play[href=u_node_slug_player(slug)] { @tr(ri.lang, "node.player_link") }
+ // @if matches!(udata.watched, WatchedState::None | WatchedState::Pending | WatchedState::Progress(_)) {
+ // form.mark_watched[method="POST", action=u_node_slug_watched(&node.slug, ApiWatchedState::Watched)] {
+ // input[type="submit", value=trs(lang, "node.watched.set")];
+ // }
+ // }
+ // @if matches!(udata.watched, WatchedState::Watched) {
+ // form.mark_unwatched[method="POST", action=u_node_slug_watched(&node.slug, ApiWatchedState::None)] {
+ // input[type="submit", value=trs(lang, "node.watched.unset")];
+ // }
+ // }
+ // @if matches!(udata.watched, WatchedState::None) {
+ // form.mark_unwatched[method="POST", action=u_node_slug_watched(&node.slug, ApiWatchedState::Pending)] {
+ // input[type="submit", value=trs(lang, "node.watchlist.set")];
+ // }
+ // }
+ // @if matches!(udata.watched, WatchedState::Pending) {
+ // form.mark_unwatched[method="POST", action=u_node_slug_watched(&node.slug, ApiWatchedState::None)] {
+ // input[type="submit", value=trs(lang, "node.watchlist.unset")];
+ // }
+ // }
+ // form.rating[method="POST", action=u_node_slug_update_rating(&node.slug)] {
+ // input[type="range", name="rating", min=-10, max=10, step=1, value=udata.rating];
+ // input[type="submit", value=trs(lang, "node.update_rating")];
+ // }
+ }
+ }
+ .details {
+ @Props { ri, nku: *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; } }
+ }
+ // @if !media.chapters.is_empty() {
+ // h2 { @trs(lang, "node.chapters") }
+ // ul.children.hlist { @for chap in &media.chapters {
+ // @let (inl, sub) = format_chapter(chap);
+ // li { .card."aspect-thumb" {
+ // .poster {
+ // a[href=u_node_slug_player_time(&node.slug, chap.time_start.unwrap_or(0.))] {
+ // img[src=u_node_slug_thumbnail(&node.slug, chapter_key_time(chap, media.duration), 1024), loading="lazy"];
+ // }
+ // .cardhover { .props { p { @inl } } }
+ // }
+ // .title { span { @sub } }
+ // }}
+ // }}
+ // }
+ // @if !node.credits.is_empty() {
+ // h2 { @trs(lang, "node.people") }
+ // @for (group, people) in &node.credits {
+ // details[open=group==&CreditCategory::Cast] {
+ // summary { h3 { @format!("{}", group) } }
+ // ul.children.hlist { @for (i, pe) in people.iter().enumerate() {
+ // li { .card."aspect-port" {
+ // .poster {
+ // a[href="#"] {
+ // img[src=u_node_slug_person_asset(&node.slug, *group, i, 1024), loading="lazy"];
+ // }
+ // }
+ // .title {
+ // // TODO span { @pe.person.name } br;
+ // @if let Some(c) = pe.characters.first() {
+ // span.subtitle { @c }
+ // }
+ // @if let Some(c) = pe.jobs.first() {
+ // span.subtitle { @c }
+ // }
+ // }
+ // }}
+ // }}
+ // }
+ // }
+ // }
+ details {
+ summary { @tr(ri.lang, "media.tracks") }
+ ol { @for track in node.iter(NO_TRACK) {
+ li { "track" @track.get(TR_NAME) }
+ }}
+ }
+ @if let Some(idents) = node.get(NO_IDENTIFIERS) {
+ details {
+ summary { @tr(ri.lang, "node.external_ids") }
+ table {
+ @for (key, value) in idents.entries::<&str>() { tr {
+ tr {
+ td { @tr(ri.lang, &format!("id.{}", TAGREG.name(key))) }
+ @if let Some(url) = external_id_url(key, value) {
+ td { a[href=url] { pre { @value } } }
+ } else {
+ td { pre { @value } }
+ }
+ }
+ }}
+ }
+ }
+ }
+ @if node.has(NO_TAG.0) {
+ details {
+ summary { @tr(ri.lang, "node.tags") }
+ ol { @for tag in node.iter(NO_TAG) {
+ li { @tag }
+ }}
+ }
+ }
+ }
+ // @if matches!(node.kind, NodeKind::Collection | NodeKind::Channel) {
+ // @NodeFilterSortForm { f: filter, lang }
+ // }
+ // @if !similar.is_empty() {
+ // h2 { @trs(lang, "node.similar") }
+ // ul.children.hlist {@for (node, udata) in similar.iter() {
+ // li { @NodeCard { node, udata, lang } }
+ // }}
+ // }
+ // @match node.kind {
+ // NodeKind::Show | NodeKind::Series | NodeKind::Season => {
+ // ol { @for (node, udata) in children.iter() {
+ // li { @NodeCardWide { node, udata, lang } }
+ // }}
+ // }
+ // NodeKind::Collection | NodeKind::Channel | _ => {
+ // ul.children {@for (node, udata) in children.iter() {
+ // li { @NodeCard { node, udata, lang } }
+ // }}
+ // }
+ // }
+ // }
+ }
+}
+
+fn chapter_key_time(c: Object, dur: f64) -> f64 {
+ let start = c.get(CH_START).unwrap_or(0.);
+ let end = c.get(CH_END).unwrap_or(dur);
+ start * 0.8 + end * 0.2
+}
+
+pub fn aspect_class(node: Object<'_>) -> &'static str {
+ let kind = node.get(NO_KIND).unwrap_or(KIND_COLLECTION);
+ match kind {
+ KIND_VIDEO | KIND_EPISODE => "aspect-thumb",
+ KIND_COLLECTION => "aspect-land",
+ KIND_SEASON | KIND_SHOW | KIND_SERIES | KIND_MOVIE | KIND_SHORTFORMVIDEO => "aspect-port",
+ KIND_CHANNEL | KIND_MUSIC | _ => "aspect-square",
+ }
+}
+
+fn external_id_url(key: Tag, value: &str) -> Option<String> {
+ Some(match TypedTag(key, PhantomData) {
+ IDENT_YOUTUBE_VIDEO => format!("https://youtube.com/watch?v={value}"),
+ IDENT_YOUTUBE_CHANNEL => format!("https://youtube.com/channel/{value}"),
+ IDENT_YOUTUBE_CHANNEL_HANDLE => format!("https://youtube.com/channel/@{value}"),
+ IDENT_MUSICBRAINZ_RELEASE => format!("https://musicbrainz.org/release/{value}"),
+ IDENT_MUSICBRAINZ_ARTIST => format!("https://musicbrainz.org/artist/{value}"),
+ IDENT_MUSICBRAINZ_RELEASE_GROUP => {
+ format!("https://musicbrainz.org/release-group/{value}")
+ }
+ IDENT_MUSICBRAINZ_RECORDING => {
+ format!("https://musicbrainz.org/recording/{value}")
+ }
+ _ => return None,
+ })
+}
diff --git a/ui/src/props.rs b/ui/src/components/props.rs
index fe7f419..f23d72e 100644
--- a/ui/src/props.rs
+++ b/ui/src/components/props.rs
@@ -5,41 +5,44 @@
*/
use crate::{
+ RenderInfo,
format::{format_count, format_duration},
locale::tr,
- node_page::NodeUdata,
- scaffold::RenderInfo,
};
use chrono::DateTime;
-use jellycommon::{jellyobject::TypedTag, *};
+use jellycommon::{
+ jellyobject::{Object, TypedTag},
+ *,
+};
use std::marker::PhantomData;
markup::define! {
- Props<'a>(ri: &'a RenderInfo<'a>, nodeu: NodeUdata<'a>, full: bool) {
+ Props<'a>(ri: &'a RenderInfo<'a>, nku: Object<'a>, full: bool) {
+ @let node = nku.get(NKU_NODE).unwrap_or_default();
.props {
- @if let Some(dur) = nodeu.node.get(NO_DURATION) {
+ @if let Some(dur) = node.get(NO_DURATION) {
p { @format_duration(dur) }
}
- // @if let Some(res) = nodeu.node.get(NO_TRACK) {
+ // @if let Some(res) = node.get(NO_TRACK) {
// p { @m.resolution_name() }
// }
- @if let Some(d) = nodeu.node.get(NO_RELEASEDATE) {
+ @if let Some(d) = node.get(NO_RELEASEDATE) {
p { @if *full {
@DateTime::from_timestamp_millis(d).unwrap().naive_utc().to_string()
} else {
@DateTime::from_timestamp_millis(d).unwrap().date_naive().to_string()
}}
}
- @match nodeu.node.get(NO_VISIBILITY).unwrap_or(VISI_VISIBLE) {
- VISI_VISIBLE => {}
+ @match node.get(NO_VISIBILITY).unwrap_or(VISI_VISIBLE) {
VISI_REDUCED => {p.visibility{@tr(ri.lang, "prop.vis.reduced")}}
VISI_HIDDEN => {p.visibility{@tr(ri.lang, "prop.vis.hidden")}}
+ VISI_VISIBLE | _ => {}
}
// TODO
// @if !node.children.is_empty() {
// p { @format!("{} items", node.children.len()) }
// }
- @for (kind, value) in nodeu.node.get(NO_RATINGS).unwrap_or_default().entries::<f64>() {
+ @for (kind, value) in node.get(NO_RATINGS).unwrap_or_default().entries::<f64>() {
@match TypedTag(kind, PhantomData) {
RTYP_YOUTUBE_LIKES => {p.likes{ @format_count(value as usize) " Likes" }}
RTYP_YOUTUBE_VIEWS => {p{ @format_count(value as usize) " Views" }}
diff --git a/ui/src/components/stats.rs b/ui/src/components/stats.rs
new file mode 100644
index 0000000..8dfb304
--- /dev/null
+++ b/ui/src/components/stats.rs
@@ -0,0 +1,55 @@
+/*
+ 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,
+ format::{format_duration, format_duration_long, format_size},
+ locale::tr,
+};
+use jellycommon::{jellyobject::Object, *};
+use markup::raw;
+
+markup::define! {
+ StatText<'a>(ri: &'a RenderInfo<'a>, stat: Object<'a>) {
+ 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()))
+ )}
+ 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())))
+ )}
+ 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())))
+ )}
+ }
+ StatGroup<'a>(ri: &'a RenderInfo<'a>, statgroup: Object<'a>) {
+ h2 { @tr(ri.lang, statgroup.get(STATGROUP_TITLE).unwrap_or_default()) }
+ table.striped {
+ tr {
+ th { @tr(ri.lang, "stats.by_kind.kind") }
+ th { @tr(ri.lang, "stats.by_kind.count") }
+ th { @tr(ri.lang, "stats.by_kind.total_size") }
+ th { @tr(ri.lang, "stats.by_kind.total_runtime") }
+ th { @tr(ri.lang, "stats.by_kind.average_size") }
+ th { @tr(ri.lang, "stats.by_kind.average_runtime") }
+ 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()) }
+ }}
+ }
+ }
+}
diff --git a/ui/src/error.rs b/ui/src/error.rs
deleted file mode 100644
index 23d323a..0000000
--- a/ui/src/error.rs
+++ /dev/null
@@ -1,27 +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::Page;
-use jellycommon::routes::u_account_login;
-
-impl Page for ErrorPage {
- fn title(&self) -> String {
- "Error".to_string()
- }
- fn to_render(&self) -> markup::DynRender<'_> {
- markup::new!(@self)
- }
-}
-
-markup::define! {
- ErrorPage(status: String) {
- h2 { "Error" }
- p { @status }
- // @if status == Status::NotFound {
- p { "You might need to " a[href=u_account_login()] { "log in" } ", to see this page" }
- // }
- }
-}
diff --git a/ui/src/format.rs b/ui/src/format.rs
index 811211a..01982af 100644
--- a/ui/src/format.rs
+++ b/ui/src/format.rs
@@ -13,12 +13,12 @@ use crate::locale::tr;
use std::fmt::Write;
pub fn format_duration(d: f64) -> String {
- format_duration_mode(d, false, LANG_ENG.0)
+ format_duration_mode(LANG_ENG.0, d, false)
}
-pub fn format_duration_long(d: f64, lang: Language) -> String {
- format_duration_mode(d, true, lang)
+pub fn format_duration_long(lang: Language, d: f64) -> String {
+ format_duration_mode(lang, d, true)
}
-fn format_duration_mode(mut d: f64, long_units: bool, lang: Language) -> String {
+fn format_duration_mode(lang: Language, mut d: f64, long_units: bool) -> String {
let mut s = String::new();
let sign = if d > 0. { "" } else { "-" };
d = d.abs();
@@ -58,15 +58,15 @@ fn test_duration_short() {
#[test]
fn test_duration_long() {
assert_eq!(
- format_duration_long(61., LANG_ENG.0).as_str(),
+ format_duration_long(LANG_ENG.0, 61.).as_str(),
"1 minute and 1 second"
);
assert_eq!(
- format_duration_long(121., LANG_ENG.0).as_str(),
+ format_duration_long(LANG_ENG.0, 121.).as_str(),
"2 minutes and 1 second"
);
assert_eq!(
- format_duration_long(3661., LANG_ENG.0).as_str(),
+ format_duration_long(LANG_ENG.0, 3661.).as_str(),
"1 hour, 1 minute and 1 second"
);
}
diff --git a/ui/src/lib.rs b/ui/src/lib.rs
index 6dbc837..72109d4 100644
--- a/ui/src/lib.rs
+++ b/ui/src/lib.rs
@@ -3,28 +3,17 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-pub mod account;
-pub mod admin;
-pub mod error;
-pub mod filter_sort;
-pub mod format;
-pub mod home;
-pub mod items;
-pub mod locale;
-pub mod node_card;
-pub mod node_page;
-pub mod props;
-pub mod scaffold;
-pub mod search;
-pub mod stats;
+mod components;
+pub(crate) mod format;
+pub(crate) mod locale;
+mod scaffold;
-use markup::DynRender;
-use scaffold::{RenderInfo, Scaffold};
-use serde::{Deserialize, Serialize};
-use std::{
- path::PathBuf,
- sync::{LazyLock, Mutex},
+use crate::{components::View, scaffold::Scaffold};
+use jellycommon::{
+ jellyobject::{Object, Tag},
+ *,
};
+use serde::{Deserialize, Serialize};
pub type FlashM = Option<(String, String)>;
@@ -33,59 +22,22 @@ pub type FlashM = Option<(String, String)>;
pub struct Config {
brand: String,
slogan: String,
- asset_path: PathBuf,
-}
-
-static CONF: LazyLock<Config> = LazyLock::new(|| {
- CONF_PRELOAD
- .lock()
- .unwrap()
- .take()
- .expect("cache config not preloaded. logic error")
-});
-pub static CONF_PRELOAD: Mutex<Option<Config>> = Mutex::new(None);
-
-pub fn get_brand() -> String {
- CONF.brand.clone()
-}
-pub fn get_slogan() -> String {
- CONF.slogan.clone()
+ logo: bool,
}
-/// render as supertrait would be possible but is not
-/// dyn compatible and I really dont want to expose generics
-/// that generate rendering code because of compile speed.
-pub trait Page {
- fn title(&self) -> String;
- fn to_render(&self) -> DynRender<'_>;
- fn class(&self) -> Option<&'static str> {
- None
- }
+pub struct RenderInfo<'a> {
+ pub user: Option<Object<'a>>,
+ pub lang: Tag,
+ pub status_message: Option<&'a str>,
+ pub config: &'a Config,
}
-pub fn render_page(renderinfo: &RenderInfo<'_>, page: &dyn Page) -> String {
+pub fn render_view(ri: RenderInfo<'_>, view: Object<'_>) -> String {
Scaffold {
- class: &format!(
- "{} theme-{}",
- page.class().unwrap_or("custom-page"),
- "dark", // todo
- ),
- ri: renderinfo,
- title: page.title(),
- main: page.to_render(),
+ ri: &ri,
+ main: View { ri: &ri, view },
+ title: view.get(VIEW_TITLE).unwrap_or_default(),
+ class: "",
}
.to_string()
}
-
-pub struct CustomPage {
- pub title: String,
- pub body: String,
-}
-impl Page for CustomPage {
- fn title(&self) -> String {
- self.title.clone()
- }
- fn to_render(&self) -> DynRender<'_> {
- markup::new!(@markup::raw(&self.body))
- }
-}
diff --git a/ui/src/locale.rs b/ui/src/locale.rs
index a2fdce0..08c73c2 100644
--- a/ui/src/locale.rs
+++ b/ui/src/locale.rs
@@ -6,7 +6,7 @@
use jellycommon::*;
use std::{collections::HashMap, sync::LazyLock};
-static LANG_TABLES: LazyLock<HashMap<Language, HashMap<&'static str, &'static str>>> =
+pub static LANG_TABLES: LazyLock<HashMap<Language, HashMap<&'static str, &'static str>>> =
LazyLock::new(|| {
let mut k = HashMap::new();
for (lang, source) in [
@@ -31,10 +31,6 @@ pub fn tr(lang: Language, key: &str) -> &'static str {
tr_map.get(key).copied().unwrap_or("MISSING TRANSLATION")
}
-pub fn get_translation_table(lang: &Language) -> &'static HashMap<&'static str, &'static str> {
- LANG_TABLES.get(lang).unwrap()
-}
-
pub fn escape(str: &str) -> String {
let mut o = String::with_capacity(str.len());
let mut last = 0;
diff --git a/ui/src/node_page.rs b/ui/src/node_page.rs
deleted file mode 100644
index f52ea5b..0000000
--- a/ui/src/node_page.rs
+++ /dev/null
@@ -1,218 +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::{Page, scaffold::RenderInfo};
-use jellycommon::{
- jellyobject::{Object, Tag, TypedTag},
- *,
-};
-use std::marker::PhantomData;
-
-impl Page for NodePage<'_> {
- fn title(&self) -> String {
- self.node.node.get(NO_TITLE).unwrap_or_default().to_string()
- }
- fn class(&self) -> Option<&'static str> {
- Some("node-page")
- }
- fn to_render(&self) -> markup::DynRender<'_> {
- markup::new!(@self)
- }
-}
-
-pub struct NodeUdata<'a> {
- pub node: Object<'a>,
- pub udata: Object<'a>,
-}
-
-markup::define! {
- NodePage<'a>(
- ri: &'a RenderInfo<'a>,
- node: NodeUdata<'a>,
- children: &'a [NodeUdata<'a>],
- parents: &'a [NodeUdata<'a>],
- similar: &'a [NodeUdata<'a>],
- ) {
- // @if !matches!(node.kind, NodeKind::Collection) && !player {
- // img.backdrop[src=u_node_image(&node.slug, PictureSlot::Backdrop, 2048), loading="lazy"];
- // }
- // .page.node {
- // @if !matches!(node.kind, NodeKind::Collection) && !player {
- // @let cls = format!("bigposter {}", aspect_class(node.kind));
- // div[class=cls] { img[src=u_node_image(&node.slug, PictureSlot::Cover, 2048), loading="lazy"]; }
- // }
- // .title {
- // h1 { @node.title }
- // ul.parents { @for (node, _) in *parents { li {
- // a.component[href=u_node_slug(&node.slug)] { @node.title }
- // }}}
- // @if node.media.is_some() {
- // a.play[href=u_node_slug_player(&node.slug)] { @trs(lang, "node.player_link") }
- // }
- // @if !matches!(node.kind, NodeKind::Collection | NodeKind::Channel) {
- // @if matches!(udata.watched, WatchedState::None | WatchedState::Pending | WatchedState::Progress(_)) {
- // form.mark_watched[method="POST", action=u_node_slug_watched(&node.slug, ApiWatchedState::Watched)] {
- // input[type="submit", value=trs(lang, "node.watched.set")];
- // }
- // }
- // @if matches!(udata.watched, WatchedState::Watched) {
- // form.mark_unwatched[method="POST", action=u_node_slug_watched(&node.slug, ApiWatchedState::None)] {
- // input[type="submit", value=trs(lang, "node.watched.unset")];
- // }
- // }
- // @if matches!(udata.watched, WatchedState::None) {
- // form.mark_unwatched[method="POST", action=u_node_slug_watched(&node.slug, ApiWatchedState::Pending)] {
- // input[type="submit", value=trs(lang, "node.watchlist.set")];
- // }
- // }
- // @if matches!(udata.watched, WatchedState::Pending) {
- // form.mark_unwatched[method="POST", action=u_node_slug_watched(&node.slug, ApiWatchedState::None)] {
- // input[type="submit", value=trs(lang, "node.watchlist.unset")];
- // }
- // }
- // form.rating[method="POST", action=u_node_slug_update_rating(&node.slug)] {
- // input[type="range", name="rating", min=-10, max=10, step=1, value=udata.rating];
- // input[type="submit", value=trs(lang, "node.update_rating")];
- // }
- // }
- // }
- // .details {
- // @Props { node, udata, full: true, lang }
- // h3 { @node.tagline }
- // @if let Some(description) = &node.description {
- // p { @for line in description.lines() { @line br; } }
- // }
- // @if let Some(media) = &node.media {
- // @if !media.chapters.is_empty() {
- // h2 { @trs(lang, "node.chapters") }
- // ul.children.hlist { @for chap in &media.chapters {
- // @let (inl, sub) = format_chapter(chap);
- // li { .card."aspect-thumb" {
- // .poster {
- // a[href=u_node_slug_player_time(&node.slug, chap.time_start.unwrap_or(0.))] {
- // img[src=u_node_slug_thumbnail(&node.slug, chapter_key_time(chap, media.duration), 1024), loading="lazy"];
- // }
- // .cardhover { .props { p { @inl } } }
- // }
- // .title { span { @sub } }
- // }}
- // }}
- // }
- // @if !node.credits.is_empty() {
- // h2 { @trs(lang, "node.people") }
- // @for (group, people) in &node.credits {
- // details[open=group==&CreditCategory::Cast] {
- // summary { h3 { @format!("{}", group) } }
- // ul.children.hlist { @for (i, pe) in people.iter().enumerate() {
- // li { .card."aspect-port" {
- // .poster {
- // a[href="#"] {
- // img[src=u_node_slug_person_asset(&node.slug, *group, i, 1024), loading="lazy"];
- // }
- // }
- // .title {
- // // TODO span { @pe.person.name } br;
- // @if let Some(c) = pe.characters.first() {
- // span.subtitle { @c }
- // }
- // @if let Some(c) = pe.jobs.first() {
- // span.subtitle { @c }
- // }
- // }
- // }}
- // }}
- // }
- // }
- // }
- // details {
- // summary { @trs(lang, "media.tracks") }
- // ol { @for track in &media.tracks {
- // li { @format!("{track}") }
- // }}
- // }
- // }
- // @if !node.identifiers.is_empty() {
- // details {
- // summary { @trs(lang, "node.external_ids") }
- // table {
- // @for (key, value) in &node.identifiers { tr {
- // tr {
- // td { @trs(lang, &format!("id.{}", key)) }
- // @if let Some(url) = external_id_url(*key, value) {
- // td { a[href=url] { pre { @value } } }
- // } else {
- // td { pre { @value } }
- // }
- // }
- // }}
- // }
- // }
- // }
- // @if !node.tags.is_empty() {
- // details {
- // summary { @trs(lang, "node.tags") }
- // ol { @for tag in &node.tags {
- // li { @tag }
- // }}
- // }
- // }
- // }
- // @if matches!(node.kind, NodeKind::Collection | NodeKind::Channel) {
- // @NodeFilterSortForm { f: filter, lang }
- // }
- // @if !similar.is_empty() {
- // h2 { @trs(lang, "node.similar") }
- // ul.children.hlist {@for (node, udata) in similar.iter() {
- // li { @NodeCard { node, udata, lang } }
- // }}
- // }
- // @match node.kind {
- // NodeKind::Show | NodeKind::Series | NodeKind::Season => {
- // ol { @for (node, udata) in children.iter() {
- // li { @NodeCardWide { node, udata, lang } }
- // }}
- // }
- // NodeKind::Collection | NodeKind::Channel | _ => {
- // ul.children {@for (node, udata) in children.iter() {
- // li { @NodeCard { node, udata, lang } }
- // }}
- // }
- // }
- // }
- }
-}
-
-fn chapter_key_time(c: Object, dur: f64) -> f64 {
- let start = c.get(CH_START).unwrap_or(0.);
- let end = c.get(CH_END).unwrap_or(dur);
- start * 0.8 + end * 0.2
-}
-
-pub fn aspect_class(kind: Tag) -> &'static str {
- match kind {
- KIND_VIDEO | KIND_EPISODE => "aspect-thumb",
- KIND_COLLECTION => "aspect-land",
- KIND_SEASON | KIND_SHOW | KIND_SERIES | KIND_MOVIE | KIND_SHORTFORMVIDEO => "aspect-port",
- KIND_CHANNEL | KIND_MUSIC | _ => "aspect-square",
- }
-}
-
-fn external_id_url(key: Tag, value: &str) -> Option<String> {
- Some(match TypedTag(key, PhantomData) {
- IDENT_YOUTUBE_VIDEO => format!("https://youtube.com/watch?v={value}"),
- IDENT_YOUTUBE_CHANNEL => format!("https://youtube.com/channel/{value}"),
- IDENT_YOUTUBE_CHANNEL_HANDLE => format!("https://youtube.com/channel/@{value}"),
- IDENT_MUSICBRAINZ_RELEASE => format!("https://musicbrainz.org/release/{value}"),
- IDENT_MUSICBRAINZ_ARTIST => format!("https://musicbrainz.org/artist/{value}"),
- IDENT_MUSICBRAINZ_RELEASE_GROUP => {
- format!("https://musicbrainz.org/release-group/{value}")
- }
- IDENT_MUSICBRAINZ_RECORDING => {
- format!("https://musicbrainz.org/recording/{value}")
- }
- _ => return None,
- })
-}
diff --git a/ui/src/account/mod.rs b/ui/src/old/account/mod.rs
index e7da26f..e7da26f 100644
--- a/ui/src/account/mod.rs
+++ b/ui/src/old/account/mod.rs
diff --git a/ui/src/account/settings.rs b/ui/src/old/account/settings.rs
index 83f72b0..83f72b0 100644
--- a/ui/src/account/settings.rs
+++ b/ui/src/old/account/settings.rs
diff --git a/ui/src/admin/import.rs b/ui/src/old/admin/import.rs
index 805d787..805d787 100644
--- a/ui/src/admin/import.rs
+++ b/ui/src/old/admin/import.rs
diff --git a/ui/src/admin/log.rs b/ui/src/old/admin/log.rs
index 637158f..637158f 100644
--- a/ui/src/admin/log.rs
+++ b/ui/src/old/admin/log.rs
diff --git a/ui/src/admin/mod.rs b/ui/src/old/admin/mod.rs
index f42ba76..f42ba76 100644
--- a/ui/src/admin/mod.rs
+++ b/ui/src/old/admin/mod.rs
diff --git a/ui/src/admin/user.rs b/ui/src/old/admin/user.rs
index e4a8975..e4a8975 100644
--- a/ui/src/admin/user.rs
+++ b/ui/src/old/admin/user.rs
diff --git a/ui/src/filter_sort.rs b/ui/src/old/filter_sort.rs
index 55cb113..55cb113 100644
--- a/ui/src/filter_sort.rs
+++ b/ui/src/old/filter_sort.rs
diff --git a/ui/src/home.rs b/ui/src/old/home.rs
index a3088c8..a3088c8 100644
--- a/ui/src/home.rs
+++ b/ui/src/old/home.rs
diff --git a/ui/src/items.rs b/ui/src/old/items.rs
index 529a5d6..529a5d6 100644
--- a/ui/src/items.rs
+++ b/ui/src/old/items.rs
diff --git a/ui/src/node_card.rs b/ui/src/old/node_card.rs
index f87f490..f87f490 100644
--- a/ui/src/node_card.rs
+++ b/ui/src/old/node_card.rs
diff --git a/ui/src/search.rs b/ui/src/old/search.rs
index 0eb34b9..0eb34b9 100644
--- a/ui/src/search.rs
+++ b/ui/src/old/search.rs
diff --git a/ui/src/scaffold.rs b/ui/src/scaffold.rs
index 82d6d5e..fee311a 100644
--- a/ui/src/scaffold.rs
+++ b/ui/src/scaffold.rs
@@ -5,11 +5,10 @@
*/
use crate::{
- CONF, FlashM,
+ RenderInfo,
locale::{escape, tr},
};
use jellycommon::{
- jellyobject::{Object, Tag},
routes::{
u_account_login, u_account_logout, u_account_register, u_account_settings,
u_admin_dashboard, u_home, u_items, u_node_slug, u_search, u_stats,
@@ -17,22 +16,13 @@ use jellycommon::{
user::{USER_ADMIN, USER_NAME},
};
use markup::{Render, raw};
-use std::sync::LazyLock;
-
-static LOGO_ENABLED: LazyLock<bool> = LazyLock::new(|| CONF.asset_path.join("logo.svg").exists());
-
-pub struct RenderInfo<'a> {
- pub user: Option<Object<'a>>,
- pub lang: Tag,
- pub status_message: Option<&'a str>,
-}
markup::define! {
- Scaffold<'a, Main: Render>(ri: &'a RenderInfo<'a>, title: String, main: Main, class: &'a str) {
+ Scaffold<'a, Main: Render>(ri: &'a RenderInfo<'a>, title: &'a str, main: Main, class: &'a str) {
@markup::doctype()
html {
head {
- title { @title " - " @CONF.brand }
+ title { @title " - " @ri.config.brand }
meta[name="viewport", content="width=device-width, initial-scale=1.0"];
link[rel="stylesheet", href="/assets/style.css"];
script[src="/assets/bundle.js"] {}
@@ -41,7 +31,7 @@ markup::define! {
@Navbar { ri }
#main { @main }
footer {
- p { @CONF.brand " - " @CONF.slogan " | powered by " a[href="https://codeberg.org/metamuffin/jellything"]{"Jellything"} }
+ p { @ri.config.brand " - " @ri.config.slogan " | powered by " a[href="https://codeberg.org/metamuffin/jellything"]{"Jellything"} }
}
}
}
@@ -49,7 +39,7 @@ markup::define! {
Navbar<'a>(ri: &'a RenderInfo<'a>) {
nav {
- h1 { a[href=if ri.user.is_some() {u_home()} else {"/".to_string()}] { @if *LOGO_ENABLED { img.logo[src="/assets/logo.svg"]; } else { @CONF.brand } } } " "
+ h1 { a[href=if ri.user.is_some() {u_home()} else {"/".to_string()}] { @if ri.config.logo { img.logo[src="/assets/logo.svg"]; } else { @ri.config.brand } } } " "
@if ri.user.is_some() {
a.library[href=u_node_slug("library")] { @tr(ri.lang, "nav.root") } " "
a.library[href=u_items()] { @tr(ri.lang, "nav.all") } " "
@@ -72,14 +62,4 @@ markup::define! {
}
}
}
-
- FlashDisplay<'a>(flash: &'a FlashM) {
- @if let Some((kind, message)) = &flash {
- @match kind.as_str() {
- "success" => { section.message { p.success { @message } } }
- "error" => { section.message { p.error { @message } } }
- _ => { section.message { p { @message } } }
- }
- }
- }
}
diff --git a/ui/src/stats.rs b/ui/src/stats.rs
deleted file mode 100644
index d69f07e..0000000
--- a/ui/src/stats.rs
+++ /dev/null
@@ -1,79 +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::{
- Page,
- format::{format_duration, format_duration_long, format_kind, format_size},
- locale::tr,
- scaffold::RenderInfo,
-};
-use jellycommon::routes::u_node_slug;
-use markup::raw;
-
-impl Page for StatsPage<'_> {
- fn title(&self) -> String {
- tr(self.ri.lang, "stats.title").to_string()
- }
- fn to_render(&self) -> markup::DynRender<'_> {
- markup::new!(@self)
- }
-}
-
-markup::define! {
- StatsPage<'a>(ri: &'a RenderInfo<'a>, r: ApiStatsResponse) {
- .page.stats {
- h1 { @tr(ri.lang, "stats.title") }
- p { @raw(tr(ri.lang, "stats.count")
- .replace("{count}", &format!("<b>{}</b>", r.total.count))
- )}
- p { @raw(tr(ri.lang, "stats.runtime")
- .replace("{dur}", &format!("<b>{}</b>", format_duration_long(r.total.runtime, ri.lang)))
- .replace("{size}", &format!("<b>{}</b>", format_size(r.total.size)))
- )}
- p { @raw(tr(ri.lang, "stats.average")
- .replace("{dur}", &format!("<b>{}</b>", format_duration(r.total.average_runtime())))
- .replace("{size}", &format!("<b>{}</b>", format_size(r.total.average_size() as u64)))
- )}
-
- h2 { @tr(ri.lang, "stats.by_kind.title") }
- table.striped {
- tr {
- th { @tr(ri.lang, "stats.by_kind.kind") }
- th { @tr(ri.lang, "stats.by_kind.count") }
- th { @tr(ri.lang, "stats.by_kind.total_size") }
- th { @tr(ri.lang, "stats.by_kind.total_runtime") }
- th { @tr(ri.lang, "stats.by_kind.average_size") }
- th { @tr(ri.lang, "stats.by_kind.average_runtime") }
- th { @tr(ri.lang, "stats.by_kind.max_size") }
- th { @tr(ri.lang, "stats.by_kind.max_runtime") }
- }
- @for (k,b) in &r.kinds { tr {
- td { @format_kind(*k, ri.lang) }
- td { @b.count }
- td { @format_size(b.size) }
- td { @format_duration(b.runtime) }
- td { @format_size(b.average_size() as u64) }
- td { @format_duration(b.average_runtime()) }
- td { @if b.max_size.0 > 0 { a[href=u_node_slug(&b.max_size.1)]{ @format_size(b.max_size.0) }}}
- td { @if b.max_runtime.0 > 0. { a[href=u_node_slug(&b.max_runtime.1)]{ @format_duration(b.max_runtime.0) }}}
- }}
- }
- }
- }
-}
-
-trait BinExt {
- fn average_runtime(&self) -> f64;
- fn average_size(&self) -> f64;
-}
-impl BinExt for StatsBin {
- fn average_runtime(&self) -> f64 {
- self.runtime / self.count as f64
- }
- fn average_size(&self) -> f64 {
- self.size as f64 / self.count as f64
- }
-}