aboutsummaryrefslogtreecommitdiff
path: root/server/src
diff options
context:
space:
mode:
authortpart <tpart120@proton.me>2023-08-07 12:00:50 +0200
committertpart <tpart120@proton.me>2023-08-07 12:00:50 +0200
commit6e3c84c12b2182ff0e33c1447d94979358919148 (patch)
tree567a0b5b3662a8f36f4ce0f4a54025300858bf46 /server/src
parenteef08157290987437829db0c1bbc82183732b6b5 (diff)
parentb8d46141a13610f7451d7ad809691b0dd99b5f89 (diff)
downloadjellything-6e3c84c12b2182ff0e33c1447d94979358919148.tar
jellything-6e3c84c12b2182ff0e33c1447d94979358919148.tar.bz2
jellything-6e3c84c12b2182ff0e33c1447d94979358919148.tar.zst
Merge branch 'master' of codeberg.org:metamuffin/jellything
Diffstat (limited to 'server/src')
-rw-r--r--server/src/main.rs1
-rw-r--r--server/src/routes/mod.rs8
-rw-r--r--server/src/routes/ui/browser.rs52
-rw-r--r--server/src/routes/ui/mod.rs1
-rw-r--r--server/src/routes/ui/node.rs25
-rw-r--r--server/src/routes/ui/sort.rs95
6 files changed, 164 insertions, 18 deletions
diff --git a/server/src/main.rs b/server/src/main.rs
index 8f26253..43b1db4 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -4,6 +4,7 @@
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
#![feature(lazy_cell)]
+#![feature(int_roundings)]
use crate::routes::ui::admin::log::enable_logging;
use database::Database;
diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs
index e898c8f..6ebde33 100644
--- a/server/src/routes/mod.rs
+++ b/server/src/routes/mod.rs
@@ -27,10 +27,10 @@ use ui::{
r_admin_remove_invite, r_admin_remove_user,
},
assets::r_item_assets,
- browser::r_all_items,
+ browser::r_all_items_filter,
error::{r_api_catch, r_catch},
home::{r_home, r_home_unpriv},
- node::r_library_node,
+ node::r_library_node_filter,
player::r_player,
style::{r_assets_font, r_assets_js, r_assets_style},
};
@@ -87,8 +87,8 @@ pub fn build_rocket(
r_home_unpriv,
r_favicon,
r_item_assets,
- r_all_items,
- r_library_node,
+ r_all_items_filter,
+ r_library_node_filter,
r_assets_style,
r_assets_font,
r_assets_js,
diff --git a/server/src/routes/ui/browser.rs b/server/src/routes/ui/browser.rs
index 6e772d0..d2c24bc 100644
--- a/server/src/routes/ui/browser.rs
+++ b/server/src/routes/ui/browser.rs
@@ -3,31 +3,65 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
-use super::{account::session::Session, error::MyError, layout::DynLayoutPage, node::NodeCard};
-use crate::database::Database;
+use super::{
+ account::session::Session,
+ error::MyError,
+ layout::DynLayoutPage,
+ node::NodeCard,
+ sort::{filter_and_sort_nodes, NodeFilterSort, NodeFilterSortForm},
+};
+use crate::{database::Database, uri};
use anyhow::Context;
-use jellycommon::{Node, NodeKind};
+use jellycommon::NodePublic;
use rocket::{get, State};
+/// This function is a stub and only useful for use in the uri! macro.
#[get("/items")]
-pub fn r_all_items(_sess: Session, db: &State<Database>) -> Result<DynLayoutPage<'_>, MyError> {
- let items = db
+pub fn r_all_items() {}
+
+#[get("/items?<page>&<filter..>")]
+pub fn r_all_items_filter(
+ _sess: Session,
+ db: &State<Database>,
+ page: Option<usize>,
+ filter: NodeFilterSort,
+) -> Result<DynLayoutPage<'_>, MyError> {
+ let mut items = db
.node
.iter()
.map(|e| e.context("listing"))
.collect::<anyhow::Result<Vec<_>>>()?
.into_iter()
- .filter(|(_, n)| matches!(n.public.kind, NodeKind::Movie | NodeKind::Series))
- .collect::<Vec<(String, Node)>>();
+ .map(|(k, n)| (k, n.public))
+ .collect::<Vec<(String, NodePublic)>>();
+
+ filter_and_sort_nodes(&filter, &mut items);
+
+ let page_size = 100;
+ let page = page.unwrap_or(0);
+ let offset = page * page_size;
+ let from = offset.min(items.len());
+ let to = (offset + page_size).min(items.len());
+ let max_page = items.len().div_ceil(page_size);
Ok(super::layout::LayoutPage {
title: "All Items".to_owned(),
content: markup::new! {
.page.dir {
h1 { "All Items" }
- ul.children { @for (id, node) in &items {
- li {@NodeCard { id, node: &node.public }}
+ @NodeFilterSortForm { f: &filter }
+ ul.children { @for (id, node) in &items[from..to] {
+ li {@NodeCard { id, node: &node }}
}}
+ p.pagecontrols {
+ span.current { "Page " @{page + 1} " of " @max_page " " }
+ @if page > 0 {
+ a.prev[href=uri!(r_all_items_filter(Some(page - 1), filter.clone()))] { "Previous page" } " "
+ }
+ @if page + 1 < max_page {
+ a.next[href=uri!(r_all_items_filter(Some(page + 1), filter.clone()))] { "Next page" }
+ }
+ }
}
},
..Default::default()
diff --git a/server/src/routes/ui/mod.rs b/server/src/routes/ui/mod.rs
index d561627..07098c8 100644
--- a/server/src/routes/ui/mod.rs
+++ b/server/src/routes/ui/mod.rs
@@ -31,6 +31,7 @@ pub mod layout;
pub mod node;
pub mod player;
pub mod style;
+pub mod sort;
pub struct HtmlTemplate<'a>(pub markup::DynRender<'a>);
diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs
index 301f89c..13afbbe 100644
--- a/server/src/routes/ui/node.rs
+++ b/server/src/routes/ui/node.rs
@@ -3,7 +3,12 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
-use super::{assets::rocket_uri_macro_r_item_assets, error::MyError, player::player_uri};
+use super::{
+ assets::rocket_uri_macro_r_item_assets,
+ error::MyError,
+ player::player_uri,
+ sort::{filter_and_sort_nodes, NodeFilterSort, NodeFilterSortForm},
+};
use crate::{
database::Database,
routes::{
@@ -20,12 +25,19 @@ use anyhow::{anyhow, Context};
use jellycommon::{MediaInfo, NodeKind, NodePublic, Rating, SourceTrackKind};
use rocket::{get, serde::json::Json, Either, State};
+/// This function is a stub and only useful for use in the uri! macro.
#[get("/n/<id>")]
-pub async fn r_library_node(
+pub fn r_library_node(id: String) {
+ drop(id)
+}
+
+#[get("/n/<id>?<filter..>")]
+pub async fn r_library_node_filter(
_sess: Session,
id: String,
db: &State<Database>,
aj: AcceptJson,
+ filter: NodeFilterSort,
) -> Result<Either<DynLayoutPage<'_>, Json<NodePublic>>, MyError> {
let node = db
.node
@@ -38,7 +50,7 @@ pub async fn r_library_node(
return Ok(Either::Right(Json(node)));
}
- let children = node
+ let mut children = node
.children
.iter()
.map(|c| {
@@ -54,11 +66,13 @@ pub async fn r_library_node(
.into_iter()
.collect();
+ filter_and_sort_nodes(&filter, &mut children);
+
Ok(Either::Left(LayoutPage {
title: node.title.to_string(),
show_back: true, //- !matches!(node.kind, NodeKind::Collection),
content: markup::new! {
- @NodePage { node: &node, id: &id, children: &children }
+ @NodePage { node: &node, id: &id, children: &children, filter: &filter }
},
..Default::default()
}))
@@ -88,7 +102,7 @@ markup::define! {
}
}
}
- NodePage<'a>(id: &'a str, node: &'a NodePublic, children: &'a Vec<(String, NodePublic)>) {
+ NodePage<'a>(id: &'a str, node: &'a NodePublic, children: &'a Vec<(String, NodePublic)>, filter: &'a NodeFilterSort) {
@if !matches!(node.kind, NodeKind::Collection) {
img.backdrop[src=uri!(r_item_assets(id, AssetRole::Backdrop, Some(2048)))];
}
@@ -110,6 +124,7 @@ markup::define! {
@if let NodeKind::Collection = node.kind {
@if let Some(parent) = &node.parent {
a.dirup[href=uri!(r_library_node(parent))] { "Go up" }
+ @NodeFilterSortForm { f: filter }
}
}
@match node.kind {
diff --git a/server/src/routes/ui/sort.rs b/server/src/routes/ui/sort.rs
new file mode 100644
index 0000000..4db8f3d
--- /dev/null
+++ b/server/src/routes/ui/sort.rs
@@ -0,0 +1,95 @@
+use jellycommon::{NodeKind, NodePublic};
+use rocket::{
+ http::uri::fmt::{Query, UriDisplay},
+ FromForm, FromFormField, UriDisplayQuery,
+};
+
+#[derive(FromForm, UriDisplayQuery, Default, Clone)]
+pub struct NodeFilterSort {
+ sort_by: Option<SortProperty>,
+ filter_kind: Option<Vec<NodeKind>>,
+ sort_order: Option<SortOrder>,
+}
+
+#[rustfmt::skip]
+#[derive(FromFormField, UriDisplayQuery, Clone, Copy, PartialEq, Eq)]
+enum SortProperty {
+ #[field(value = "release_date")] ReleaseDate,
+ #[field(value = "title")] Title,
+}
+
+#[rustfmt::skip]
+#[derive(FromFormField, UriDisplayQuery, Clone, Copy, PartialEq, Eq)]
+enum SortOrder {
+ #[field(value = "ascending")] Ascending,
+ #[field(value = "descending")] Descending,
+}
+
+pub fn filter_and_sort_nodes(f: &NodeFilterSort, nodes: &mut Vec<(String, NodePublic)>) {
+ nodes.retain(|(_id, node)| {
+ let mut o = true;
+ if let Some(kind) = &f.filter_kind {
+ o &= kind.contains(&node.kind)
+ }
+ o
+ });
+ if let Some(sort_prop) = f.sort_by {
+ match sort_prop {
+ SortProperty::ReleaseDate => nodes.sort_by_key(|(_, _n)| 0), // TODO
+ SortProperty::Title => nodes.sort_by(|(_, a), (_, b)| a.title.cmp(&b.title)),
+ }
+ }
+ match f.sort_order.unwrap_or(SortOrder::Ascending) {
+ SortOrder::Ascending => (),
+ SortOrder::Descending => nodes.reverse(),
+ }
+}
+
+markup::define! {
+ NodeFilterSortForm<'a>(f: &'a NodeFilterSort) {
+ details.filtersort {
+ summary { "Filter and Sort" }
+ form[method="GET", action=""] {
+ fieldset.filter {
+ legend { "Filter" }
+ @use NodeKind::*;
+ @for (value, label) in [(Movie, "Movie"), (Episode, "Episode"), (Video, "Video"), (Channel, "Channel"), (Series, "Series"), (Season, "Season"), (Collection, "Collection")] {
+ label { input[type="checkbox", name="filter_kind", value=A(value), checked=f.filter_kind.as_ref().map(|k|k.contains(&value)).unwrap_or(true)]; @label } br;
+ }
+ }
+ fieldset.sortby {
+ legend { "Sort By" }
+ @use SortProperty::*;
+ @for (value, label) in [(Title, "Title"), (ReleaseDate, "Release Date")] {
+ label { input[type="radio", name="sort_by", value=value, checked=Some(value)==f.sort_by]; @label } br;
+ }
+ }
+ fieldset.sortorder {
+ legend { "Sort Order" }
+ @use SortOrder::*;
+ @for (value, label) in [(Ascending, "Ascending"), (Descending, "Descending")] {
+ label { input[type="radio", name="sort_order", value=value, checked=Some(value)==f.sort_order]; @label } br;
+ }
+ }
+ input[type="submit", value="Apply"];
+ }
+ }
+ }
+}
+
+impl markup::Render for SortProperty {
+ fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
+ writer.write_fmt(format_args!("{}", self as &dyn UriDisplay<Query>))
+ }
+}
+impl markup::Render for SortOrder {
+ fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
+ writer.write_fmt(format_args!("{}", self as &dyn UriDisplay<Query>))
+ }
+}
+struct A(NodeKind);
+impl markup::Render for A {
+ fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
+ writer.write_fmt(format_args!("{}", &self.0 as &dyn UriDisplay<Query>))
+ }
+}