diff options
| -rw-r--r-- | common/src/api.rs | 1 | ||||
| -rw-r--r-- | locale/en.ini | 2 | ||||
| -rw-r--r-- | server/src/compat/youtube.rs | 6 | ||||
| -rw-r--r-- | server/src/ui/home.rs | 27 | ||||
| -rw-r--r-- | ui/client-style/src/node_card.css | 56 | ||||
| -rw-r--r-- | ui/client-style/src/node_page.css | 2 | ||||
| -rw-r--r-- | ui/src/components/node_card.rs | 35 | ||||
| -rw-r--r-- | ui/src/components/node_list.rs | 38 | ||||
| -rw-r--r-- | ui/src/components/node_page.rs | 11 | ||||
| -rw-r--r-- | ui/src/components/props.rs | 6 | ||||
| -rw-r--r-- | ui/src/format.rs | 90 |
11 files changed, 168 insertions, 106 deletions
diff --git a/common/src/api.rs b/common/src/api.rs index 12d9243..beccaa5 100644 --- a/common/src/api.rs +++ b/common/src/api.rs @@ -60,6 +60,7 @@ enums! { NLSTYLE_GRID = b"grid"; NLSTYLE_INLINE = b"inli"; NLSTYLE_LIST = b"list"; + NLSTYLE_HIGHLIGHT = b"hglt"; } // use crate::user::{NodeUserData, User}; diff --git a/locale/en.ini b/locale/en.ini index 1e901ef..b49273c 100644 --- a/locale/en.ini +++ b/locale/en.ini @@ -33,7 +33,7 @@ home.bin.latest_video=Latest in Videos home.bin.latest_music=Latest in Music home.bin.latest_short_form=Latest in Short form home.bin.max_rating=Top Rated -home.bin.daily_random=Today´s Picks +home.bin.daily_random=Today´s Pick home.bin.watch_again=Watch again home.bin.daily_random_music=Discover Music diff --git a/server/src/compat/youtube.rs b/server/src/compat/youtube.rs index 9d27235..a5f540b 100644 --- a/server/src/compat/youtube.rs +++ b/server/src/compat/youtube.rs @@ -38,7 +38,8 @@ pub fn r_youtube_watch(ri: RequestInfo<'_>, v: &str) -> MyResult<Redirect> { } #[get("/channel/<id>")] -pub fn r_youtube_channel(ri: RequestInfo<'_>, id: &str) -> MyResult<Redirect> { +pub fn r_youtube_channel(_ri: RequestInfo<'_>, id: &str) -> MyResult<Redirect> { + let _ = id; // let Some(id) = (if id.starts_with("UC") { // get_node_by_eid(&session.0, IdentifierType::YoutubeChannel, id)? // } else if id.starts_with("@") { @@ -54,7 +55,8 @@ pub fn r_youtube_channel(ri: RequestInfo<'_>, id: &str) -> MyResult<Redirect> { } #[get("/embed/<v>")] -pub fn r_youtube_embed(ri: RequestInfo<'_>, v: &str) -> MyResult<Redirect> { +pub fn r_youtube_embed(_ri: RequestInfo<'_>, v: &str) -> MyResult<Redirect> { + let _ = v; // if v.len() != 11 { // Err(anyhow!("video id length incorrect"))? // } diff --git a/server/src/ui/home.rs b/server/src/ui/home.rs index f6340be..0fd4432 100644 --- a/server/src/ui/home.rs +++ b/server/src/ui/home.rs @@ -45,6 +45,15 @@ pub fn r_home(ri: RequestInfo<'_>) -> MyResult<UiResponse> { ); page.push( VIEW_NODE_LIST, + home_row_highlight( + &ri, + "home.bin.daily_random", + "FILTER (visi = visi AND kind = movi) SORT RANDOM", + )? + .as_object(), + ); + page.push( + VIEW_NODE_LIST, home_row( &ri, "home.bin.max_rating", @@ -77,3 +86,21 @@ fn home_row(ri: &RequestInfo<'_>, title: &str, query: &str) -> Result<ObjectBuff })?; Ok(res) } + +fn home_row_highlight(ri: &RequestInfo<'_>, title: &str, query: &str) -> Result<ObjectBuffer> { + let q = Query::from_str(query).context("parse query")?; + let mut res = ObjectBuffer::empty(); + ri.state.database.transaction(&mut |txn| { + let Some(row) = txn.query(q.clone())?.next() else { + return Ok(()); + }; + let row = row?.0; + let node = txn.get(row)?.unwrap(); + let nku = ObjectBuffer::new(&mut [(NKU_NODE.0, &node.as_object())]); + res = Object::EMPTY.insert(NODELIST_DISPLAYSTYLE, NLSTYLE_HIGHLIGHT); + res = res.as_object().insert(NODELIST_TITLE, title); + res = res.as_object().insert(NODELIST_ITEM, nku.as_object()); + Ok(()) + })?; + Ok(res) +} diff --git a/ui/client-style/src/node_card.css b/ui/client-style/src/node_card.css index 2c0b97f..43c3898 100644 --- a/ui/client-style/src/node_card.css +++ b/ui/client-style/src/node_card.css @@ -68,7 +68,7 @@ grid-area: 1 / 1; } -.card .poster .cardhover { +.card .poster .overlay { position: relative; pointer-events: none; grid-area: 1 / 1; @@ -79,7 +79,7 @@ justify-content: center; align-items: center; } -.card .poster:hover .cardhover { +.card .poster:hover .overlay { opacity: 1; } @@ -90,12 +90,11 @@ .card .poster a img { transition: transform 0.3s; } - .card .poster:hover a img { transform: scale(1.1); } -.card .poster .cardhover a.play { +.card .poster .overlay a.play { text-decoration: none; width: 1em; height: 1em; @@ -108,40 +107,61 @@ background-color: var(--overlay); transition: background-color 0.3s, font-size 0.3s; } -.card .poster .cardhover a.play:hover { +.card .poster .overlay a.play:hover { background-color: var(--overlay-hover); font-size: 3em; } -.card .poster .cardhover .props { +.card .poster .overlay .props { position: absolute; bottom: 0px; left: 0px; } -.widecard { +.card.wide { display: grid; grid-template-columns: 1fr 10000fr; width: 100%; } -.widecard .poster { +.card.wide .poster { grid-column: 1; } -.widecard .details { +.card.wide .details { grid-column: 2; margin: 1em; } -.widecard .details .title { +.card.wide .details .title { font-size: large; } -.widecard .details .props { +.card.wide .details .props { margin-bottom: 0.5em; } -@media (max-width: 750px) { - nav .library { - display: none; - } - .children { - justify-content: center; - } +.card.highlight { + padding: 0em; + background-size: cover; + background-position: center; + background-image: linear-gradient(#0009); + border-radius: 1em; + width: 100%; + box-sizing: border-box; +} +.card.highlight .inner { + height: var(--card-size); + padding: 2em; + display: flex; + flex-direction: row; + backdrop-filter: blur(5px); + background-image: linear-gradient( + 90deg, + #000a 0%, + #0005 60%, + transparent 100% + ); +} +.card.highlight .overview h2 { + margin-bottom: 0em; +} +.card.highlight .poster { + flex-shrink: 0; + margin-left: 5em; } diff --git a/ui/client-style/src/node_page.css b/ui/client-style/src/node_page.css index 62c8c7e..b8306e8 100644 --- a/ui/client-style/src/node_page.css +++ b/ui/client-style/src/node_page.css @@ -4,7 +4,7 @@ Copyright (C) 2026 metamuffin <metamuffin.org> Copyright (C) 2023 tpart */ -.backdrop { +#main > .backdrop { width: calc(100% + 2 * var(--main-side-margin)); height: min(50vh, calc(var(--backdrop-height) + 5em)); margin-left: calc(-1 * var(--main-side-margin)); diff --git a/ui/src/components/node_card.rs b/ui/src/components/node_card.rs index eb00ccc..d93825b 100644 --- a/ui/src/components/node_card.rs +++ b/ui/src/components/node_card.rs @@ -18,13 +18,12 @@ markup::define! { NodeCard<'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 cls = format!("node card poster {}", aspect_class(node)); - div[class=cls] { + div[class=&format!("card {}", aspect_class(node))] { .poster { a[href=u_node_slug(&slug)] { img[src=cover_image(&node, 512), loading="lazy"]; } - .cardhover.item { + .overlay { @if node.has(NO_TRACK.0) { a.play.icon[href=u_node_slug_player(&slug)] { "play_arrow" } } @@ -43,15 +42,16 @@ markup::define! { } } } + NodeCardWide<'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(); - div[class="node card widecard poster"] { + div[class="card wide"] { div[class=&format!("poster {}", aspect_class(node))] { a[href=u_node_slug(&slug)] { img[src=cover_image(&node, 512), loading="lazy"]; } - .cardhover.item { + .overlay { @if node.has(NO_TRACK.0) { a.play.icon[href=u_node_slug_player(&slug)] { "play_arrow" } } @@ -64,6 +64,31 @@ markup::define! { } } } + + NodeCardHightlight<'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 backdrop = u_image(node.get(NO_PICTURES).unwrap_or_default().get(PICT_BACKDROP).unwrap_or_default(), 2048); + div[class="card highlight", style=format!("background-image: url(\"{backdrop}\")")] { + .inner { + div.overview { + h2 { a[href=u_node_slug(slug)] { @node.get(NO_TITLE) } } + @Props { ri, nku: *nku, full: false } + p { b { @node.get(NO_TAGLINE) } " " @node.get(NO_DESCRIPTION) } + } + div[class=&format!("poster {}", aspect_class(node))] { + a[href=u_node_slug(&slug)] { + img[src=cover_image(&node, 512), loading="lazy"]; + } + .overlay { + @if node.has(NO_TRACK.0) { + a.play.icon[href=u_node_slug_player(&slug)] { "play_arrow" } + } + } + } + } + } + } } fn cover_image(node: &Object, size: usize) -> String { diff --git a/ui/src/components/node_list.rs b/ui/src/components/node_list.rs index d1d16fb..679a11d 100644 --- a/ui/src/components/node_list.rs +++ b/ui/src/components/node_list.rs @@ -6,7 +6,7 @@ use crate::{ RenderInfo, - components::node_card::{NodeCard, NodeCardWide}, + components::node_card::{NodeCard, NodeCardHightlight, NodeCardWide}, }; use jellycommon::{jellyobject::Object, *}; use jellyui_locale::tr; @@ -17,20 +17,28 @@ markup::define! { @if let Some(title) = nl.get(NODELIST_TITLE) { h2 { @tr(ri.lang, title) } } - @if ds == NLSTYLE_GRID { - ul.nl.grid { @for nku in nl.iter(NODELIST_ITEM) { - li { @NodeCard { ri, nku } } - }} - } - @if ds == NLSTYLE_INLINE { - ul.nl.inline { @for nku in nl.iter(NODELIST_ITEM) { - li { @NodeCard { ri, nku } } - }} - } - @if ds == NLSTYLE_LIST { - ol.nl.list { @for nku in nl.iter(NODELIST_ITEM) { - li { @NodeCardWide { ri, nku } } - }} + @match ds { + NLSTYLE_GRID => { + ul.nl.grid { @for nku in nl.iter(NODELIST_ITEM) { + li { @NodeCard { ri, nku } } + }} + } + NLSTYLE_INLINE => { + ul.nl.inline { @for nku in nl.iter(NODELIST_ITEM) { + li { @NodeCard { ri, nku } } + }} + } + NLSTYLE_LIST => { + ol.nl.list { @for nku in nl.iter(NODELIST_ITEM) { + li { @NodeCardWide { ri, nku } } + }} + } + NLSTYLE_HIGHLIGHT => { + @if let Some(nku) = nl.get(NODELIST_ITEM) { + @NodeCardHightlight { ri, nku } + } + } + _ => {} } } } diff --git a/ui/src/components/node_page.rs b/ui/src/components/node_page.rs index 7deaf61..5823933 100644 --- a/ui/src/components/node_page.rs +++ b/ui/src/components/node_page.rs @@ -181,6 +181,7 @@ markup::define! { } Player<'a>(ri: &'a RenderInfo<'a>, nku: Object<'a>) { + @let _ = ri; @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))] {} @@ -188,11 +189,11 @@ markup::define! { } -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 -} +// 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); diff --git a/ui/src/components/props.rs b/ui/src/components/props.rs index c11dca6..5fa9d3e 100644 --- a/ui/src/components/props.rs +++ b/ui/src/components/props.rs @@ -47,11 +47,11 @@ markup::define! { RTYP_YOUTUBE_LIKES => {p.likes{ @format_count(value as usize) " Likes" }} RTYP_YOUTUBE_VIEWS => {p{ @format_count(value as usize) " Views" }} RTYP_YOUTUBE_SUBSCRIBERS => {p{ @format_count(value as usize) " Subscribers" }} - RTYP_ROTTEN_TOMATOES => {p.rating{ "Rotten Tomatoes: " @value "%" }} + RTYP_ROTTEN_TOMATOES if *full => {p.rating{ "Rotten Tomatoes: " @value "%" }} RTYP_METACRITIC if *full => {p.rating{ "Metacritic: " @value "/100" }} RTYP_IMDB => {p.rating{ "IMDb " @value }} - RTYP_TMDB => {p.rating{ "TMDB " @format!("{:.01}", value) }} - RTYP_TRAKT => {p.rating{ "Trakt " @format!("{:.01}", value) }} + RTYP_TMDB if *full => {p.rating{ "TMDB " @format!("{:.01}", value) }} + RTYP_TRAKT if *full => {p.rating{ "Trakt " @format!("{:.01}", value) }} _ => {} } } diff --git a/ui/src/format.rs b/ui/src/format.rs index 4eb8f84..46d7154 100644 --- a/ui/src/format.rs +++ b/ui/src/format.rs @@ -4,12 +4,8 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ -use jellycommon::{ - jellyobject::{Object, Tag}, - *, -}; use jellyui_locale::tr; -use std::{borrow::Cow, fmt::Write}; +use std::fmt::Write; pub fn format_duration(d: f64) -> String { format_duration_mode("en", d, false) @@ -73,48 +69,30 @@ fn test_duration_long() { pub fn format_size(size: u64) -> String { humansize::format_size(size, humansize::DECIMAL) } -pub fn format_kind(lang: &str, kind: Tag) -> Cow<'static, str> { - tr( - lang, - match kind { - KIND_MOVIE => "kind.movie", - KIND_VIDEO => "kind.video", - KIND_MUSIC => "kind.music", - KIND_SHORTFORMVIDEO => "kind.short_form_video", - KIND_COLLECTION => "kind.collection", - KIND_CHANNEL => "kind.channel", - KIND_SHOW => "kind.show", - KIND_SERIES => "kind.series", - KIND_SEASON => "kind.season", - KIND_EPISODE => "kind.episode", - _ => "kind.unknown", - }, - ) -} -pub fn node_resolution_name(node: &Object) -> &'static str { - let mut maxdim = 0; - for t in node.iter(NO_TRACK) { - if let Some(width) = t.get(TR_PIXEL_WIDTH) { - maxdim = maxdim.max(width) - } - if let Some(height) = t.get(TR_PIXEL_HEIGHT) { - maxdim = maxdim.max(height) - } - } - match maxdim { - 30720.. => "32K", - 15360.. => "16K", - 7680.. => "8K UHD", - 5120.. => "5K UHD", - 3840.. => "4K UHD", - 2560.. => "QHD 1440p", - 1920.. => "FHD 1080p", - 1280.. => "HD 720p", - 854.. => "SD 480p", - _ => "Unkown", - } -} +// pub fn node_resolution_name(node: &Object) -> &'static str { +// let mut maxdim = 0; +// for t in node.iter(NO_TRACK) { +// if let Some(width) = t.get(TR_PIXEL_WIDTH) { +// maxdim = maxdim.max(width) +// } +// if let Some(height) = t.get(TR_PIXEL_HEIGHT) { +// maxdim = maxdim.max(height) +// } +// } +// match maxdim { +// 30720.. => "32K", +// 15360.. => "16K", +// 7680.. => "8K UHD", +// 5120.. => "5K UHD", +// 3840.. => "4K UHD", +// 2560.. => "QHD 1440p", +// 1920.. => "FHD 1080p", +// 1280.. => "HD 720p", +// 854.. => "SD 480p", +// _ => "Unkown", +// } +// } pub fn format_count(n: impl Into<usize>) -> String { let n: usize = n.into(); @@ -128,13 +106,13 @@ pub fn format_count(n: impl Into<usize>) -> String { } } -pub fn format_chapter(c: &Object) -> (String, String) { - ( - format!( - "{}-{}", - c.get(CH_START).map(format_duration).unwrap_or_default(), - c.get(CH_END).map(format_duration).unwrap_or_default(), - ), - c.get(CH_NAME).unwrap_or_default().to_string(), - ) -} +// pub fn format_chapter(c: &Object) -> (String, String) { +// ( +// format!( +// "{}-{}", +// c.get(CH_START).map(format_duration).unwrap_or_default(), +// c.get(CH_END).map(format_duration).unwrap_or_default(), +// ), +// c.get(CH_NAME).unwrap_or_default().to_string(), +// ) +// } |