aboutsummaryrefslogtreecommitdiff
path: root/ui/src
diff options
context:
space:
mode:
Diffstat (limited to 'ui/src')
-rw-r--r--ui/src/filter_sort.rs5
-rw-r--r--ui/src/format.rs10
-rw-r--r--ui/src/home.rs24
-rw-r--r--ui/src/items.rs34
-rw-r--r--ui/src/lib.rs12
-rw-r--r--ui/src/locale.rs43
-rw-r--r--ui/src/node_card.rs85
-rw-r--r--ui/src/node_page.rs345
-rw-r--r--ui/src/props.rs2
-rw-r--r--ui/src/scaffold.rs73
-rw-r--r--ui/src/search.rs2
-rw-r--r--ui/src/stats.rs2
12 files changed, 294 insertions, 343 deletions
diff --git a/ui/src/filter_sort.rs b/ui/src/filter_sort.rs
index cf06609..70e6b8a 100644
--- a/ui/src/filter_sort.rs
+++ b/ui/src/filter_sort.rs
@@ -4,11 +4,10 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::locale::{Language, trs};
+use crate::locale::trs;
use markup::RenderAttributeValue;
const SORT_CATS: &[(&str, &[(SortProperty, &str)])] = {
- use SortProperty::*;
&[
(
"filter_sort.sort.general",
@@ -35,7 +34,6 @@ const SORT_CATS: &[(&str, &[(SortProperty, &str)])] = {
]
};
const FILTER_CATS: &[(&str, &[(FilterProperty, &str)])] = {
- use FilterProperty::*;
&[
(
"filter_sort.filter.kind",
@@ -103,7 +101,6 @@ markup::define! {
}
fieldset.sortorder {
legend { "Sort Order" }
- @use SortOrder::*;
@for (value, label) in [(Ascending, "filter_sort.order.asc"), (Descending, "filter_sort.order.desc")] {
label { input[type="radio", name="sort_order", value=A(value), checked=Some(value)==f.sort_order]; @trs(lang, label) } br;
}
diff --git a/ui/src/format.rs b/ui/src/format.rs
index 138589f..3b49695 100644
--- a/ui/src/format.rs
+++ b/ui/src/format.rs
@@ -4,7 +4,9 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::locale::{Language, TrString, tr, trs};
+use jellycommon::LANG_ENG;
+
+use crate::locale::{TrString, tr, trs};
use std::fmt::Write;
pub fn format_duration(d: f64) -> String {
@@ -53,15 +55,15 @@ fn test_duration_short() {
#[test]
fn test_duration_long() {
assert_eq!(
- format_duration_long(61., Language::English).as_str(),
+ format_duration_long(61., LANG_ENG).as_str(),
"1 minute and 1 second"
);
assert_eq!(
- format_duration_long(121., Language::English).as_str(),
+ format_duration_long(121., LANG_ENG).as_str(),
"2 minutes and 1 second"
);
assert_eq!(
- format_duration_long(3661., Language::English).as_str(),
+ format_duration_long(3661., LANG_ENG).as_str(),
"1 hour, 1 minute and 1 second"
);
}
diff --git a/ui/src/home.rs b/ui/src/home.rs
index 64e1ee5..5de9e17 100644
--- a/ui/src/home.rs
+++ b/ui/src/home.rs
@@ -3,33 +3,29 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::{
- CONF, Page,
- locale::{Language, tr, trs},
- node_card::NodeCard,
-};
+use crate::{CONF, Page, locale::tr, node_card::NodeCard, scaffold::RenderInfo};
use markup::DynRender;
markup::define! {
- HomePage<'a>(lang: &'a Language, r: ApiHomeResponse) {
- h2 { @tr(**lang, "home.bin.root").replace("{title}", &CONF.brand) }
+ HomePage<'a>(ri: RenderInfo<'a>, r: ApiHomeResponse) {
+ h2 { @tr(ri.lang, "home.bin.root").replace("{title}", &CONF.brand) }
ul.children.hlist {@for (node, udata) in &r.toplevel {
li { @NodeCard { node, udata, lang } }
}}
@for (name, nodes) in &r.categories {
- @if !nodes.is_empty() {
- h2 { @trs(lang, name) }
- ul.children.hlist {@for (node, udata) in nodes {
- li { @NodeCard { node, udata, lang } }
- }}
- }
+ // @if !nodes.is_empty() {
+ // h2 { @trs(lang, name) }
+ // ul.children.hlist {@for (node, udata) in nodes {
+ // li { @NodeCard { node, udata, lang } }
+ // }}
+ // }
}
}
}
impl Page for HomePage<'_> {
fn title(&self) -> String {
- tr(*self.lang, "home").to_string()
+ tr(self.ri.lang, "home").to_string()
}
fn to_render(&self) -> DynRender<'_> {
markup::new!(@self)
diff --git a/ui/src/items.rs b/ui/src/items.rs
index 3879b29..4bce43c 100644
--- a/ui/src/items.rs
+++ b/ui/src/items.rs
@@ -3,32 +3,26 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::{
- Page,
- filter_sort::NodeFilterSortForm,
- locale::{Language, tr, trs},
- node_card::NodeCard,
-};
-use jellycommon::routes::u_items_filter;
+use crate::{Page, locale::tr};
use markup::DynRender;
markup::define! {
ItemsPage<'a>(lang: &'a Language, r: ApiItemsResponse, filter: &'a NodeFilterSort, page: usize) {
.page.dir {
h1 { "All Items" }
- @NodeFilterSortForm { f: filter, lang }
- ul.children { @for (node, udata) in &r.items {
- li {@NodeCard { node, udata, lang }}
- }}
- p.pagecontrols {
- span.current { @tr(**lang, "page.curr").replace("{cur}", &(page + 1).to_string()).replace("{max}", &r.pages.to_string()) " " }
- @if *page > 0 {
- a.prev[href=u_items_filter(page - 1, filter)] { @trs(lang, "page.prev") } " "
- }
- @if page + 1 < r.pages {
- a.next[href=u_items_filter(page + 1, filter)] { @trs(lang, "page.next") }
- }
- }
+ // @NodeFilterSortForm { f: filter, lang }
+ // ul.children { @for (node, udata) in &r.items {
+ // li {@NodeCard { node, udata, lang }}
+ // }}
+ // p.pagecontrols {
+ // span.current { @tr(**lang, "page.curr").replace("{cur}", &(page + 1).to_string()).replace("{max}", &r.pages.to_string()) " " }
+ // @if *page > 0 {
+ // a.prev[href=u_items_filter(page - 1, filter)] { @trs(lang, "page.prev") } " "
+ // }
+ // @if page + 1 < r.pages {
+ // a.next[href=u_items_filter(page + 1, filter)] { @trs(lang, "page.next") }
+ // }
+ // }
}
}
}
diff --git a/ui/src/lib.rs b/ui/src/lib.rs
index 7e17a20..6dbc837 100644
--- a/ui/src/lib.rs
+++ b/ui/src/lib.rs
@@ -63,20 +63,14 @@ pub trait Page {
}
}
-pub fn render_page(page: &dyn Page, renderinfo: RenderInfo) -> String {
+pub fn render_page(renderinfo: &RenderInfo<'_>, page: &dyn Page) -> String {
Scaffold {
- lang: renderinfo.lang,
class: &format!(
"{} theme-{}",
page.class().unwrap_or("custom-page"),
- renderinfo
- .session
- .as_ref()
- .map(|s| s.user.theme)
- .unwrap_or(Theme::Dark)
- .to_str()
+ "dark", // todo
),
- renderinfo,
+ ri: renderinfo,
title: page.title(),
main: page.to_render(),
}
diff --git a/ui/src/locale.rs b/ui/src/locale.rs
index b3bf3b6..a2fdce0 100644
--- a/ui/src/locale.rs
+++ b/ui/src/locale.rs
@@ -3,21 +3,15 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use markup::{Render, RenderAttributeValue};
-use std::{borrow::Cow, collections::HashMap, sync::LazyLock};
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum Language {
- English,
- German,
-}
+use jellycommon::*;
+use std::{collections::HashMap, sync::LazyLock};
static LANG_TABLES: LazyLock<HashMap<Language, HashMap<&'static str, &'static str>>> =
LazyLock::new(|| {
let mut k = HashMap::new();
for (lang, source) in [
- (Language::English, include_str!("../../locale/en.ini")),
- (Language::German, include_str!("../../locale/de.ini")),
+ (LANG_ENG.0, include_str!("../../locale/en.ini")),
+ (LANG_DEU.0, include_str!("../../locale/de.ini")),
] {
// TODO fallback to english
let tr_map = source
@@ -32,36 +26,15 @@ static LANG_TABLES: LazyLock<HashMap<Language, HashMap<&'static str, &'static st
k
});
-pub fn tr(lang: Language, key: &str) -> Cow<'static, str> {
+pub fn tr(lang: Language, key: &str) -> &'static str {
let tr_map = LANG_TABLES.get(&lang).unwrap();
- match tr_map.get(key) {
- Some(value) => Cow::Borrowed(value),
- None => Cow::Owned(format!("TR[{key}]")),
- }
+ 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 struct TrString<'a>(Cow<'a, str>);
-impl Render for TrString<'_> {
- fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
- self.0.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;
@@ -81,7 +54,3 @@ pub fn escape(str: &str) -> String {
o += &str[last..];
o
}
-
-pub fn trs<'a>(lang: &Language, key: &str) -> TrString<'a> {
- TrString(tr(*lang, key))
-}
diff --git a/ui/src/node_card.rs b/ui/src/node_card.rs
index d0f9904..f87f490 100644
--- a/ui/src/node_card.rs
+++ b/ui/src/node_card.rs
@@ -4,53 +4,56 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::{locale::Language, node_page::aspect_class, props::Props};
-use jellycommon::routes::{u_node_image, u_node_slug, u_node_slug_player};
+use crate::{
+ node_page::{NodeUdata, aspect_class},
+ scaffold::RenderInfo,
+};
+use jellycommon::*;
markup::define! {
- NodeCard<'a>(node: &'a Node, udata: &'a NodeUserData, lang: &'a Language) {
- @let cls = format!("node card poster {}", aspect_class(node.kind));
+ NodeCard<'a>(ri: &'a RenderInfo<'a>, nodeu: NodeUdata<'a>) {
+ @let cls = format!("node card poster {}", aspect_class(nodeu.node.get(NO_KIND).unwrap_or(KIND_COLLECTION)));
div[class=cls] {
- .poster {
- a[href=u_node_slug(&node.slug)] {
- img[src=u_node_image(&node.slug, PictureSlot::Cover, 512), loading="lazy"];
- }
- .cardhover.item {
- @if node.media.is_some() {
- a.play.icon[href=u_node_slug_player(&node.slug)] { "play_arrow" }
- }
- @Props { node, udata, full: false, lang }
- }
- }
- div.title {
- a[href=u_node_slug(&node.slug)] {
- @node.title
- }
- }
- div.subtitle {
- span {
- @node.subtitle
- }
- }
+ // .poster {
+ // a[href=u_node_slug(&node.slug)] {
+ // img[src=u_node_image(&node.slug, PictureSlot::Cover, 512), loading="lazy"];
+ // }
+ // .cardhover.item {
+ // @if node.media.is_some() {
+ // a.play.icon[href=u_node_slug_player(&node.slug)] { "play_arrow" }
+ // }
+ // @Props { node, udata, full: false, lang }
+ // }
+ // }
+ // div.title {
+ // a[href=u_node_slug(&node.slug)] {
+ // @node.title
+ // }
+ // }
+ // div.subtitle {
+ // span {
+ // @node.subtitle
+ // }
+ // }
}
}
- NodeCardWide<'a>(node: &'a Node, udata: &'a NodeUserData, lang: &'a Language) {
+ NodeCardWide<'a>(ri: &'a RenderInfo<'a>, nodeu: NodeUdata<'a>) {
div[class="node card widecard poster"] {
- div[class=&format!("poster {}", aspect_class(node.kind))] {
- a[href=u_node_slug(&node.slug)] {
- img[src=u_node_image(&node.slug, PictureSlot::Cover, 512), loading="lazy"];
- }
- .cardhover.item {
- @if node.media.is_some() {
- a.play.icon[href=u_node_slug_player(&node.slug)] { "play_arrow" }
- }
- }
- }
- div.details {
- a.title[href=u_node_slug(&node.slug)] { @node.title }
- @Props { node, udata, full: false, lang }
- span.overview { @node.description }
- }
+ // div[class=&format!("poster {}", aspect_class(node.kind))] {
+ // a[href=u_node_slug(&node.slug)] {
+ // img[src=u_node_image(&node.slug, PictureSlot::Cover, 512), loading="lazy"];
+ // }
+ // .cardhover.item {
+ // @if node.media.is_some() {
+ // a.play.icon[href=u_node_slug_player(&node.slug)] { "play_arrow" }
+ // }
+ // }
+ // }
+ // div.details {
+ // a.title[href=u_node_slug(&node.slug)] { @node.title }
+ // @Props { node, udata, full: false, lang }
+ // span.overview { @node.description }
+ // }
}
}
}
diff --git a/ui/src/node_page.rs b/ui/src/node_page.rs
index 3e0c64f..fa5c93b 100644
--- a/ui/src/node_page.rs
+++ b/ui/src/node_page.rs
@@ -4,193 +4,183 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::{
- Page,
- filter_sort::NodeFilterSortForm,
- format::format_chapter,
- locale::{Language, trs},
- node_card::{NodeCard, NodeCardWide},
- props::Props,
+use crate::Page;
+use jellycommon::{
+ jellyobject::{Object, Tag},
+ *,
};
-use jellycommon::routes::{
- u_node_image, u_node_slug, u_node_slug_person_asset, u_node_slug_player,
- u_node_slug_player_time, u_node_slug_thumbnail, u_node_slug_update_rating, u_node_slug_watched,
-};
-use std::sync::Arc;
impl Page for NodePage<'_> {
fn title(&self) -> String {
- self.node.title.clone().unwrap_or_default()
+ self.node.node.get(NO_TITLE).unwrap_or_default().to_string()
}
fn class(&self) -> Option<&'static str> {
- if self.player {
- Some("player")
- } else {
- Some("node-page")
- }
+ 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>(
- node: &'a Node,
- udata: &'a NodeUserData,
- children: &'a [(Arc<Node>, NodeUserData)],
- parents: &'a [(Arc<Node>, NodeUserData)],
- similar: &'a [(Arc<Node>, NodeUserData)],
- filter: &'a NodeFilterSort,
- lang: &'a Language,
- player: bool,
+ ri: 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 } }
- }}
- }
- }
- }
+ // @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 } }
+ // }}
+ // }
+ // }
+ // }
}
}
@@ -200,13 +190,12 @@ fn chapter_key_time(c: &Chapter, dur: f64) -> f64 {
start * 0.8 + end * 0.2
}
-pub fn aspect_class(kind: NodeKind) -> &'static str {
- use NodeKind::*;
+pub fn aspect_class(kind: Tag) -> &'static str {
match kind {
- Video | Episode => "aspect-thumb",
- Collection => "aspect-land",
- Season | Show | Series | Movie | ShortFormVideo => "aspect-port",
- Channel | Music | Unknown => "aspect-square",
+ 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",
}
}
diff --git a/ui/src/props.rs b/ui/src/props.rs
index a2e79e5..2860933 100644
--- a/ui/src/props.rs
+++ b/ui/src/props.rs
@@ -5,7 +5,7 @@
*/
use crate::{
format::{MediaInfoExt, format_count, format_duration},
- locale::{Language, tr, trs},
+ locale::{tr, trs},
};
markup::define! {
diff --git a/ui/src/scaffold.rs b/ui/src/scaffold.rs
index 4f1bc8b..0962f6e 100644
--- a/ui/src/scaffold.rs
+++ b/ui/src/scaffold.rs
@@ -6,21 +6,24 @@
use crate::{
CONF, FlashM,
- locale::{Language, escape, tr, trs},
+ locale::{escape, tr, trs},
};
-use jellycommon::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,
+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,
+ },
};
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 {
- pub session: Option<SessionInfo>,
- pub lang: Language,
- pub importing: bool,
+pub struct RenderInfo<'a> {
+ pub user: Option<Object<'a>>,
+ pub lang: Tag,
+ pub status_message: Option<&'a str>,
}
pub struct SessionInfo {
@@ -28,7 +31,7 @@ pub struct SessionInfo {
}
markup::define! {
- Scaffold<'a, Main: Render>(title: String, main: Main, class: &'a str, renderinfo: RenderInfo, lang: Language) {
+ Scaffold<'a, Main: Render>(ri: &'a RenderInfo<'a>, title: String, main: Main, class: &'a str) {
@markup::doctype()
html {
head {
@@ -38,29 +41,7 @@ markup::define! {
script[src="/assets/bundle.js"] {}
}
body[class=class] {
- nav {
- h1 { a[href=if renderinfo.session.is_some() {u_home()} else {"/".to_string()}] { @if *LOGO_ENABLED { img.logo[src="/assets/logo.svg"]; } else { @CONF.brand } } } " "
- @if let Some(_) = &renderinfo.session {
- a.library[href=u_node_slug("library")] { @trs(lang, "nav.root") } " "
- a.library[href=u_items()] { @trs(lang, "nav.all") } " "
- a.library[href=u_search()] { @trs(lang, "nav.search") } " "
- a.library[href=u_stats()] { @trs(lang, "nav.stats") } " "
- @if renderinfo.importing { span.warn { @trs(lang, "nav.importing") } }
- }
- div.account {
- @if let Some(session) = &renderinfo.session {
- span { @raw(tr(*lang, "nav.username").replace("{name}", &format!("<b class=\"username\">{}</b>", escape(&session.user.display_name)))) } " "
- @if session.user.admin {
- a.admin.hybrid_button[href=u_admin_dashboard()] { p {@trs(lang, "nav.admin")} } " "
- }
- a.settings.hybrid_button[href=u_account_settings()] { p {@trs(lang, "nav.settings")} } " "
- a.logout.hybrid_button[href=u_account_logout()] { p {@trs(lang, "nav.logout")} }
- } else {
- a.register.hybrid_button[href=u_account_register()] { p {@trs(lang, "nav.register")} } " "
- a.login.hybrid_button[href=u_account_login()] { p {@trs(lang, "nav.login")} }
- }
- }
- }
+ @Navbar { ri }
#main { @main }
footer {
p { @CONF.brand " - " @CONF.slogan " | powered by " a[href="https://codeberg.org/metamuffin/jellything"]{"Jellything"} }
@@ -69,8 +50,34 @@ 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 } } } " "
+ @if ri.user.is_some() {
+ a.library[href=u_node_slug("library")] { @trs(lang, "nav.root") } " "
+ a.library[href=u_items()] { @trs(lang, "nav.all") } " "
+ a.library[href=u_search()] { @trs(lang, "nav.search") } " "
+ a.library[href=u_stats()] { @trs(lang, "nav.stats") } " "
+ @if renderinfo.importing { span.warn { @trs(lang, "nav.importing") } }
+ }
+ div.account {
+ @if let Some(user) = &ri.user {
+ span { @raw(tr(*lang, "nav.username").replace("{name}", &format!("<b class=\"username\">{}</b>", escape(&session.user.display_name)))) } " "
+ @if session.user.admin {
+ a.admin.hybrid_button[href=u_admin_dashboard()] { p {@trs(lang, "nav.admin")} } " "
+ }
+ a.settings.hybrid_button[href=u_account_settings()] { p {@trs(lang, "nav.settings")} } " "
+ a.logout.hybrid_button[href=u_account_logout()] { p {@trs(lang, "nav.logout")} }
+ } else {
+ a.register.hybrid_button[href=u_account_register()] { p {@trs(lang, "nav.register")} } " "
+ a.login.hybrid_button[href=u_account_login()] { p {@trs(lang, "nav.login")} }
+ }
+ }
+ }
+ }
+
FlashDisplay<'a>(flash: &'a FlashM) {
- @if let Some((kind,message)) = &flash {
+ @if let Some((kind, message)) = &flash {
@match kind.as_str() {
"success" => { section.message { p.success { @message } } }
"error" => { section.message { p.error { @message } } }
diff --git a/ui/src/search.rs b/ui/src/search.rs
index 99380e9..5b9909c 100644
--- a/ui/src/search.rs
+++ b/ui/src/search.rs
@@ -6,7 +6,7 @@
use crate::{
Page,
- locale::{Language, tr, trs},
+ locale::{tr, trs},
node_card::NodeCard,
};
use markup::DynRender;
diff --git a/ui/src/stats.rs b/ui/src/stats.rs
index 2617b2c..0a88fac 100644
--- a/ui/src/stats.rs
+++ b/ui/src/stats.rs
@@ -7,7 +7,7 @@
use crate::{
Page,
format::{format_duration, format_duration_long, format_kind, format_size},
- locale::{Language, tr, trs},
+ locale::{tr, trs},
};
use jellycommon::routes::u_node_slug;
use markup::raw;