diff options
Diffstat (limited to 'server/src/routes/ui')
| -rw-r--r-- | server/src/routes/ui/browser.rs | 52 | ||||
| -rw-r--r-- | server/src/routes/ui/mod.rs | 1 | ||||
| -rw-r--r-- | server/src/routes/ui/node.rs | 25 | ||||
| -rw-r--r-- | server/src/routes/ui/sort.rs | 95 | 
4 files changed, 159 insertions, 14 deletions
| 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>)) +    } +} | 
