aboutsummaryrefslogtreecommitdiff
path: root/ui/src/components/node_page.rs
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 /ui/src/components/node_page.rs
parent10cdaaa30a6b4a187797434dc8d959780f0e8fbf (diff)
downloadjellything-3671a4e07565c86f8071fb2309f463aeaf684ba3.tar
jellything-3671a4e07565c86f8071fb2309f463aeaf684ba3.tar.bz2
jellything-3671a4e07565c86f8071fb2309f463aeaf684ba3.tar.zst
move ui code around
Diffstat (limited to 'ui/src/components/node_page.rs')
-rw-r--r--ui/src/components/node_page.rs194
1 files changed, 194 insertions, 0 deletions
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,
+ })
+}