aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-04-28 21:50:51 +0200
committermetamuffin <metamuffin@disroot.org>2025-04-28 21:50:51 +0200
commit73d2d5eb01fceae9e0b1c58afb648822000c878a (patch)
tree8fd0279949251245e2086ad28e99b114eac1bf14
parent51761cbdefa39107b9e1f931f1aa8df6aebb2a94 (diff)
downloadjellything-73d2d5eb01fceae9e0b1c58afb648822000c878a.tar
jellything-73d2d5eb01fceae9e0b1c58afb648822000c878a.tar.bz2
jellything-73d2d5eb01fceae9e0b1c58afb648822000c878a.tar.zst
yes
-rw-r--r--common/src/api.rs26
-rw-r--r--common/src/impl.rs98
-rw-r--r--common/src/lib.rs170
-rw-r--r--common/src/routes.rs63
-rw-r--r--common/src/user.rs70
-rw-r--r--ui/src/filter_sort.rs6
-rw-r--r--ui/src/lib.rs15
-rw-r--r--ui/src/node_card.rs22
-rw-r--r--ui/src/node_page.rs34
-rw-r--r--ui/src/scaffold.rs44
-rw-r--r--ui/src/settings.rs31
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 <metamuffin.org>
*/
-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<SortProperty>,
pub filter_kind: Option<Vec<FilterProperty>>,
pub sort_order: Option<SortOrder>,
}
-
-pub trait UrlEnum: Sized {
- fn to_str(&self) -> &'static str;
- fn from_str(s: &str) -> Option<Self>;
-}
-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<Self> { 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 <metamuffin.org>
*/
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<Self, Self::Err> {
- 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<Self, Self::Error> {
- if let Some(id) = param.strip_prefix("+") {
+impl FromStr for NodeID {
+ type Err = hex::FromHexError;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ 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<rocket::http::uri::fmt::Path, NodeID> for NodeID {
- type Target = NodeID;
- fn from_uri_param(param: NodeID) -> Self::Target {
- param
- }
-}
-#[cfg(feature = "rocket")]
-impl<'a> rocket::http::uri::fmt::FromUriParam<rocket::http::uri::fmt::Path, &'a String> 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<rocket::http::uri::fmt::Path, &'a str> 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<rocket::http::uri::fmt::Path> 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<Self> { 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, Self::Err> {
+ 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<u64>,
}
-#[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<String>,
}
-#[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 <metamuffin.org>
*/
-
-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 <metamuffin.org>
*/
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<HashSet<UserPermission>>,
}
-#[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<UserPermission, bool>);
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<T>(pub T);
impl markup::Render for A<SortProperty> {
fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
- writer.write_fmt(format_args!("{}", self as &dyn UriDisplay<Query>))
+ writer.write_str(self.0.to_str())
}
}
impl markup::Render for A<SortOrder> {
fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
- writer.write_fmt(format_args!("{}", self as &dyn UriDisplay<Query>))
+ writer.write_str(self.0.to_str())
}
}
impl markup::Render for A<FilterProperty> {
fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
- writer.write_fmt(format_args!("{}", self as &dyn UriDisplay<Query>))
+ writer.write_str(self.0.to_str())
}
}
impl RenderAttributeValue for A<SortOrder> {}
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<bool> = 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::<usize>))] { @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!("<b class=\"username\">{}</b>", 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<DynRender<'a>>;
-
-pub struct LayoutPage<T> {
- pub title: String,
- pub class: Option<&'static str>,
- pub content: T,
-}
-
-impl Default for LayoutPage<DynRender<'_>> {
- 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 <metamuffin.org>
*/
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<T>(pub T);
impl markup::Render for A<Theme> {
fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
- writer.write_fmt(format_args!("{}", self as &dyn UriDisplay<Query>))
+ writer.write_str(self.0.to_str())
}
}
impl markup::Render for A<PlayerKind> {
fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
- writer.write_fmt(format_args!("{}", self as &dyn UriDisplay<Query>))
+ writer.write_str(self.0.to_str())
}
}
impl RenderAttributeValue for A<Theme> {}