aboutsummaryrefslogtreecommitdiff
path: root/ui/src/components/node_page.rs
blob: 9be20f2eca6cb0dc9fc88a9ee4f8fc373bd0c238 (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
/*
    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};
use jellycommon::{
    jellyobject::{Object, Tag, TypedTag},
    routes::{u_image, u_node_slug_player},
    *,
};
use jellyui_locale::tr;
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();
        @let pics = node.get(NO_PICTURES).unwrap_or_default();
        @if let Some(path) = pics.get(PICT_BACKDROP) {
            img.backdrop[src=u_image(path, 2048)];
        }
        @if let Some(path) = pics.get(PICT_COVER) {
            @let cls = format!("bigposter {} {}", aspect_class(node), if pics.has(PICT_BACKDROP.0) { "has_backdrop" } else { "" });
            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 }
            //                         }
            //                     }
            //                 }}
            //             }}
            //         }
            //     }
            // }
            @if node.has(NO_TRACK.0) {
                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 {
                            td { @tr(ri.lang, &format!("tag.iden.{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 node.has(NO_METASOURCE.0) {
                details {
                    summary { @tr(ri.lang, "tag.msrc") }
                    table {
                        tr { th {"Attribute"} th {"Source"} }
                        @for (key, source) in node.get(NO_METASOURCE).unwrap_or_default().entries::<Tag>() { tr {
                            td { @tr(ri.lang, &format!("tag.{key}")) }
                            td { @tr(ri.lang, &format!("tag.msrc.{source}")) } 
                        }}
                    }
                }
            }
        }
        //     @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 } }
        //             }}
        //         }
        //     }
        // }
    }

    Player<'a>(ri: &'a RenderInfo<'a>, nku: Object<'a>) {
        @let node = nku.get(NKU_NODE).unwrap_or_default();
        @let pics = node.get(NO_PICTURES).unwrap_or_default();
        video[id="player", poster=pics.get(PICT_COVER).map(|p| u_image(p, 2048))] {}
    }

}

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_PERSON | 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,
    })
}