aboutsummaryrefslogtreecommitdiff
path: root/ui/src/components/node_page.rs
blob: 265e2d4749343606f678f109e4c85291005053bd (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::{
    RenderInfo,
    components::{node_card::NodeCard, props::Props},
    page,
};
use jellycommon::{
    jellyobject::{EMPTY, Object, Tag, TypedTag},
    routes::{u_image, u_node_slug_player},
    *,
};
use jellyui_locale::tr;
use std::marker::PhantomData;

page!(NodePage<'_>, |x| x
    .nku
    .node
    .get(NO_TITLE)
    .unwrap_or_default()
    .to_string()
    .into());
page!(Player<'_>, |x| x
    .nku
    .node
    .get(NO_TITLE)
    .unwrap_or_default()
    .to_string()
    .into());

markup::define! {
    NodePage<'a>(
        ri: &'a RenderInfo<'a>,
        nku: Nku<'a>,
        children: &'a [Nku<'a>],
        credits: &'a [(Tag, Vec<Nku<'a>>)],
        credited: &'a [Nku<'a>]
    ) {
        @let node = &nku.node;
        @let slug = node.get(NO_SLUG).unwrap_or_default();
        @let pics = node.get(NO_PICTURES).unwrap_or(EMPTY);
        @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, 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.has(NO_TRACK.0) {
                details {
                    summary { @tr(ri.lang, "tag.trak") }
                    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, "tag.iden") }
                    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, "tag.tag1") }
                    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(EMPTY).entries::<Tag>() { tr {
                            td { @tr(ri.lang, &format!("tag.{key}")) }
                            td { @tr(ri.lang, &format!("tag.msrc.{source}")) }
                        }}
                        @for nkey in [NO_PICTURES, NO_IDENTIFIERS, NO_RATINGS] {
                            @let nob = node.get(nkey).unwrap_or(EMPTY);
                            @for (key, source) in nob.get(NO_METASOURCE).unwrap_or(EMPTY).entries::<Tag>() { tr {
                                td { @tr(ri.lang, &format!("tag.{nkey}")) ": " @tr(ri.lang, &format!("tag.{nkey}.{key}")) }
                                td { @tr(ri.lang, &format!("tag.msrc.{source}")) }
                            }}
                        }
                    }
                }
            }
        }

        @for (cat, items) in *credits {
            h2 { @tr(ri.lang, &format!("tag.cred.kind.{cat}")) }
            ul.nl.inline { @for nku in items {
                li { @NodeCard { ri, nku } }
            }}
        }
        @if !credited.is_empty() {
            h2 { @tr(ri.lang, &format!("node.credited")) }
            ul.nl.grid { @for nku in *credited {
                li { @NodeCard { ri, nku } }
            }}
        }
        @if !children.is_empty() {
            ul.nl.grid { @for nku in *children {
                li { @NodeCard { ri, nku } }
            }}
        }
    }

    Player<'a>(ri: &'a RenderInfo<'a>, nku: Nku<'a>) {
        @let _ = ri;
        @let pics = nku.node.get(NO_PICTURES).unwrap_or(EMPTY);
        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,
    })
}