aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-04-29 11:10:21 +0200
committermetamuffin <metamuffin@disroot.org>2025-04-29 11:10:21 +0200
commitf62c7f2a8cc143454779dc99334ca9fc80ddabd5 (patch)
treef31dbb908715d2deb2860e2097fa13dd41d759d5
parent73d2d5eb01fceae9e0b1c58afb648822000c878a (diff)
downloadjellything-f62c7f2a8cc143454779dc99334ca9fc80ddabd5.tar
jellything-f62c7f2a8cc143454779dc99334ca9fc80ddabd5.tar.bz2
jellything-f62c7f2a8cc143454779dc99334ca9fc80ddabd5.tar.zst
still just moving code around
-rw-r--r--Cargo.lock6
-rw-r--r--common/src/lib.rs4
-rw-r--r--common/src/routes.rs24
-rw-r--r--server/src/api.rs6
-rw-r--r--server/src/locale.rs2
-rw-r--r--server/src/logic/session.rs29
-rw-r--r--server/src/logic/stream.rs2
-rw-r--r--server/src/logic/userdata.rs12
-rw-r--r--server/src/routes.rs8
-rw-r--r--server/src/ui/admin/user.rs63
-rw-r--r--server/src/ui/assets.rs7
-rw-r--r--server/src/ui/browser.rs61
-rw-r--r--server/src/ui/error.rs37
-rw-r--r--server/src/ui/home.rs38
-rw-r--r--server/src/ui/items.rs48
-rw-r--r--server/src/ui/mod.rs89
-rw-r--r--server/src/ui/node.rs106
-rw-r--r--server/src/ui/player.rs102
-rw-r--r--server/src/ui/search.rs47
-rw-r--r--server/src/ui/stats.rs44
-rw-r--r--ui/Cargo.toml2
-rw-r--r--ui/src/admin/mod.rs7
-rw-r--r--ui/src/admin/user.rs55
-rw-r--r--ui/src/home.rs2
-rw-r--r--ui/src/items.rs46
-rw-r--r--ui/src/lib.rs48
-rw-r--r--ui/src/scaffold.rs32
-rw-r--r--ui/src/settings.rs7
-rw-r--r--ui/src/stats.rs9
29 files changed, 493 insertions, 450 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 98a4879..de6ac57 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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) }}}
}}
}
}