/* 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 */ use crate::Page; use jellycommon::{ jellyobject::{Object, Tag}, *, }; 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: 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: &Chapter, dur: f64) -> f64 { let start = c.time_start.unwrap_or(0.); let end = c.time_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: IdentifierType, value: &str) -> Option { Some(match key { IdentifierType::YoutubeVideo => format!("https://youtube.com/watch?v={value}"), IdentifierType::YoutubeChannel => format!("https://youtube.com/channel/{value}"), IdentifierType::YoutubeChannelHandle => format!("https://youtube.com/channel/@{value}"), IdentifierType::MusicbrainzRelease => format!("https://musicbrainz.org/release/{value}"), IdentifierType::MusicbrainzArtist => format!("https://musicbrainz.org/artist/{value}"), IdentifierType::MusicbrainzReleaseGroup => { format!("https://musicbrainz.org/release-group/{value}") } IdentifierType::MusicbrainzRecording => { format!("https://musicbrainz.org/recording/{value}") } _ => return None, }) }