aboutsummaryrefslogtreecommitdiff
path: root/ui
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 /ui
parent51761cbdefa39107b9e1f931f1aa8df6aebb2a94 (diff)
downloadjellything-73d2d5eb01fceae9e0b1c58afb648822000c878a.tar
jellything-73d2d5eb01fceae9e0b1c58afb648822000c878a.tar.bz2
jellything-73d2d5eb01fceae9e0b1c58afb648822000c878a.tar.zst
yes
Diffstat (limited to 'ui')
-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
6 files changed, 80 insertions, 72 deletions
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> {}