From 73d2d5eb01fceae9e0b1c58afb648822000c878a Mon Sep 17 00:00:00 2001 From: metamuffin Date: Mon, 28 Apr 2025 21:50:51 +0200 Subject: yes --- common/src/api.rs | 26 ++------ common/src/impl.rs | 98 ++--------------------------- common/src/lib.rs | 170 ++++++++++++++++++++++++++++++-------------------- common/src/routes.rs | 63 +++++++++++++++++-- common/src/user.rs | 70 ++++++++++----------- ui/src/filter_sort.rs | 6 +- ui/src/lib.rs | 15 +++-- ui/src/node_card.rs | 22 ++++--- ui/src/node_page.rs | 34 ++++++---- ui/src/scaffold.rs | 44 +++++-------- ui/src/settings.rs | 31 ++++----- 11 files changed, 284 insertions(+), 295 deletions(-) diff --git a/common/src/api.rs b/common/src/api.rs index a58c445..aaff940 100644 --- a/common/src/api.rs +++ b/common/src/api.rs @@ -3,7 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ -use crate::{user::NodeUserData, Node, NodeKind}; +use crate::{url_enum, user::NodeUserData, Node, NodeKind}; use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, sync::Arc, time::Duration}; @@ -53,32 +53,14 @@ pub struct StatsBin { } #[derive(Debug, Default, Clone)] -#[cfg_attr(feature = "rocket", derive(FromForm, UriDisplayQuery))] pub struct NodeFilterSort { pub sort_by: Option, pub filter_kind: Option>, pub sort_order: Option, } - -pub trait UrlEnum: Sized { - fn to_str(&self) -> &'static str; - fn from_str(s: &str) -> Option; -} -macro_rules! url_enum { - (enum $i:ident { $($vi:ident = $vk:literal),*, }) => { - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - #[cfg_attr(feature = "rocket", derive(FromFormField, UriDisplayQuery))] - pub enum $i { $(#[cfg_attr(feature = "rocket", field(value = $vk))] $vi),* } - impl $i { pub const ALL: &'static [$i] = &[$($i::$vi),*]; } - impl UrlEnum for $i { - fn to_str(&self) -> &'static str { match self { $(Self::$vi => $vk),* } } - fn from_str(s: &str) -> Option { match s { $($vk => Some(Self::$vi) ),*, _ => None } } - } - }; -} - url_enum!( + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum FilterProperty { FederationLocal = "fed_local", FederationRemote = "fed_remote", @@ -98,6 +80,7 @@ url_enum!( } ); url_enum!( + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum SortProperty { ReleaseDate = "release_date", Title = "title", @@ -115,8 +98,9 @@ url_enum!( } ); url_enum!( + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum SortOrder { Ascending = "ascending", Descending = "descending", - } + } ); diff --git a/common/src/impl.rs b/common/src/impl.rs index 5b35be3..690a189 100644 --- a/common/src/impl.rs +++ b/common/src/impl.rs @@ -4,10 +4,8 @@ Copyright (C) 2025 metamuffin */ use crate::{ - Node, NodeID, NodeIDOrSlug, ObjectIds, PeopleGroup, SourceTrack, SourceTrackKind, TmdbKind, - TraktKind, + Node, NodeID, NodeIDOrSlug, ObjectIds, SourceTrack, SourceTrackKind, TmdbKind, TraktKind, }; -use hex::FromHexError; use serde::{Deserialize, Serialize}; use std::{fmt::Display, str::FromStr}; @@ -111,58 +109,6 @@ impl Display for ObjectIds { Ok(()) } } -impl Display for PeopleGroup { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - PeopleGroup::Cast => "Cast", - PeopleGroup::Writing => "Writing", - PeopleGroup::Directing => "Directing", - PeopleGroup::Art => "Art", - PeopleGroup::Sound => "Sound", - PeopleGroup::Camera => "Camera", - PeopleGroup::Lighting => "Lighting", - PeopleGroup::Crew => "Crew", - PeopleGroup::Editing => "Editing", - PeopleGroup::Production => "Production", - PeopleGroup::Vfx => "Visual Effects", - PeopleGroup::CostumeMakeup => "Costume & Makeup", - PeopleGroup::CreatedBy => "Created by:", - PeopleGroup::Performance => "Performance", - PeopleGroup::Instrument => "Instrument", - PeopleGroup::Vocal => "Vocal", - PeopleGroup::Arranger => "Arranger", - PeopleGroup::Producer => "Producer", - PeopleGroup::Engineer => "Engineer", - }) - } -} -impl FromStr for PeopleGroup { - type Err = (); - fn from_str(s: &str) -> Result { - Ok(match s { - "Cast" => PeopleGroup::Cast, - "Writing" => PeopleGroup::Writing, - "Directing" => PeopleGroup::Directing, - "Art" => PeopleGroup::Art, - "Sound" => PeopleGroup::Sound, - "Camera" => PeopleGroup::Camera, - "Lighting" => PeopleGroup::Lighting, - "Crew" => PeopleGroup::Crew, - "Editing" => PeopleGroup::Editing, - "Production" => PeopleGroup::Production, - "Visual Effects" => PeopleGroup::Vfx, - "Costume & Makeup" => PeopleGroup::CostumeMakeup, - "Created by:" => PeopleGroup::CreatedBy, - "Performance" => PeopleGroup::Performance, - "Instrument" => PeopleGroup::Instrument, - "Vocal" => PeopleGroup::Vocal, - "Arranger" => PeopleGroup::Arranger, - "Producer" => PeopleGroup::Producer, - "Engineer" => PeopleGroup::Engineer, - _ => return Err(()), - }) - } -} impl NodeID { pub fn from_slug(slug: &str) -> Self { @@ -186,50 +132,18 @@ impl NodeID { pub const MAX: NodeID = NodeID([255; 32]); } -#[cfg(feature = "rocket")] -impl<'a> rocket::request::FromParam<'a> for NodeID { - type Error = FromHexError; - fn from_param(param: &'a str) -> Result { - if let Some(id) = param.strip_prefix("+") { +impl FromStr for NodeID { + type Err = hex::FromHexError; + fn from_str(s: &str) -> Result { + if let Some(id) = s.strip_prefix("+") { let mut k = [0; 32]; hex::decode_to_slice(id, &mut k)?; Ok(NodeID(k)) } else { - Ok(NodeID::from_slug(param)) + Ok(NodeID::from_slug(s)) } } } -#[cfg(feature = "rocket")] -impl rocket::http::uri::fmt::FromUriParam for NodeID { - type Target = NodeID; - fn from_uri_param(param: NodeID) -> Self::Target { - param - } -} -#[cfg(feature = "rocket")] -impl<'a> rocket::http::uri::fmt::FromUriParam for NodeID { - type Target = &'a str; - fn from_uri_param(param: &'a String) -> Self::Target { - param.as_str() - } -} -#[cfg(feature = "rocket")] -impl<'a> rocket::http::uri::fmt::FromUriParam for NodeID { - type Target = &'a str; - fn from_uri_param(param: &'a str) -> Self::Target { - param - } -} -#[cfg(feature = "rocket")] -impl rocket::http::uri::fmt::UriDisplay for NodeID { - fn fmt( - &self, - f: &mut rocket::http::uri::fmt::Formatter<'_, rocket::http::uri::fmt::Path>, - ) -> std::fmt::Result { - f.write_value(format!("+{}", hex::encode(self.0)))?; - Ok(()) - } -} impl Display for NodeID { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("+")?; diff --git a/common/src/lib.rs b/common/src/lib.rs index c606b86..26bf361 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -9,9 +9,9 @@ pub mod config; pub mod helpers; pub mod r#impl; pub mod jhls; +pub mod routes; pub mod stream; pub mod user; -pub mod routes; pub use chrono; @@ -22,6 +22,30 @@ use std::{ path::PathBuf, }; +#[macro_export] +macro_rules! url_enum { + ($(#[$a:meta])* enum $i:ident { $($(#[$va:meta])* $vi:ident = $vk:literal),*, }) => { + $(#[$a])* + pub enum $i { $($(#[$va])* $vi),* } + impl $i { + pub const ALL: &'static [$i] = &[$($i::$vi),*]; + pub fn to_str(&self) -> &'static str { match self { $(Self::$vi => $vk),* } } + pub fn from_str(s: &str) -> Option { match s { $($vk => Some(Self::$vi) ),*, _ => None } } + } + impl std::fmt::Display for $i { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.to_str()) + } + } + impl std::str::FromStr for $i { + type Err = (); + fn from_str(s: &str) -> Result { + Self::from_str(s).ok_or(()) + } + } + }; +} + #[derive(Clone, Copy, Debug, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct NodeID(pub [u8; 32]); @@ -90,31 +114,35 @@ pub struct ObjectIds { pub tvdb: Option, } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)] -#[serde(rename_all = "snake_case")] -pub enum PeopleGroup { - Cast, - Writing, - Directing, - Art, - Sound, - Camera, - Lighting, - Crew, - Editing, - Production, - Vfx, - CostumeMakeup, - CreatedBy, - // https://musicbrainz.org/relationships/artist-recording - // modelling after this, but its too many categories - Performance, - Instrument, - Vocal, - Arranger, - Producer, - Engineer, -} +url_enum!( + #[derive( + Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Encode, Decode, + )] + #[serde(rename_all = "snake_case")] + enum PeopleGroup { + Cast = "cast", + Writing = "writing", + Directing = "directing", + Art = "art", + Sound = "sound", + Camera = "camera", + Lighting = "lighting", + Crew = "crew", + Editing = "editing", + Production = "production", + Vfx = "vfx", + CostumeMakeup = "costume_makeup", + CreatedBy = "created_by", + // https://musicbrainz.org/relationships/artist-recording + // modelling after this, but its too many categories + Performance = "performance", + Instrument = "instrument", + Vocal = "vocal", + Arranger = "arranger", + Producer = "producer", + Engineer = "engineer", + } +); #[derive( Debug, @@ -138,35 +166,37 @@ pub enum Visibility { Visible, } -#[derive( - Debug, - Clone, - Copy, - Deserialize, - Serialize, - PartialEq, - Eq, - Default, - Encode, - Decode, - PartialOrd, - Ord, -)] -#[serde(rename_all = "snake_case")] -pub enum NodeKind { - #[default] - Unknown, - Movie, - Video, - Music, - ShortFormVideo, - Collection, - Channel, - Show, - Series, - Season, - Episode, -} +url_enum!( + #[derive( + Debug, + Clone, + Copy, + Deserialize, + Serialize, + PartialEq, + Eq, + Default, + Encode, + Decode, + PartialOrd, + Ord, + )] + #[serde(rename_all = "snake_case")] + enum NodeKind { + #[default] + Unknown = "unknown", + Movie = "movie", + Video = "video", + Music = "music", + ShortFormVideo = "short_form_video", + Collection = "collection", + Channel = "channel", + Show = "show", + Series = "series", + Season = "season", + Episode = "episode", + } +); #[derive(Debug, Clone, Deserialize, Serialize, Encode, Decode)] #[serde(rename_all = "snake_case")] @@ -213,20 +243,22 @@ pub struct SourceTrack { pub federated: Vec, } -#[derive( - Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash, Encode, Decode, -)] -#[serde(rename_all = "snake_case")] -pub enum Rating { - Imdb, - Tmdb, - RottenTomatoes, - Metacritic, - YoutubeViews, - YoutubeLikes, - YoutubeFollowers, - Trakt, -} +url_enum!( + #[derive( + Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash, Encode, Decode, + )] + #[serde(rename_all = "snake_case")] + enum Rating { + Imdb = "imdb", + Tmdb = "tmdb", + RottenTomatoes = "rotten_tomatoes", + Metacritic = "metacritic", + YoutubeViews = "youtube_views", + YoutubeLikes = "youtube_likes", + YoutubeFollowers = "youtube_followers", + Trakt = "trakt", + } +); #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Encode, Decode)] #[serde(rename_all = "snake_case")] diff --git a/common/src/routes.rs b/common/src/routes.rs index 71ca3fa..e510e22 100644 --- a/common/src/routes.rs +++ b/common/src/routes.rs @@ -3,15 +3,70 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ - -use crate::NodeID; +use crate::{user::ApiWatchedState, NodeID, PeopleGroup}; pub fn u_home() -> String { "/home".to_owned() } pub fn u_node_id(node: NodeID) -> String { - format!("/n/{}", node) + format!("/n/{node}") } pub fn u_node_slug(node: &str) -> String { - format!("/n/{}", node) + format!("/n/{node}") +} +pub fn u_node_slug_player(node: &str) -> String { + format!("/n/{node}/player") +} +pub fn u_node_slug_player_time(node: &str, time: f64) -> String { + format!("/n/{node}/player?t={time}") +} +pub fn u_node_slug_poster(node: &str, width: usize) -> String { + format!("/n/{node}/poster?width={width}") +} +pub fn u_node_slug_backdrop(node: &str, width: usize) -> String { + format!("/n/{node}/backdrop?width={width}") +} +pub fn u_node_slug_watched(node: &str, state: ApiWatchedState) -> String { + format!("/n/{node}/watched?state={state}") +} +pub fn u_node_slug_person_asset( + node: &str, + group: PeopleGroup, + index: usize, + width: usize, +) -> String { + format!("/n/{node}/person/{index}/asset?group={group}&width={width}") +} +pub fn u_node_slug_thumbnail(node: &str, time: f64, width: usize) -> String { + format!("/n/{node}/thumbnail?t={time}&width={width}") +} +pub fn u_node_slug_update_rating(node: &str) -> String { + format!("/n/{node}/update_rating") +} +pub fn u_node_slug_progress(node: &str, time: f64) -> String { + format!("/n/{node}/progress?t={time}") +} +pub fn u_account_register() -> String { + "/account/register".to_owned() +} +pub fn u_account_login() -> String { + "/account/login".to_owned() +} +pub fn u_account_logout() -> String { + "/account/logout".to_owned() +} +pub fn u_admin_dashboard() -> String { + "/admin/dashboard".to_owned() +} +pub fn u_account_settings() -> String { + "/account/settings".to_owned() +} +pub fn u_stats() -> String { + "/stats".to_owned() +} +pub fn u_search() -> String { + "/search".to_owned() +} +pub fn u_items() -> String { + "/items".to_owned() } diff --git a/common/src/user.rs b/common/src/user.rs index e0e7a0d..c6da166 100644 --- a/common/src/user.rs +++ b/common/src/user.rs @@ -4,14 +4,14 @@ Copyright (C) 2025 metamuffin */ use bincode::{Decode, Encode}; -#[cfg(feature = "rocket")] -use rocket::{FromFormField, UriDisplayQuery}; use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet}, fmt::Display, }; +use crate::url_enum; + #[rustfmt::skip] #[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, Default)] pub struct User { @@ -41,6 +41,16 @@ pub enum WatchedState { Pending, } +url_enum!( + #[derive(Debug, Clone, Serialize, Deserialize)] + #[serde(rename_all = "snake_case")] + enum ApiWatchedState { + None = "none", + Watched = "watched", + Pending = "pending", + } +); + #[derive(Debug, Serialize, Deserialize)] pub struct CreateSessionParams { pub username: String, @@ -49,42 +59,28 @@ pub struct CreateSessionParams { pub drop_permissions: Option>, } -#[derive(Debug, Clone, Copy, Serialize, Default, Deserialize, PartialEq, Encode, Decode)] -#[cfg_attr(feature = "rocket", derive(FromFormField, UriDisplayQuery))] -#[serde(rename_all = "snake_case")] -pub enum Theme { - #[default] - Dark, - Light, - Purple, - Black, -} - -#[derive(Debug, Clone, Copy, Serialize, Default, Deserialize, PartialEq, Encode, Decode)] -#[cfg_attr(feature = "rocket", derive(FromFormField, UriDisplayQuery))] -#[serde(rename_all = "snake_case")] -pub enum PlayerKind { - #[default] - Browser, - Native, - NativeFullscreen, -} +url_enum!( + #[derive(Debug, Clone, Copy, Serialize, Default, Deserialize, PartialEq, Encode, Decode)] + #[serde(rename_all = "snake_case")] + enum Theme { + #[default] + Dark = "dark", + Light = "light", + Purple = "purple", + Black = "black", + } +); -impl Theme { - pub const LIST: &'static [(Theme, &'static str)] = &[ - (Theme::Dark, "Dark"), - (Theme::Light, "Light"), - (Theme::Purple, "Purple"), - (Theme::Black, "Black"), - ]; -} -impl PlayerKind { - pub const LIST: &'static [(PlayerKind, &'static str)] = &[ - (PlayerKind::Browser, "In-Browser"), - (PlayerKind::Native, "Native"), - (PlayerKind::NativeFullscreen, "Native (Fullscreen)"), - ]; -} +url_enum!( + #[derive(Debug, Clone, Copy, Serialize, Default, Deserialize, PartialEq, Encode, Decode)] + #[serde(rename_all = "snake_case")] + enum PlayerKind { + #[default] + Browser = "browser", + Native = "native", + NativeFullscreen = "native_fullscreen", + } +); #[derive(Debug, Clone, Serialize, Deserialize, Default, Encode, Decode)] pub struct PermissionSet(pub HashMap); diff --git a/ui/src/filter_sort.rs b/ui/src/filter_sort.rs index ec83f6f..4d1a1ad 100644 --- a/ui/src/filter_sort.rs +++ b/ui/src/filter_sort.rs @@ -119,17 +119,17 @@ markup::define! { struct A(pub T); impl markup::Render for A { fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result { - writer.write_fmt(format_args!("{}", self as &dyn UriDisplay)) + writer.write_str(self.0.to_str()) } } impl markup::Render for A { fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result { - writer.write_fmt(format_args!("{}", self as &dyn UriDisplay)) + writer.write_str(self.0.to_str()) } } impl markup::Render for A { fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result { - writer.write_fmt(format_args!("{}", self as &dyn UriDisplay)) + writer.write_str(self.0.to_str()) } } impl RenderAttributeValue for A {} diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 4298623..67dc067 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -1,5 +1,3 @@ -use markup::DynRender; - /* 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. @@ -28,7 +26,16 @@ pub trait Page { } } +use markup::DynRender; +use scaffold::Scaffold; + pub fn render_page(page: &dyn Page) -> String { - // page.render() - "a".to_string() + Scaffold { + lang, + context, + class: page.class().unwrap_or("aaaa"), + title: page.title(), + main: page.to_render(), + } + .to_string() } diff --git a/ui/src/node_card.rs b/ui/src/node_card.rs index cedb81e..b4481b7 100644 --- a/ui/src/node_card.rs +++ b/ui/src/node_card.rs @@ -5,25 +5,29 @@ */ use crate::{locale::Language, node_page::aspect_class, props::Props}; -use jellycommon::{Node, user::NodeUserData}; +use jellycommon::{ + Node, + routes::{u_node_slug, u_node_slug_player, u_node_slug_poster}, + user::NodeUserData, +}; markup::define! { NodeCard<'a>(node: &'a Node, udata: &'a NodeUserData, lang: &'a Language) { @let cls = format!("node card poster {}", aspect_class(node.kind)); div[class=cls] { .poster { - a[href=uri!(r_library_node(&node.slug))] { - img[src=uri!(r_item_poster(&node.slug, Some(1024))), loading="lazy"]; + a[href=u_node_slug(&node.slug)] { + img[src=u_node_slug_poster(&node.slug, 1024), loading="lazy"]; } .cardhover.item { @if node.media.is_some() { - a.play.icon[href=&uri!(r_player(&node.slug, PlayerConfig::default()))] { "play_arrow" } + a.play.icon[href=u_node_slug_player(&node.slug)] { "play_arrow" } } @Props { node, udata, full: false, lang } } } div.title { - a[href=uri!(r_library_node(&node.slug))] { + a[href=u_node_slug(&node.slug)] { @node.title } } @@ -37,17 +41,17 @@ markup::define! { NodeCardWide<'a>(node: &'a Node, udata: &'a NodeUserData, lang: &'a Language) { div[class="node card widecard poster"] { div[class=&format!("poster {}", aspect_class(node.kind))] { - a[href=uri!(r_library_node(&node.slug))] { - img[src=uri!(r_item_poster(&node.slug, Some(1024))), loading="lazy"]; + a[href=u_node_slug(&node.slug)] { + img[src=u_node_slug_poster(&node.slug, 1024), loading="lazy"]; } .cardhover.item { @if node.media.is_some() { - a.play.icon[href=&uri!(r_player(&node.slug, PlayerConfig::default()))] { "play_arrow" } + a.play.icon[href=u_node_slug_player(&node.slug)] { "play_arrow" } } } } div.details { - a.title[href=uri!(r_library_node(&node.slug))] { @node.title } + a.title[href=u_node_slug(&node.slug)] { @node.title } @Props { node, udata, full: false, lang } span.overview { @node.description } } diff --git a/ui/src/node_page.rs b/ui/src/node_page.rs index b48fca2..7fb299f 100644 --- a/ui/src/node_page.rs +++ b/ui/src/node_page.rs @@ -15,7 +15,12 @@ use crate::{ use jellycommon::{ Chapter, Node, NodeKind, PeopleGroup, api::NodeFilterSort, - user::{NodeUserData, WatchedState}, + routes::{ + u_node_slug, u_node_slug_backdrop, u_node_slug_person_asset, u_node_slug_player, + u_node_slug_player_time, u_node_slug_poster, u_node_slug_thumbnail, + u_node_slug_update_rating, u_node_slug_watched, + }, + user::{ApiWatchedState, NodeUserData, WatchedState}, }; use std::sync::Arc; @@ -23,6 +28,9 @@ impl Page for NodePage<'_> { fn title(&self) -> String { self.node.title.clone().unwrap_or_default() } + fn to_render(&self) -> markup::DynRender { + markup::new!(@self) + } } markup::define! { @@ -37,43 +45,43 @@ markup::define! { player: bool, ) { @if !matches!(node.kind, NodeKind::Collection) && !player { - img.backdrop[src=uri!(r_item_backdrop(&node.slug, Some(2048))), loading="lazy"]; + img.backdrop[src=u_node_slug_backdrop(&node.slug, 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=uri!(r_item_poster(&node.slug, Some(2048))), loading="lazy"]; } + div[class=cls] { img[src=u_node_slug_poster(&node.slug, 2048), loading="lazy"]; } } .title { h1 { @node.title } ul.parents { @for (node, _) in *parents { li { - a.component[href=uri!(r_library_node(&node.slug))] { @node.title } + a.component[href=u_node_slug(&node.slug)] { @node.title } }}} @if node.media.is_some() { - a.play[href=&uri!(r_player(&node.slug, PlayerConfig::default()))] { @trs(lang, "node.player_link") } + 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=uri!(r_node_userdata_watched(&node.slug, UrlWatchedState::Watched))] { + 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=uri!(r_node_userdata_watched(&node.slug, UrlWatchedState::None))] { + 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=uri!(r_node_userdata_watched(&node.slug, UrlWatchedState::Pending))] { + 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=uri!(r_node_userdata_watched(&node.slug, UrlWatchedState::None))] { + 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=uri!(r_node_userdata_rating(&node.slug))] { + 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")]; } @@ -92,8 +100,8 @@ markup::define! { @let (inl, sub) = format_chapter(chap); li { .card."aspect-thumb" { .poster { - a[href=&uri!(r_player(&node.slug, PlayerConfig::seek(chap.time_start.unwrap_or(0.))))] { - img[src=&uri!(r_node_thumbnail(&node.slug, chapter_key_time(chap, media.duration), Some(1024))), loading="lazy"]; + 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 } } } } @@ -110,7 +118,7 @@ markup::define! { li { .card."aspect-port" { .poster { a[href="#"] { - img[src=&uri!(r_person_asset(&node.slug, i, group.to_string(), Some(1024))), loading="lazy"]; + img[src=u_node_slug_person_asset(&node.slug, *group, i, 1024), loading="lazy"]; } } .title { diff --git a/ui/src/scaffold.rs b/ui/src/scaffold.rs index cc5886b..bcff54c 100644 --- a/ui/src/scaffold.rs +++ b/ui/src/scaffold.rs @@ -5,7 +5,11 @@ */ use crate::locale::{Language, escape, tr, trs}; -use markup::{DynRender, Render, raw}; +use jellycommon::routes::{ + u_account_login, u_account_logout, u_account_register, u_account_settings, u_admin_dashboard, + u_home, u_items, u_node_slug, u_search, u_stats, +}; +use markup::{Render, raw}; use std::sync::LazyLock; static LOGO_ENABLED: LazyLock = LazyLock::new(|| CONF.asset_path.join("logo.svg").exists()); @@ -22,25 +26,25 @@ markup::define! { } body[class=class] { nav { - h1 { a[href=if session.is_some() {"/home"} else {"/"}] { @if *LOGO_ENABLED { img.logo[src="/assets/logo.svg"]; } else { @CONF.brand } } } " " + h1 { a[href=if session.is_some() {u_home()} else {"/".to_string()}] { @if *LOGO_ENABLED { img.logo[src="/assets/logo.svg"]; } else { @CONF.brand } } } " " @if let Some(_) = session { - a.library[href=uri!(r_library_node("library"))] { @trs(lang, "nav.root") } " " - a.library[href=uri!(r_all_items())] { @trs(lang, "nav.all") } " " - a.library[href=uri!(r_search(None::<&'static str>, None::))] { @trs(lang, "nav.search") } " " - a.library[href=uri!(r_stats())] { @trs(lang, "nav.stats") } " " + a.library[href=u_node_slug("library")] { @trs(lang, "nav.root") } " " + a.library[href=u_items()] { @trs(lang, "nav.all") } " " + a.library[href=u_search()] { @trs(lang, "nav.search") } " " + a.library[href=u_stats()] { @trs(lang, "nav.stats") } " " } @if is_importing() { span.warn { "Library database is updating..." } } div.account { @if let Some(session) = session { span { @raw(tr(*lang, "nav.username").replace("{name}", &format!("{}", escape(&session.user.display_name)))) } " " @if session.user.admin { - a.admin.hybrid_button[href=uri!(r_admin_dashboard())] { p {@trs(lang, "nav.admin")} } " " + a.admin.hybrid_button[href=u_admin_dashboard()] { p {@trs(lang, "nav.admin")} } " " } - a.settings.hybrid_button[href=uri!(r_account_settings())] { p {@trs(lang, "nav.settings")} } " " - a.logout.hybrid_button[href=uri!(r_account_logout())] { p {@trs(lang, "nav.logout")} } + a.settings.hybrid_button[href=u_account_settings()] { p {@trs(lang, "nav.settings")} } " " + a.logout.hybrid_button[href=u_account_logout()] { p {@trs(lang, "nav.logout")} } } else { - a.register.hybrid_button[href=uri!(r_account_register())] { p {@trs(lang, "nav.register")} } " " - a.login.hybrid_button[href=uri!(r_account_login())] { p {@trs(lang, "nav.login")} } + a.register.hybrid_button[href=u_account_register()] { p {@trs(lang, "nav.register")} } " " + a.login.hybrid_button[href=u_account_login()] { p {@trs(lang, "nav.login")} } } } } @@ -61,21 +65,3 @@ markup::define! { } } } - -pub type DynLayoutPage<'a> = LayoutPage>; - -pub struct LayoutPage { - pub title: String, - pub class: Option<&'static str>, - pub content: T, -} - -impl Default for LayoutPage> { - fn default() -> Self { - Self { - class: None, - content: markup::new!(), - title: String::new(), - } - } -} diff --git a/ui/src/settings.rs b/ui/src/settings.rs index 9bc4b1d..fb4ef0f 100644 --- a/ui/src/settings.rs +++ b/ui/src/settings.rs @@ -4,7 +4,10 @@ Copyright (C) 2025 metamuffin */ use crate::locale::{Language, tr, trs}; -use jellycommon::user::{PlayerKind, Theme}; +use jellycommon::{ + routes::{u_account_login, u_account_settings}, + user::{PlayerKind, Theme}, +}; use markup::RenderAttributeValue; markup::define! { @@ -17,42 +20,42 @@ markup::define! { } } h2 { @trs(&lang, "account") } - a.switch_account[href=uri!(r_account_login())] { "Switch Account" } - form[method="POST", action=uri!(r_account_settings_post())] { + a.switch_account[href=u_account_login()] { "Switch Account" } + form[method="POST", action=u_account_settings()] { label[for="username"] { @trs(&lang, "account.username") } input[type="text", id="username", disabled, value=&session.user.name]; input[type="submit", disabled, value=&*tr(**lang, "settings.immutable")]; } - form[method="POST", action=uri!(r_account_settings_post())] { + form[method="POST", action=u_account_settings()] { label[for="display_name"] { @trs(lang, "account.display_name") } input[type="text", id="display_name", name="display_name", value=&session.user.display_name]; input[type="submit", value=&*tr(**lang, "settings.update")]; } - form[method="POST", action=uri!(r_account_settings_post())] { + form[method="POST", action=u_account_settings()] { label[for="password"] { @trs(lang, "account.password") } input[type="password", id="password", name="password"]; input[type="submit", value=&*tr(**lang, "settings.update")]; } h2 { @trs(&lang, "settings.appearance") } - form[method="POST", action=uri!(r_account_settings_post())] { + form[method="POST", action=u_account_settings()] { fieldset { legend { @trs(&lang, "settings.appearance.theme") } - @for (t, tlabel) in Theme::LIST { - label { input[type="radio", name="theme", value=A(*t), checked=session.user.theme==*t]; @tlabel } br; + @for theme in Theme::ALL { + label { input[type="radio", name="theme", value=A(*theme), checked=session.user.theme==*theme]; @trs(lang, &format!("theme.{theme}")) } br; } } input[type="submit", value=&*tr(**lang, "settings.apply")]; } - form[method="POST", action=uri!(r_account_settings_post())] { + form[method="POST", action=u_account_settings()] { fieldset { legend { @trs(&lang, "settings.player_preference") } - @for (t, tlabel) in PlayerKind::LIST { - label { input[type="radio", name="player_preference", value=A(*t), checked=session.user.player_preference==*t]; @tlabel } br; + @for kind in PlayerKind::ALL { + label { input[type="radio", name="player_preference", value=A(*kind), checked=session.user.player_preference==*kind]; @trs(lang, &format!("player_kind.{kind}")) } br; } } input[type="submit", value=&*tr(**lang, "settings.apply")]; } - form[method="POST", action=uri!(r_account_settings_post())] { + form[method="POST", action=u_account_settings()] { label[for="native_secret"] { "Native Secret" } input[type="password", id="native_secret", name="native_secret"]; input[type="submit", value=&*tr(**lang, "settings.update")]; @@ -64,12 +67,12 @@ markup::define! { struct A(pub T); impl markup::Render for A { fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result { - writer.write_fmt(format_args!("{}", self as &dyn UriDisplay)) + writer.write_str(self.0.to_str()) } } impl markup::Render for A { fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result { - writer.write_fmt(format_args!("{}", self as &dyn UriDisplay)) + writer.write_str(self.0.to_str()) } } impl RenderAttributeValue for A {} -- cgit v1.2.3-70-g09d2