diff options
author | metamuffin <metamuffin@disroot.org> | 2025-04-29 11:10:21 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2025-04-29 11:10:21 +0200 |
commit | f62c7f2a8cc143454779dc99334ca9fc80ddabd5 (patch) | |
tree | f31dbb908715d2deb2860e2097fa13dd41d759d5 | |
parent | 73d2d5eb01fceae9e0b1c58afb648822000c878a (diff) | |
download | jellything-f62c7f2a8cc143454779dc99334ca9fc80ddabd5.tar jellything-f62c7f2a8cc143454779dc99334ca9fc80ddabd5.tar.bz2 jellything-f62c7f2a8cc143454779dc99334ca9fc80ddabd5.tar.zst |
still just moving code around
-rw-r--r-- | Cargo.lock | 6 | ||||
-rw-r--r-- | common/src/lib.rs | 4 | ||||
-rw-r--r-- | common/src/routes.rs | 24 | ||||
-rw-r--r-- | server/src/api.rs | 6 | ||||
-rw-r--r-- | server/src/locale.rs | 2 | ||||
-rw-r--r-- | server/src/logic/session.rs | 29 | ||||
-rw-r--r-- | server/src/logic/stream.rs | 2 | ||||
-rw-r--r-- | server/src/logic/userdata.rs | 12 | ||||
-rw-r--r-- | server/src/routes.rs | 8 | ||||
-rw-r--r-- | server/src/ui/admin/user.rs | 63 | ||||
-rw-r--r-- | server/src/ui/assets.rs | 7 | ||||
-rw-r--r-- | server/src/ui/browser.rs | 61 | ||||
-rw-r--r-- | server/src/ui/error.rs | 37 | ||||
-rw-r--r-- | server/src/ui/home.rs | 38 | ||||
-rw-r--r-- | server/src/ui/items.rs | 48 | ||||
-rw-r--r-- | server/src/ui/mod.rs | 89 | ||||
-rw-r--r-- | server/src/ui/node.rs | 106 | ||||
-rw-r--r-- | server/src/ui/player.rs | 102 | ||||
-rw-r--r-- | server/src/ui/search.rs | 47 | ||||
-rw-r--r-- | server/src/ui/stats.rs | 44 | ||||
-rw-r--r-- | ui/Cargo.toml | 2 | ||||
-rw-r--r-- | ui/src/admin/mod.rs | 7 | ||||
-rw-r--r-- | ui/src/admin/user.rs | 55 | ||||
-rw-r--r-- | ui/src/home.rs | 2 | ||||
-rw-r--r-- | ui/src/items.rs | 46 | ||||
-rw-r--r-- | ui/src/lib.rs | 48 | ||||
-rw-r--r-- | ui/src/scaffold.rs | 32 | ||||
-rw-r--r-- | ui/src/settings.rs | 7 | ||||
-rw-r--r-- | ui/src/stats.rs | 9 |
29 files changed, 493 insertions, 450 deletions
@@ -1988,6 +1988,8 @@ dependencies = [ "humansize", "jellycommon", "markup", + "serde", + "serde_json", ] [[package]] @@ -3360,9 +3362,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", diff --git a/common/src/lib.rs b/common/src/lib.rs index 26bf361..8993d22 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -30,7 +30,7 @@ macro_rules! url_enum { 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 } } + pub fn from_str_opt(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 { @@ -40,7 +40,7 @@ macro_rules! url_enum { impl std::str::FromStr for $i { type Err = (); fn from_str(s: &str) -> Result<Self, Self::Err> { - Self::from_str(s).ok_or(()) + Self::from_str_opt(s).ok_or(()) } } }; diff --git a/common/src/routes.rs b/common/src/routes.rs index e510e22..9472c85 100644 --- a/common/src/routes.rs +++ b/common/src/routes.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::ApiWatchedState, NodeID, PeopleGroup}; +use crate::{api::NodeFilterSort, user::ApiWatchedState, NodeID, PeopleGroup}; pub fn u_home() -> String { "/home".to_owned() @@ -46,6 +46,25 @@ pub fn u_node_slug_update_rating(node: &str) -> String { pub fn u_node_slug_progress(node: &str, time: f64) -> String { format!("/n/{node}/progress?t={time}") } +pub fn u_items() -> String { + format!("/items") +} +pub fn u_items_filter(page: usize, _filter: &NodeFilterSort) -> String { + // TODO + format!("/items?page={page}") +} +pub fn u_admin_users() -> String { + format!("/admin/users") +} +pub fn u_admin_user(name: &str) -> String { + format!("/admin/user/{name}") +} +pub fn u_admin_user_permission(name: &str) -> String { + format!("/admin/user/{name}/update_permissions") +} +pub fn u_admin_user_remove(name: &str) -> String { + format!("/admin/user/{name}/remove") +} pub fn u_account_register() -> String { "/account/register".to_owned() } @@ -67,6 +86,3 @@ pub fn u_stats() -> String { pub fn u_search() -> String { "/search".to_owned() } -pub fn u_items() -> String { - "/items".to_owned() -} diff --git a/server/src/api.rs b/server/src/api.rs index f246eab..a9df1bd 100644 --- a/server/src/api.rs +++ b/server/src/api.rs @@ -4,12 +4,10 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::ui::{account::login_logic, error::MyResult}; -use crate::{ - database::Database, - logic::session::{AdminSession, Session}, -}; +use crate::database::Database; use jellybase::assetfed::AssetInner; use jellycommon::{user::CreateSessionParams, NodeID, Visibility}; +use jellylogic::session::{AdminSession, Session}; use rocket::{ get, http::MediaType, diff --git a/server/src/locale.rs b/server/src/locale.rs index 6d16c17..8314306 100644 --- a/server/src/locale.rs +++ b/server/src/locale.rs @@ -1,4 +1,4 @@ -use jellybase::locale::Language; +use jellyui::locale::Language; use rocket::{ outcome::Outcome, request::{self, FromRequest}, diff --git a/server/src/logic/session.rs b/server/src/logic/session.rs index d77c4fc..105aa10 100644 --- a/server/src/logic/session.rs +++ b/server/src/logic/session.rs @@ -4,16 +4,9 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use crate::ui::error::MyError; -use aes_gcm_siv::{ - aead::{generic_array::GenericArray, Aead}, - KeyInit, -}; use anyhow::anyhow; -use base64::Engine; -use chrono::{DateTime, Duration, Utc}; -use jellybase::{database::Database, SECRETS}; -use jellycommon::user::{PermissionSet, User}; -use jellylogic::session::validate; +use jellybase::database::Database; +use jellylogic::session::{validate, AdminSession, Session}; use log::warn; use rocket::{ async_trait, @@ -22,11 +15,11 @@ use rocket::{ request::{self, FromRequest}, Request, State, }; -use serde::{Deserialize, Serialize}; -use std::sync::LazyLock; -impl Session { - pub async fn from_request_ut(req: &Request<'_>) -> Result<Self, MyError> { +pub struct A<T>(pub T); + +impl A<Session> { + async fn from_request_ut(req: &Request<'_>) -> Result<Self, MyError> { let username; #[cfg(not(feature = "bypass-auth"))] @@ -59,7 +52,7 @@ impl Session { let user = db.get_user(&username)?.ok_or(anyhow!("user not found"))?; - Ok(Session { user }) + Ok(A(Session { user })) } } @@ -75,7 +68,7 @@ fn parse_jellyfin_auth(h: &str) -> Option<&str> { } #[async_trait] -impl<'r> FromRequest<'r> for Session { +impl<'r> FromRequest<'r> for A<Session> { type Error = MyError; async fn from_request<'life0>( request: &'r Request<'life0>, @@ -91,15 +84,15 @@ impl<'r> FromRequest<'r> for Session { } #[async_trait] -impl<'r> FromRequest<'r> for AdminSession { +impl<'r> FromRequest<'r> for A<AdminSession> { type Error = MyError; async fn from_request<'life0>( request: &'r Request<'life0>, ) -> request::Outcome<Self, Self::Error> { - match Session::from_request_ut(request).await { + match A::<Session>::from_request_ut(request).await { Ok(x) => { if x.user.admin { - Outcome::Success(AdminSession(x)) + Outcome::Success(A(AdminSession(x.0))) } else { Outcome::Error(( Status::Unauthorized, diff --git a/server/src/logic/stream.rs b/server/src/logic/stream.rs index 5bba9c2..f9cdb41 100644 --- a/server/src/logic/stream.rs +++ b/server/src/logic/stream.rs @@ -3,11 +3,11 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use super::session::Session; use crate::{database::Database, ui::error::MyError}; use anyhow::{anyhow, Result}; use jellybase::{assetfed::AssetInner, federation::Federation}; use jellycommon::{stream::StreamSpec, TrackSource}; +use jellylogic::session::Session; use jellystream::SMediaInfo; use log::{info, warn}; use rocket::{ diff --git a/server/src/logic/userdata.rs b/server/src/logic/userdata.rs index 64a136f..8da6be9 100644 --- a/server/src/logic/userdata.rs +++ b/server/src/logic/userdata.rs @@ -3,19 +3,17 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::{ui::error::MyResult, ui::node::rocket_uri_macro_r_library_node}; +use crate::ui::error::MyResult; use jellybase::database::Database; use jellycommon::{ - user::{NodeUserData, WatchedState}, - NodeID, + routes::u_node_id, user::{NodeUserData, WatchedState}, NodeID }; +use jellylogic::session::Session; use rocket::{ form::Form, get, post, response::Redirect, serde::json::Json, FromForm, FromFormField, State, UriDisplayQuery, }; -use super::session::Session; - #[derive(Debug, FromFormField, UriDisplayQuery)] pub enum UrlWatchedState { None, @@ -51,7 +49,7 @@ pub async fn r_node_userdata_watched( }; Ok(()) })?; - Ok(Redirect::found(rocket::uri!(r_library_node(id)))) + Ok(Redirect::found(u_node_id(id))) } #[derive(FromForm)] @@ -72,7 +70,7 @@ pub async fn r_node_userdata_rating( udata.rating = form.rating; Ok(()) })?; - Ok(Redirect::found(rocket::uri!(r_library_node(id)))) + Ok(Redirect::found(u_node_id(id))) } #[post("/n/<id>/progress?<t>")] diff --git a/server/src/routes.rs b/server/src/routes.rs index 4e452c3..4b52da0 100644 --- a/server/src/routes.rs +++ b/server/src/routes.rs @@ -18,10 +18,10 @@ use crate::ui::{ user::{r_admin_remove_user, r_admin_user, r_admin_user_permission, r_admin_users}, }, assets::{r_asset, r_item_backdrop, r_item_poster, r_node_thumbnail, r_person_asset}, - browser::r_all_items_filter, + items::r_items, error::{r_api_catch, r_catch}, home::r_home, - node::r_library_node_filter, + node::r_node, player::r_player, r_favicon, r_index, search::r_search, @@ -141,7 +141,7 @@ pub fn build_rocket(database: Database, federation: Federation) -> Rocket<Build> r_admin_user_permission, r_admin_user, r_admin_users, - r_all_items_filter, + r_items, r_asset, r_assets_font, r_assets_js_map, @@ -152,7 +152,7 @@ pub fn build_rocket(database: Database, federation: Federation) -> Rocket<Build> r_index, r_item_backdrop, r_item_poster, - r_library_node_filter, + r_node, r_node_thumbnail, r_node_userdata_progress, r_node_userdata_rating, diff --git a/server/src/ui/admin/user.rs b/server/src/ui/admin/user.rs index 818e416..1af83d4 100644 --- a/server/src/ui/admin/user.rs +++ b/server/src/ui/admin/user.rs @@ -3,9 +3,10 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::{database::Database, logic::session::AdminSession, ui::error::MyResult, uri}; +use crate::{database::Database, ui::error::MyResult}; use anyhow::{anyhow, Context}; use jellycommon::user::UserPermission; +use jellylogic::session::AdminSession; use rocket::{form::Form, get, post, FromForm, FromFormField, State}; #[get("/admin/users")] @@ -61,62 +62,13 @@ fn manage_single_user<'a>( Ok(LayoutPage { title: "User management".to_string(), - content: markup::new! { - h1 { @format!("{:?}", user.display_name) " (" @user.name ")" } - a[href=uri!(r_admin_users())] "Back to the User List" - @FlashDisplay { flash: flash.clone() } - form[method="POST", action=uri!(r_admin_remove_user())] { - input[type="text", name="name", value=&user.name, hidden]; - input.danger[type="submit", value="Remove user(!)"]; - } - - h2 { "Permissions" } - @PermissionDisplay { perms: &user.permissions } - - form[method="POST", action=uri!(r_admin_user_permission())] { - input[type="text", name="name", value=&user.name, hidden]; - fieldset.perms { - legend { "Permission" } - @for p in UserPermission::ALL_ENUMERABLE { - label { - input[type="radio", name="permission", value=serde_json::to_string(p).unwrap()]; - @format!("{p}") - } br; - } - } - fieldset.perms { - legend { "Permission" } - label { input[type="radio", name="action", value="unset"]; "Unset" } br; - label { input[type="radio", name="action", value="grant"]; "Grant" } br; - label { input[type="radio", name="action", value="revoke"]; "Revoke" } br; - } - input[type="submit", value="Update"]; - } - - }, + content: markup::new! {}, ..Default::default() }) } -markup::define! { - PermissionDisplay<'a>(perms: &'a PermissionSet) { - ul { @for (perm,grant) in &perms.0 { - @if *grant { - li[class="perm-grant"] { @format!("Allow {}", perm) } - } else { - li[class="perm-revoke"] { @format!("Deny {}", perm) } - } - }} - } -} - -#[derive(FromForm)] -pub struct DeleteUser { - name: String, -} #[derive(FromForm)] pub struct UserPermissionForm { - name: String, permission: String, action: GrantState, } @@ -128,11 +80,12 @@ pub enum GrantState { Unset, } -#[post("/admin/update_user_permission", data = "<form>")] +#[post("/admin/user/<name>/update_permission", data = "<form>")] pub fn r_admin_user_permission( session: AdminSession, database: &State<Database>, form: Form<UserPermissionForm>, + name: &str, ) -> MyResult<DynLayoutPage<'static>> { drop(session); let perm = serde_json::from_str::<UserPermission>(&form.permission) @@ -154,14 +107,14 @@ pub fn r_admin_user_permission( ) } -#[post("/admin/remove_user", data = "<form>")] +#[post("/admin/<name>/remove")] pub fn r_admin_remove_user( session: AdminSession, database: &State<Database>, - form: Form<DeleteUser>, + name: &str, ) -> MyResult<DynLayoutPage<'static>> { drop(session); - if !database.delete_user(&form.name)? { + if !database.delete_user(&name)? { Err(anyhow!("user did not exist"))?; } user_management(database, Some(Ok("User removed".into()))) diff --git a/server/src/ui/assets.rs b/server/src/ui/assets.rs index 63d8525..ecab3d3 100644 --- a/server/src/ui/assets.rs +++ b/server/src/ui/assets.rs @@ -4,15 +4,16 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ use super::error::MyResult; -use crate::{helper::cache::CacheControlFile, logic::session::Session}; +use crate::helper::cache::CacheControlFile; use anyhow::{anyhow, bail, Context}; use base64::Engine; use jellybase::{assetfed::AssetInner, database::Database, federation::Federation, CONF}; use jellycache::async_cache_file; use jellycommon::{LocalTrack, NodeID, PeopleGroup, SourceTrackKind, TrackSource}; +use jellylogic::session::Session; use log::info; use rocket::{get, http::ContentType, response::Redirect, State}; -use std::{path::PathBuf, str::FromStr}; +use std::path::PathBuf; pub const AVIF_QUALITY: f32 = 50.; pub const AVIF_SPEED: u8 = 5; @@ -120,7 +121,7 @@ pub async fn r_person_asset( let node = db.get_node(id)?.ok_or(anyhow!("node does not exist"))?; let app = node .people - .get(&PeopleGroup::from_str(&group).map_err(|()| anyhow!("unknown people group"))?) + .get(&PeopleGroup::from_str_opt(&group).ok_or(anyhow!("unknown people group"))?) .ok_or(anyhow!("group has no members"))? .get(index) .ok_or(anyhow!("person does not exist"))?; diff --git a/server/src/ui/browser.rs b/server/src/ui/browser.rs deleted file mode 100644 index b780934..0000000 --- a/server/src/ui/browser.rs +++ /dev/null @@ -1,61 +0,0 @@ -/* - 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) 2025 metamuffin <metamuffin.org> -*/ -use super::{ - error::MyError, - layout::{trs, DynLayoutPage, LayoutPage}, - node::NodeCard, - sort::{filter_and_sort_nodes, NodeFilterSort, NodeFilterSortForm, SortOrder, SortProperty}, -}; -use crate::{ - api::AcceptJson, database::Database, locale::AcceptLanguage, logic::session::Session, uri, -}; -use jellybase::locale::tr; -use jellycommon::{api::ApiItemsResponse, Visibility}; -use rocket::{get, serde::json::Json, Either, State}; - -/// This function is a stub and only useful for use in the uri! macro. -#[get("/items")] -pub fn r_all_items() {} - -#[get("/items?<page>&<filter..>")] -pub fn r_all_items_filter( - sess: Session, - db: &State<Database>, - aj: AcceptJson, - page: Option<usize>, - filter: NodeFilterSort, - lang: AcceptLanguage, -) -> Result<Either<DynLayoutPage<'_>, Json<ApiItemsResponse>>, MyError> { - let AcceptLanguage(lang) = lang; - - let data = all_items()?; - Ok(if *aj { - Either::Right(Json(data)) - } else { - Either::Left(LayoutPage { - title: "All Items".to_owned(), - content: markup::new! { - .page.dir { - h1 { "All Items" } - @NodeFilterSortForm { f: &filter, lang: &lang } - ul.children { @for (node, udata) in &items[from..to] { - li {@NodeCard { node, udata, lang: &lang }} - }} - p.pagecontrols { - span.current { @tr(lang, "page.curr").replace("{cur}", &(page + 1).to_string()).replace("{max}", &max_page.to_string()) " " } - @if page > 0 { - a.prev[href=uri!(r_all_items_filter(Some(page - 1), filter.clone()))] { @trs(&lang, "page.prev") } " " - } - @if page + 1 < max_page { - a.next[href=uri!(r_all_items_filter(Some(page + 1), filter.clone()))] { @trs(&lang, "page.next") } - } - } - } - }, - ..Default::default() - }) - }) -} diff --git a/server/src/ui/error.rs b/server/src/ui/error.rs index 6ba2ba9..0ea1a8d 100644 --- a/server/src/ui/error.rs +++ b/server/src/ui/error.rs @@ -8,7 +8,7 @@ use log::info; use rocket::{ catch, http::{ContentType, Status}, - response::{self, Responder}, + response::{self, content::RawHtml, Responder}, Request, }; use serde_json::{json, Value}; @@ -24,18 +24,19 @@ static ERROR_IMAGE: LazyLock<Vec<u8>> = LazyLock::new(|| { }); #[catch(default)] -pub fn r_catch<'a>(status: Status, _request: &Request) -> DynLayoutPage<'a> { - LayoutPage { - title: "Not found".to_string(), - content: markup::new! { - h2 { "Error" } - p { @format!("{status}") } - @if status == Status::NotFound { - p { "You might need to " a[href=uri!(r_account_login())] { "log in" } ", to see this page" } - } - }, - ..Default::default() - } +pub fn r_catch<'a>(status: Status, _request: &Request) -> RawHtml<String> { + // LayoutPage { + // title: "Not found".to_string(), + // content: markup::new! { + // h2 { "Error" } + // p { @format!("{status}") } + // @if status == Status::NotFound { + // p { "You might need to " a[href=uri!(r_account_login())] { "log in" } ", to see this page" } + // } + // }, + // ..Default::default() + // } + RawHtml("as".to_string()) } #[catch(default)] @@ -56,15 +57,7 @@ impl<'r> Responder<'r, 'static> for MyError { Some(x) if x.is_avif() || x.is_png() || x.is_jpeg() => { (ContentType::AVIF, ERROR_IMAGE.as_slice()).respond_to(req) } - _ => LayoutPage { - title: "Error".to_string(), - content: markup::new! { - h2 { "An error occured. Nobody is sorry"} - pre.error { @format!("{:?}", self.0) } - }, - ..Default::default() - } - .respond_to(req), + _ => r_catch(Status::InternalServerError, req).respond_to(req), } } } diff --git a/server/src/ui/home.rs b/server/src/ui/home.rs index 2a79965..6127e8c 100644 --- a/server/src/ui/home.rs +++ b/server/src/ui/home.rs @@ -3,27 +3,43 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use super::{error::MyResult, node::DatabaseNodeUserDataExt}; -use crate::{api::AcceptJson, database::Database, locale::AcceptLanguage, logic::session::Session}; -use anyhow::Context; -use chrono::{Datelike, Utc}; -use jellycommon::{api::ApiHomeResponse, user::WatchedState, NodeID, NodeKind, Rating, Visibility}; -use rocket::{get, serde::json::Json, Either, State}; + +use super::error::MyResult; +use crate::{api::AcceptJson, locale::AcceptLanguage}; +use jellybase::database::Database; +use jellycommon::api::ApiHomeResponse; +use jellyimport::is_importing; +use jellylogic::session::Session; +use jellyui::{ + home::HomePage, + render_page, + scaffold::{RenderInfo, SessionInfo}, +}; +use rocket::{ + figment::value::magic::Either, get, response::content::RawHtml, serde::json::Json, State, +}; #[get("/home")] pub fn r_home( - sess: Session, + session: Session, db: &State<Database>, aj: AcceptJson, lang: AcceptLanguage, -) -> MyResult<Either<DynLayoutPage, Json<ApiHomeResponse>>> { +) -> MyResult<Either<RawHtml<String>, Json<ApiHomeResponse>>> { let AcceptLanguage(lang) = lang; - let resp = jellylogic::home::home(&db, sess)?; + let r = jellylogic::home::home(&db, &session)?; Ok(if *aj { - Either::Right(Json(resp)) + Either::Right(Json(r)) } else { - Either::Left(jellyui::home::home_page(resp)) + Either::Left(RawHtml(render_page( + &HomePage { lang: &lang, r }, + RenderInfo { + importing: is_importing(), + session: Some(SessionInfo { user: session.user }), + }, + lang, + ))) }) } diff --git a/server/src/ui/items.rs b/server/src/ui/items.rs new file mode 100644 index 0000000..c7d062d --- /dev/null +++ b/server/src/ui/items.rs @@ -0,0 +1,48 @@ +/* + 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) 2025 metamuffin <metamuffin.org> +*/ +use super::error::MyError; +use crate::{api::AcceptJson, database::Database, locale::AcceptLanguage}; +use jellycommon::api::{ApiItemsResponse, NodeFilterSort}; +use jellyimport::is_importing; +use jellylogic::{items::all_items, session::Session}; +use jellyui::{ + items::ItemsPage, + render_page, + scaffold::{RenderInfo, SessionInfo}, +}; +use rocket::{get, response::content::RawHtml, serde::json::Json, Either, State}; + +#[get("/items?<page>&<filter..>")] +pub fn r_items( + session: Session, + db: &State<Database>, + aj: AcceptJson, + page: Option<usize>, + filter: NodeFilterSort, + lang: AcceptLanguage, +) -> Result<Either<RawHtml<String>, Json<ApiItemsResponse>>, MyError> { + let AcceptLanguage(lang) = lang; + + let r = all_items(db, &session, page, filter.clone())?; + + Ok(if *aj { + Either::Right(Json(r)) + } else { + Either::Left(RawHtml(render_page( + &ItemsPage { + lang: &lang, + r, + filter: &filter, + page: page.unwrap_or(0), + }, + RenderInfo { + importing: is_importing(), + session: Some(SessionInfo { user: session.user }), + }, + lang, + ))) + }) +} diff --git a/server/src/ui/mod.rs b/server/src/ui/mod.rs index 6728b81..f59118e 100644 --- a/server/src/ui/mod.rs +++ b/server/src/ui/mod.rs @@ -3,27 +3,19 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::logic::session::Session; +use crate::locale::AcceptLanguage; use error::MyResult; use home::rocket_uri_macro_r_home; use jellybase::CONF; -use log::debug; +use jellylogic::session::Session; +use jellyui::{render_page, scaffold::RenderInfo, CustomPage}; use rocket::{ futures::FutureExt, get, - http::{ContentType, Header, Status}, - response::{self, Redirect, Responder}, - Either, Request, Response, -}; -use std::{ - collections::hash_map::DefaultHasher, - future::Future, - hash::{Hash, Hasher}, - io::Cursor, - os::unix::prelude::MetadataExt, - path::Path, - pin::Pin, + response::{content::RawHtml, Redirect}, + Either, }; +use std::{future::Future, pin::Pin}; use tokio::{ fs::{read_to_string, File}, io::AsyncRead, @@ -32,60 +24,36 @@ use tokio::{ pub mod account; pub mod admin; pub mod assets; -pub mod browser; pub mod error; pub mod home; +pub mod items; pub mod node; pub mod player; pub mod search; pub mod stats; pub mod style; -impl<'r, Main: Render> Responder<'r, 'static> for LayoutPage<Main> { - fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { - // TODO blocking the event loop here. it seems like there is no other way to - // TODO offload this, since the guard references `req` which has a lifetime. - // TODO therefore we just block. that is fine since the database is somewhat fast. - let lang = lang_from_request(&req); - let session = block_on(req.guard::<Option<Session>>()).unwrap(); - let mut out = String::new(); - Scaffold { - main: self.content, - title: self.title, - class: &format!( - "{} theme-{:?}", - self.class.unwrap_or(""), - session - .as_ref() - .map(|s| s.user.theme) - .unwrap_or(Theme::Dark) - ), - session, - lang, - } - .render(&mut out) - .unwrap(); - - Response::build() - .header(ContentType::HTML) - .streamed_body(Cursor::new(out)) - .ok() - } -} - #[get("/")] -pub async fn r_index(sess: Option<Session>) -> MyResult<Either<Redirect, DynLayoutPage<'static>>> { +pub async fn r_index( + lang: AcceptLanguage, + sess: Option<Session>, +) -> MyResult<Either<Redirect, RawHtml<String>>> { + let AcceptLanguage(lang) = lang; if sess.is_some() { Ok(Either::Left(Redirect::temporary(rocket::uri!(r_home())))) } else { let front = read_to_string(CONF.asset_path.join("front.htm")).await?; - Ok(Either::Right(LayoutPage { - title: "Home".to_string(), - content: markup::new! { - @markup::raw(&front) + Ok(Either::Right(RawHtml(render_page( + &CustomPage { + title: "Jellything".to_string(), + body: front, }, - ..Default::default() - })) + RenderInfo { + importing: false, + session: None, + }, + lang, + )))) } } @@ -94,19 +62,6 @@ pub async fn r_favicon() -> MyResult<File> { Ok(File::open(CONF.asset_path.join("favicon.ico")).await?) } -pub struct HtmlTemplate<'a>(pub markup::DynRender<'a>); - -impl<'r> Responder<'r, 'static> for HtmlTemplate<'_> { - fn respond_to(self, _req: &'r Request<'_>) -> response::Result<'static> { - let mut out = String::new(); - self.0.render(&mut out).unwrap(); - Response::build() - .header(ContentType::HTML) - .sized_body(out.len(), Cursor::new(out)) - .ok() - } -} - pub struct Defer(Pin<Box<dyn Future<Output = String> + Send>>); impl AsyncRead for Defer { diff --git a/server/src/ui/node.rs b/server/src/ui/node.rs index 5d0f1ff..1a0ff16 100644 --- a/server/src/ui/node.rs +++ b/server/src/ui/node.rs @@ -3,25 +3,23 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use super::{error::MyResult, sort::filter_and_sort_nodes}; -use crate::{api::AcceptJson, database::Database, locale::AcceptLanguage, logic::session::Session}; -use anyhow::{anyhow, Result}; +use super::error::MyResult; +use crate::{api::AcceptJson, database::Database, locale::AcceptLanguage}; use jellycommon::{ - api::{ApiNodeResponse, NodeFilterSort, SortOrder, SortProperty}, - user::NodeUserData, - Node, NodeID, NodeKind, Visibility, + api::{ApiNodeResponse, NodeFilterSort}, + NodeID, }; -use rocket::{get, serde::json::Json, Either, State}; -use std::{cmp::Reverse, collections::BTreeMap, sync::Arc}; - -/// This function is a stub and only useful for use in the uri! macro. -#[get("/n/<id>")] -pub fn r_library_node(id: NodeID) { - let _ = id; -} +use jellyimport::is_importing; +use jellylogic::{node::get_node, session::Session}; +use jellyui::{ + node_page::NodePage, + render_page, + scaffold::{RenderInfo, SessionInfo}, +}; +use rocket::{get, response::content::RawHtml, serde::json::Json, Either, State}; #[get("/n/<id>?<parents>&<children>&<filter..>")] -pub async fn r_library_node_filter<'a>( +pub async fn r_node<'a>( session: Session, id: NodeID, db: &'a State<Database>, @@ -30,65 +28,37 @@ pub async fn r_library_node_filter<'a>( lang: AcceptLanguage, parents: bool, children: bool, -) -> MyResult<Either<DynLayoutPage<'a>, Json<ApiNodeResponse>>> { +) -> MyResult<Either<RawHtml<String>, Json<ApiNodeResponse>>> { let AcceptLanguage(lang) = lang; - let (node, udata) = db.get_node_with_userdata(id, &session)?; - let mut children = if !*aj || children { - db.get_node_children(id)? - .into_iter() - .map(|c| db.get_node_with_userdata(c, &session)) - .collect::<anyhow::Result<Vec<_>>>()? - } else { - Vec::new() - }; - - let mut parents = if !*aj || parents { - node.parents - .iter() - .map(|pid| db.get_node_with_userdata(*pid, &session)) - .collect::<anyhow::Result<Vec<_>>>()? - } else { - Vec::new() - }; - - let mut similar = get_similar_media(&node, db, &session)?; - - similar.retain(|(n, _)| n.visibility >= Visibility::Reduced); - children.retain(|(n, _)| n.visibility >= Visibility::Reduced); - parents.retain(|(n, _)| n.visibility >= Visibility::Reduced); - - filter_and_sort_nodes( - &filter, - match node.kind { - NodeKind::Channel => (SortProperty::ReleaseDate, SortOrder::Descending), - NodeKind::Season | NodeKind::Show => (SortProperty::Index, SortOrder::Ascending), - _ => (SortProperty::Title, SortOrder::Ascending), - }, - &mut children, - ); + let r = get_node( + &db, + id, + &session, + !*aj || children, + !*aj || parents, + filter.clone(), + )?; Ok(if *aj { - Either::Right(Json(ApiNodeResponse { - children, - parents, - node, - userdata: udata, - })) + Either::Right(Json(r)) } else { - Either::Left(LayoutPage { - title: node.title.clone().unwrap_or_default(), - content: markup::new!(@NodePage { - node: &node, - udata: &udata, - children: &children, - parents: &parents, + Either::Left(RawHtml(render_page( + &NodePage { + node: &r.node, + udata: &r.userdata, + children: &r.children, + parents: &r.parents, + similar: &[], filter: &filter, - player: false, - similar: &similar, lang: &lang, - }), - ..Default::default() - }) + player: false, + }, + RenderInfo { + importing: is_importing(), + session: Some(SessionInfo { user: session.user }), + }, + lang, + ))) }) } diff --git a/server/src/ui/player.rs b/server/src/ui/player.rs index db2f665..573530b 100644 --- a/server/src/ui/player.rs +++ b/server/src/ui/player.rs @@ -3,38 +3,27 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use super::sort::NodeFilterSort; -use crate::{ - database::Database, - locale::AcceptLanguage, - logic::session::{self, Session}, -}; +use super::error::MyResult; +use crate::{database::Database, locale::AcceptLanguage}; use jellybase::CONF; use jellycommon::{ + api::NodeFilterSort, stream::{StreamContainer, StreamSpec}, user::{PermissionSet, PlayerKind}, - NodeID, TrackID, Visibility, + NodeID, +}; +use jellyimport::is_importing; +use jellylogic::{node::get_node, session::Session}; +use jellyui::{ + node_page::NodePage, + render_page, + scaffold::{RenderInfo, SessionInfo}, +}; +use rocket::{ + get, + response::{content::RawHtml, Redirect}, + Either, State, }; -use rocket::{get, response::Redirect, Either, FromForm, State, UriDisplayQuery}; -use std::sync::Arc; - -#[derive(FromForm, Default, Clone, Debug, UriDisplayQuery)] -pub struct PlayerConfig { - pub a: Option<TrackID>, - pub v: Option<TrackID>, - pub s: Option<TrackID>, - pub t: Option<f64>, - pub kind: Option<PlayerKind>, -} - -impl PlayerConfig { - pub fn seek(t: f64) -> Self { - Self { - t: Some(t), - ..Default::default() - } - } -} fn jellynative_url(action: &str, seek: f64, secret: &str, node: &str, session: &str) -> String { let protocol = if CONF.tls { "https" } else { "http" }; @@ -50,35 +39,25 @@ fn jellynative_url(action: &str, seek: f64, secret: &str, node: &str, session: & format!("jellynative://{action}/{secret}/{session}/{seek}/{protocol}://{host}{stream_url}",) } -#[get("/n/<id>/player?<conf..>", rank = 4)] +#[get("/n/<id>/player?<t>", rank = 4)] pub fn r_player( session: Session, lang: AcceptLanguage, db: &State<Database>, + t: Option<f64>, id: NodeID, - conf: PlayerConfig, -) -> MyResult<Either<DynLayoutPage<'_>, Redirect>> { +) -> MyResult<Either<RawHtml<String>, Redirect>> { let AcceptLanguage(lang) = lang; - let (node, udata) = db.get_node_with_userdata(id, &session)?; - let mut parents = node - .parents - .iter() - .map(|pid| db.get_node_with_userdata(*pid, &session)) - .collect::<anyhow::Result<Vec<_>>>()?; - - let mut similar = get_similar_media(&node, db, &session)?; - - similar.retain(|(n, _)| n.visibility >= Visibility::Reduced); - parents.retain(|(n, _)| n.visibility >= Visibility::Reduced); + let r = get_node(&db, id, &session, false, true, NodeFilterSort::default())?; let native_session = |action: &str| { Ok(Either::Right(Redirect::temporary(jellynative_url( action, - conf.t.unwrap_or(0.), + t.unwrap_or(0.), &session.user.native_secret, &id.to_string(), - &session::create( + &jellylogic::session::create( session.user.name, PermissionSet::default(), // TODO chrono::Duration::hours(24), @@ -86,7 +65,7 @@ pub fn r_player( )))) }; - match conf.kind.unwrap_or(session.user.player_preference) { + match session.user.player_preference { PlayerKind::Browser => (), PlayerKind::Native => { return native_session("player-v2"); @@ -111,26 +90,23 @@ pub fn r_player( // let playing = false; // !spec.track.is_empty(); // let conf = player_conf(node.clone(), playing)?; - Ok(Either::Left(LayoutPage { - title: node.title.to_owned().unwrap_or_default(), - class: Some("player"), - content: markup::new! { - // @if playing { - // // video[src=uri!(r_stream(&node.slug, &spec)), controls, preload="auto"]{} - // } - // @conf - @NodePage { - children: &[], - parents: &parents, - filter: &NodeFilterSort::default(), - node: &node, - udata: &udata, - player: true, - similar: &similar, - lang: &lang - } + Ok(Either::Left(RawHtml(render_page( + &NodePage { + node: &r.node, + udata: &r.userdata, + children: &r.children, + parents: &r.parents, + similar: &[], + filter: &NodeFilterSort::default(), + lang: &lang, + player: true, + }, + RenderInfo { + importing: is_importing(), + session: Some(SessionInfo { user: session.user }), }, - })) + lang, + )))) } // pub fn player_conf<'a>(item: Arc<Node>, playing: bool) -> anyhow::Result<DynRender<'a>> { diff --git a/server/src/ui/search.rs b/server/src/ui/search.rs index 51fdcb8..bacaaee 100644 --- a/server/src/ui/search.rs +++ b/server/src/ui/search.rs @@ -3,17 +3,19 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use super::{ - error::MyResult, - layout::{trs, DynLayoutPage, LayoutPage}, - node::{DatabaseNodeUserDataExt, NodeCard}, -}; -use crate::{api::AcceptJson, locale::AcceptLanguage, logic::session::Session}; +use super::error::MyResult; +use crate::{api::AcceptJson, locale::AcceptLanguage}; use anyhow::anyhow; -use jellybase::{database::Database, locale::tr}; -use jellycommon::{api::ApiSearchResponse, Visibility}; -use rocket::{get, serde::json::Json, Either, State}; -use std::time::Instant; +use jellybase::database::Database; +use jellycommon::api::ApiSearchResponse; +use jellyimport::is_importing; +use jellylogic::{search::search, session::Session}; +use jellyui::{ + render_page, + scaffold::{RenderInfo, SessionInfo}, + search::SearchPage, +}; +use rocket::{get, response::content::RawHtml, serde::json::Json, Either, State}; #[get("/search?<query>&<page>")] pub async fn r_search<'a>( @@ -23,15 +25,30 @@ pub async fn r_search<'a>( query: Option<&str>, page: Option<usize>, lang: AcceptLanguage, -) -> MyResult<Either<DynLayoutPage<'a>, Json<ApiSearchResponse>>> { +) -> MyResult<Either<RawHtml<String>, Json<ApiSearchResponse>>> { let AcceptLanguage(lang) = lang; - + + let r = query + .map(|query| search(db, &session, query, page)) + .transpose()?; + Ok(if *aj { - let Some((count, results, _)) = results else { + let Some(r) = r else { Err(anyhow!("no query"))? }; - Either::Right(Json(ApiSearchResponse { count, results })) + Either::Right(Json(r)) } else { - Either::Left() + Either::Left(RawHtml(render_page( + &SearchPage { + lang: &lang, + query: &query.map(|s| s.to_string()), + r, + }, + RenderInfo { + importing: is_importing(), + session: Some(SessionInfo { user: session.user }), + }, + lang, + ))) }) } diff --git a/server/src/ui/stats.rs b/server/src/ui/stats.rs index a91e670..8bfecbf 100644 --- a/server/src/ui/stats.rs +++ b/server/src/ui/stats.rs @@ -3,38 +3,38 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use super::{ - error::MyError, - layout::{DynLayoutPage, LayoutPage}, +use super::error::MyError; +use crate::{api::AcceptJson, database::Database, locale::AcceptLanguage}; +use jellycommon::api::ApiStatsResponse; +use jellyimport::is_importing; +use jellylogic::{session::Session, stats::stats}; +use jellyui::{ + render_page, + scaffold::{RenderInfo, SessionInfo}, + stats::StatsPage, }; -use crate::{ - api::AcceptJson, database::Database, locale::AcceptLanguage, logic::session::Session, uri, -}; -use jellybase::locale::tr; -use jellycommon::{ - api::{ApiStatsResponse, StatsBin}, - Node, NodeID, NodeKind, Visibility, -}; -use jellylogic::stats::stats; -use markup::raw; -use rocket::{get, serde::json::Json, Either, State}; -use serde::Serialize; -use serde_json::{json, Value}; -use std::collections::BTreeMap; +use rocket::{get, response::content::RawHtml, serde::json::Json, Either, State}; #[get("/stats")] pub fn r_stats( - sess: Session, + session: Session, db: &State<Database>, aj: AcceptJson, lang: AcceptLanguage, -) -> Result<Either<DynLayoutPage<'_>, Json<ApiStatsResponse>>, MyError> { +) -> Result<Either<RawHtml<String>, Json<ApiStatsResponse>>, MyError> { let AcceptLanguage(lang) = lang; - let data = stats(db)?; + let r = stats(db, &session)?; Ok(if *aj { - Either::Right(Json(data)) + Either::Right(Json(r)) } else { - Either::Left(1) + Either::Left(RawHtml(render_page( + &StatsPage { lang: &lang, r }, + RenderInfo { + importing: is_importing(), + session: Some(SessionInfo { user: session.user }), + }, + lang, + ))) }) } diff --git a/ui/Cargo.toml b/ui/Cargo.toml index 86f336c..3868b1f 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -7,3 +7,5 @@ edition = "2024" markup = "0.15.0" jellycommon = { path = "../common" } humansize = "2.1.3" +serde = { version = "1.0.217", features = ["derive", "rc"] } +serde_json = "1.0.140" diff --git a/ui/src/admin/mod.rs b/ui/src/admin/mod.rs new file mode 100644 index 0000000..292e445 --- /dev/null +++ b/ui/src/admin/mod.rs @@ -0,0 +1,7 @@ +/* + 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) 2025 metamuffin <metamuffin.org> +*/ + +pub mod user; diff --git a/ui/src/admin/user.rs b/ui/src/admin/user.rs new file mode 100644 index 0000000..9878803 --- /dev/null +++ b/ui/src/admin/user.rs @@ -0,0 +1,55 @@ +/* + 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) 2025 metamuffin <metamuffin.org> +*/ + +use crate::{locale::Language, scaffold::FlashDisplay}; +use jellycommon::{ + routes::{u_admin_user_permission, u_admin_user_remove, u_admin_users}, + user::{PermissionSet, User, UserPermission}, +}; + +markup::define! { + AdminUserPage<'a>(lang: &'a Language, user: &'a User, flash: Option<Result<String, String>>) { + h1 { @format!("{:?}", user.display_name) " (" @user.name ")" } + a[href=u_admin_users()] "Back to the User List" + @FlashDisplay { flash: flash.clone() } + form[method="POST", action=u_admin_user_remove(&user.name)] { + // input[type="text", name="name", value=&user.name, hidden]; + input.danger[type="submit", value="Remove user(!)"]; + } + + h2 { "Permissions" } + @PermissionDisplay { perms: &user.permissions } + + form[method="POST", action=u_admin_user_permission(&user.name)] { + // input[type="text", name="name", value=&user.name, hidden]; + fieldset.perms { + legend { "Permission" } + @for p in UserPermission::ALL_ENUMERABLE { + label { + input[type="radio", name="permission", value=serde_json::to_string(p).unwrap()]; + @format!("{p}") + } br; + } + } + fieldset.perms { + legend { "Permission" } + label { input[type="radio", name="action", value="unset"]; "Unset" } br; + label { input[type="radio", name="action", value="grant"]; "Grant" } br; + label { input[type="radio", name="action", value="revoke"]; "Revoke" } br; + } + input[type="submit", value="Update"]; + } + } + PermissionDisplay<'a>(perms: &'a PermissionSet) { + ul { @for (perm,grant) in &perms.0 { + @if *grant { + li[class="perm-grant"] { @format!("Allow {}", perm) } + } else { + li[class="perm-revoke"] { @format!("Deny {}", perm) } + } + }} + } +} diff --git a/ui/src/home.rs b/ui/src/home.rs index ec0c634..53055e8 100644 --- a/ui/src/home.rs +++ b/ui/src/home.rs @@ -12,7 +12,7 @@ use jellycommon::api::ApiHomeResponse; use markup::DynRender; markup::define! { - HomePage<'a>(lang: &'a Language, r: &'a ApiHomeResponse) { + HomePage<'a>(lang: &'a Language, r: ApiHomeResponse) { h2 { @trs(lang, "home.bin.root") } //.replace("{title}", &CONF.brand) } ul.children.hlist {@for (node, udata) in &r.toplevel { li { @NodeCard { node, udata, lang: &lang } } diff --git a/ui/src/items.rs b/ui/src/items.rs new file mode 100644 index 0000000..29bc78c --- /dev/null +++ b/ui/src/items.rs @@ -0,0 +1,46 @@ +/* + 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) 2025 metamuffin <metamuffin.org> +*/ +use crate::{ + Page, + filter_sort::NodeFilterSortForm, + locale::{Language, tr, trs}, + node_card::NodeCard, +}; +use jellycommon::{ + api::{ApiItemsResponse, NodeFilterSort}, + routes::u_items_filter, +}; +use markup::DynRender; + +markup::define! { + ItemsPage<'a>(lang: &'a Language, r: ApiItemsResponse, filter: &'a NodeFilterSort, page: usize) { + .page.dir { + h1 { "All Items" } + @NodeFilterSortForm { f: &filter, lang: &lang } + ul.children { @for (node, udata) in &r.items { + li {@NodeCard { node, udata, lang: &lang }} + }} + p.pagecontrols { + span.current { @tr(**lang, "page.curr").replace("{cur}", &(page + 1).to_string()).replace("{max}", &r.pages.to_string()) " " } + @if *page > 0 { + a.prev[href=u_items_filter(page - 1, filter)] { @trs(&lang, "page.prev") } " " + } + @if page + 1 < r.pages { + a.next[href=u_items_filter(page + 1, filter)] { @trs(&lang, "page.next") } + } + } + } + } +} + +impl Page for ItemsPage<'_> { + fn title(&self) -> String { + tr(*self.lang, "home").to_string() + } + fn to_render(&self) -> DynRender { + markup::new!(@self) + } +} diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 67dc067..2521054 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -14,6 +14,34 @@ pub mod scaffold; pub mod search; pub mod settings; pub mod stats; +pub mod items; +pub mod admin; + +use locale::Language; +use markup::DynRender; +use scaffold::{RenderInfo, Scaffold}; +use serde::{Deserialize, Serialize}; +use std::{ + path::PathBuf, + sync::{LazyLock, Mutex}, +}; + +#[rustfmt::skip] +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct Config { + brand: String, + slogan: String, + asset_path: PathBuf, +} + +static CONF: LazyLock<Config> = LazyLock::new(|| { + CONF_PRELOAD + .lock() + .unwrap() + .take() + .expect("cache config not preloaded. logic error") +}); +static CONF_PRELOAD: Mutex<Option<Config>> = Mutex::new(None); /// render as supertrait would be possible but is not /// dyn compatible and I really dont want to expose generics @@ -26,16 +54,26 @@ pub trait Page { } } -use markup::DynRender; -use scaffold::Scaffold; - -pub fn render_page(page: &dyn Page) -> String { +pub fn render_page(page: &dyn Page, renderinfo: RenderInfo, lang: Language) -> String { Scaffold { lang, - context, + renderinfo, class: page.class().unwrap_or("aaaa"), title: page.title(), main: page.to_render(), } .to_string() } + +pub struct CustomPage { + pub title: String, + pub body: String, +} +impl Page for CustomPage { + fn title(&self) -> String { + self.title.clone() + } + fn to_render(&self) -> DynRender { + markup::new!(@markup::raw(&self.body)) + } +} diff --git a/ui/src/scaffold.rs b/ui/src/scaffold.rs index bcff54c..461a9f1 100644 --- a/ui/src/scaffold.rs +++ b/ui/src/scaffold.rs @@ -4,18 +4,32 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::locale::{Language, escape, tr, trs}; -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 crate::{ + CONF, + locale::{Language, escape, tr, trs}, +}; +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, + }, + user::User, }; use markup::{Render, raw}; use std::sync::LazyLock; static LOGO_ENABLED: LazyLock<bool> = LazyLock::new(|| CONF.asset_path.join("logo.svg").exists()); +pub struct RenderInfo { + pub session: Option<SessionInfo>, + pub importing: bool, +} +pub struct SessionInfo { + pub user: User, +} + markup::define! { - Scaffold<'a, Main: Render>(title: String, main: Main, class: &'a str, session: Option<Session>, lang: Language) { + Scaffold<'a, Main: Render>(title: String, main: Main, class: &'a str, renderinfo: RenderInfo, lang: Language) { @markup::doctype() html { head { @@ -26,16 +40,16 @@ markup::define! { } body[class=class] { nav { - 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 { + h1 { a[href=if renderinfo.session.is_some() {u_home()} else {"/".to_string()}] { @if *LOGO_ENABLED { img.logo[src="/assets/logo.svg"]; } else { @CONF.brand } } } " " + @if let Some(_) = &renderinfo.session { 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 renderinfo.importing { span.warn { "Library database is updating..." } } } - @if is_importing() { span.warn { "Library database is updating..." } } div.account { - @if let Some(session) = session { + @if let Some(session) = &renderinfo.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=u_admin_dashboard()] { p {@trs(lang, "nav.admin")} } " " diff --git a/ui/src/settings.rs b/ui/src/settings.rs index fb4ef0f..5ff3946 100644 --- a/ui/src/settings.rs +++ b/ui/src/settings.rs @@ -3,7 +3,10 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::locale::{Language, tr, trs}; +use crate::{ + locale::{Language, tr, trs}, + scaffold::SessionInfo, +}; use jellycommon::{ routes::{u_account_login, u_account_settings}, user::{PlayerKind, Theme}, @@ -11,7 +14,7 @@ use jellycommon::{ use markup::RenderAttributeValue; markup::define! { - Settings<'a>(flash: Option<Result<String, String>>, lang: &'a Language) { + Settings<'a>(flash: Option<Result<String, String>>, session: &'a SessionInfo, lang: &'a Language) { h1 { "Settings" } @if let Some(flash) = &flash { @match flash { diff --git a/ui/src/stats.rs b/ui/src/stats.rs index 3655245..c3e5a14 100644 --- a/ui/src/stats.rs +++ b/ui/src/stats.rs @@ -8,7 +8,10 @@ use crate::{ format::{format_duration, format_duration_long, format_kind, format_size}, locale::{Language, tr, trs}, }; -use jellycommon::api::{ApiStatsResponse, StatsBin}; +use jellycommon::{ + api::{ApiStatsResponse, StatsBin}, + routes::u_node_slug, +}; use markup::raw; markup::define! { @@ -46,8 +49,8 @@ markup::define! { td { @format_duration(b.runtime) } td { @format_size(b.average_size() as u64) } td { @format_duration(b.average_runtime()) } - td { @if b.max_size.0 > 0 { a[href=uri!(r_library_node(&b.max_size.1))]{ @format_size(b.max_size.0) }}} - td { @if b.max_runtime.0 > 0. { a[href=uri!(r_library_node(&b.max_runtime.1))]{ @format_duration(b.max_runtime.0) }}} + td { @if b.max_size.0 > 0 { a[href=u_node_slug(&b.max_size.1)]{ @format_size(b.max_size.0) }}} + td { @if b.max_runtime.0 > 0. { a[href=u_node_slug(&b.max_runtime.1)]{ @format_duration(b.max_runtime.0) }}} }} } } |