aboutsummaryrefslogtreecommitdiff
path: root/ui/src/node_page.rs
blob: f52ea5b33be732f394fe0e926404550fbacdbc2e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
/*
    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,
    })
}