aboutsummaryrefslogtreecommitdiff
path: root/server/src/ui/sort.rs
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/ui/sort.rs')
-rw-r--r--server/src/ui/sort.rs297
1 files changed, 297 insertions, 0 deletions
diff --git a/server/src/ui/sort.rs b/server/src/ui/sort.rs
new file mode 100644
index 0000000..a241030
--- /dev/null
+++ b/server/src/ui/sort.rs
@@ -0,0 +1,297 @@
+/*
+ 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::ui::layout::trs;
+use jellybase::locale::Language;
+use jellycommon::{helpers::SortAnyway, user::NodeUserData, Node, NodeKind, Rating};
+use markup::RenderAttributeValue;
+use rocket::{
+ http::uri::fmt::{Query, UriDisplay},
+ FromForm, FromFormField, UriDisplayQuery,
+};
+use std::sync::Arc;
+
+#[derive(FromForm, UriDisplayQuery, Default, Clone)]
+pub struct NodeFilterSort {
+ pub sort_by: Option<SortProperty>,
+ pub filter_kind: Option<Vec<FilterProperty>>,
+ pub sort_order: Option<SortOrder>,
+}
+
+macro_rules! form_enum {
+ (enum $i:ident { $($vi:ident = $vk:literal),*, }) => {
+ #[derive(Debug, FromFormField, UriDisplayQuery, Clone, Copy, PartialEq, Eq)]
+ pub enum $i { $(#[field(value = $vk)] $vi),* }
+ impl $i { #[allow(unused)] const ALL: &'static [$i] = &[$($i::$vi),*]; }
+ };
+}
+
+form_enum!(
+ enum FilterProperty {
+ FederationLocal = "fed_local",
+ FederationRemote = "fed_remote",
+ Watched = "watched",
+ Unwatched = "unwatched",
+ WatchProgress = "watch_progress",
+ KindMovie = "kind_movie",
+ KindVideo = "kind_video",
+ KindShortFormVideo = "kind_short_form_video",
+ KindMusic = "kind_music",
+ KindCollection = "kind_collection",
+ KindChannel = "kind_channel",
+ KindShow = "kind_show",
+ KindSeries = "kind_series",
+ KindSeason = "kind_season",
+ KindEpisode = "kind_episode",
+ }
+);
+
+form_enum!(
+ enum SortProperty {
+ ReleaseDate = "release_date",
+ Title = "title",
+ Index = "index",
+ Duration = "duration",
+ RatingRottenTomatoes = "rating_rt",
+ RatingMetacritic = "rating_mc",
+ RatingImdb = "rating_imdb",
+ RatingTmdb = "rating_tmdb",
+ RatingYoutubeViews = "rating_yt_views",
+ RatingYoutubeLikes = "rating_yt_likes",
+ RatingYoutubeFollowers = "rating_yt_followers",
+ RatingUser = "rating_user",
+ RatingLikesDivViews = "rating_loved",
+ }
+);
+
+impl SortProperty {
+ const CATS: &'static [(&'static str, &'static [(SortProperty, &'static str)])] = {
+ use SortProperty::*;
+ &[
+ (
+ "filter_sort.sort.general",
+ &[(Title, "node.title"), (ReleaseDate, "node.release_date")],
+ ),
+ ("filter_sort.sort.media", &[(Duration, "media.runtime")]),
+ (
+ "filter_sort.sort.rating",
+ &[
+ (RatingImdb, "rating.imdb"),
+ (RatingTmdb, "rating.tmdb"),
+ (RatingMetacritic, "rating.metacritic"),
+ (RatingRottenTomatoes, "rating.rotten_tomatoes"),
+ (RatingYoutubeFollowers, "rating.youtube_followers"),
+ (RatingYoutubeLikes, "rating.youtube_likes"),
+ (RatingYoutubeViews, "rating.youtube_views"),
+ (RatingUser, "filter_sort.sort.rating.user"),
+ (
+ RatingLikesDivViews,
+ "filter_sort.sort.rating.likes_div_views",
+ ),
+ ],
+ ),
+ ]
+ };
+}
+impl FilterProperty {
+ const CATS: &'static [(&'static str, &'static [(FilterProperty, &'static str)])] = {
+ use FilterProperty::*;
+ &[
+ (
+ "filter_sort.filter.kind",
+ &[
+ (KindMovie, "kind.movie"),
+ (KindVideo, "kind.video"),
+ (KindShortFormVideo, "kind.short_form_video"),
+ (KindMusic, "kind.music"),
+ (KindCollection, "kind.collection"),
+ (KindChannel, "kind.channel"),
+ (KindShow, "kind.show"),
+ (KindSeries, "kind.series"),
+ (KindSeason, "kind.season"),
+ (KindEpisode, "kind.episode"),
+ ],
+ ),
+ (
+ "filter_sort.filter.federation",
+ &[
+ (FederationLocal, "federation.local"),
+ (FederationRemote, "federation.remote"),
+ ],
+ ),
+ (
+ "filter_sort.filter.watched",
+ &[
+ (Watched, "watched.watched"),
+ (Unwatched, "watched.none"),
+ (WatchProgress, "watched.progress"),
+ ],
+ ),
+ ]
+ };
+}
+
+impl NodeFilterSort {
+ pub fn is_open(&self) -> bool {
+ self.filter_kind.is_some() || self.sort_by.is_some()
+ }
+}
+
+#[rustfmt::skip]
+#[derive(FromFormField, UriDisplayQuery, Clone, Copy, PartialEq, Eq)]
+pub enum SortOrder {
+ #[field(value = "ascending")] Ascending,
+ #[field(value = "descending")] Descending,
+}
+
+pub fn filter_and_sort_nodes(
+ f: &NodeFilterSort,
+ default_sort: (SortProperty, SortOrder),
+ nodes: &mut Vec<(Arc<Node>, NodeUserData)>,
+) {
+ let sort_prop = f.sort_by.unwrap_or(default_sort.0);
+ nodes.retain(|(node, _udata)| {
+ let mut o = true;
+ if let Some(prop) = &f.filter_kind {
+ o = false;
+ for p in prop {
+ o |= match p {
+ // FilterProperty::FederationLocal => node.federated.is_none(),
+ // FilterProperty::FederationRemote => node.federated.is_some(),
+ FilterProperty::KindMovie => node.kind == NodeKind::Movie,
+ FilterProperty::KindVideo => node.kind == NodeKind::Video,
+ FilterProperty::KindShortFormVideo => node.kind == NodeKind::ShortFormVideo,
+ FilterProperty::KindMusic => node.kind == NodeKind::Music,
+ FilterProperty::KindCollection => node.kind == NodeKind::Collection,
+ FilterProperty::KindChannel => node.kind == NodeKind::Channel,
+ FilterProperty::KindShow => node.kind == NodeKind::Show,
+ FilterProperty::KindSeries => node.kind == NodeKind::Series,
+ FilterProperty::KindSeason => node.kind == NodeKind::Season,
+ FilterProperty::KindEpisode => node.kind == NodeKind::Episode,
+ // FilterProperty::Watched => udata.watched == WatchedState::Watched,
+ // FilterProperty::Unwatched => udata.watched == WatchedState::None,
+ // FilterProperty::WatchProgress => {
+ // matches!(udata.watched, WatchedState::Progress(_))
+ // }
+ _ => false, // TODO
+ }
+ }
+ }
+ match sort_prop {
+ SortProperty::ReleaseDate => o &= node.release_date.is_some(),
+ SortProperty::Duration => o &= node.media.is_some(),
+ _ => (),
+ }
+ o
+ });
+ match sort_prop {
+ SortProperty::Duration => {
+ nodes.sort_by_key(|(n, _)| (n.media.as_ref().unwrap().duration * 1000.) as i64)
+ }
+ SortProperty::ReleaseDate => {
+ nodes.sort_by_key(|(n, _)| n.release_date.expect("asserted above"))
+ }
+ SortProperty::Title => nodes.sort_by(|(a, _), (b, _)| a.title.cmp(&b.title)),
+ SortProperty::Index => nodes.sort_by(|(a, _), (b, _)| {
+ a.index
+ .unwrap_or(usize::MAX)
+ .cmp(&b.index.unwrap_or(usize::MAX))
+ }),
+ SortProperty::RatingRottenTomatoes => nodes.sort_by_cached_key(|(n, _)| {
+ SortAnyway(*n.ratings.get(&Rating::RottenTomatoes).unwrap_or(&0.))
+ }),
+ SortProperty::RatingMetacritic => nodes.sort_by_cached_key(|(n, _)| {
+ SortAnyway(*n.ratings.get(&Rating::Metacritic).unwrap_or(&0.))
+ }),
+ SortProperty::RatingImdb => nodes
+ .sort_by_cached_key(|(n, _)| SortAnyway(*n.ratings.get(&Rating::Imdb).unwrap_or(&0.))),
+ SortProperty::RatingTmdb => nodes
+ .sort_by_cached_key(|(n, _)| SortAnyway(*n.ratings.get(&Rating::Tmdb).unwrap_or(&0.))),
+ SortProperty::RatingYoutubeViews => nodes.sort_by_cached_key(|(n, _)| {
+ SortAnyway(*n.ratings.get(&Rating::YoutubeViews).unwrap_or(&0.))
+ }),
+ SortProperty::RatingYoutubeLikes => nodes.sort_by_cached_key(|(n, _)| {
+ SortAnyway(*n.ratings.get(&Rating::YoutubeLikes).unwrap_or(&0.))
+ }),
+ SortProperty::RatingYoutubeFollowers => nodes.sort_by_cached_key(|(n, _)| {
+ SortAnyway(*n.ratings.get(&Rating::YoutubeFollowers).unwrap_or(&0.))
+ }),
+ SortProperty::RatingLikesDivViews => nodes.sort_by_cached_key(|(n, _)| {
+ SortAnyway(
+ *n.ratings.get(&Rating::YoutubeLikes).unwrap_or(&0.)
+ / (1. + *n.ratings.get(&Rating::YoutubeViews).unwrap_or(&0.)),
+ )
+ }),
+ SortProperty::RatingUser => nodes.sort_by_cached_key(|(_, u)| u.rating),
+ }
+
+ match f.sort_order.unwrap_or(default_sort.1) {
+ SortOrder::Ascending => (),
+ SortOrder::Descending => nodes.reverse(),
+ }
+}
+
+markup::define! {
+ NodeFilterSortForm<'a>(f: &'a NodeFilterSort, lang: &'a Language) {
+ details.filtersort[open=f.is_open()] {
+ summary { "Filter and Sort" }
+ form[method="GET", action=""] {
+ fieldset.filter {
+ legend { "Filter" }
+ .categories {
+ @for (cname, cat) in FilterProperty::CATS {
+ .category {
+ h3 { @trs(lang, cname) }
+ @for (value, label) in *cat {
+ label { input[type="checkbox", name="filter_kind", value=value, checked=f.filter_kind.as_ref().map(|k|k.contains(value)).unwrap_or(true)]; @trs(lang, label) } br;
+ }
+ }
+ }
+ }
+ }
+ fieldset.sortby {
+ legend { "Sort" }
+ .categories {
+ @for (cname, cat) in SortProperty::CATS {
+ .category {
+ h3 { @trs(lang, cname) }
+ @for (value, label) in *cat {
+ label { input[type="radio", name="sort_by", value=value, checked=Some(value)==f.sort_by.as_ref()]; @trs(lang, label) } br;
+ }
+ }
+ }
+ }
+ }
+ fieldset.sortorder {
+ legend { "Sort Order" }
+ @use SortOrder::*;
+ @for (value, label) in [(Ascending, "filter_sort.order.asc"), (Descending, "filter_sort.order.desc")] {
+ label { input[type="radio", name="sort_order", value=value, checked=Some(value)==f.sort_order]; @trs(lang, label) } br;
+ }
+ }
+ input[type="submit", value="Apply"]; a[href="?"] { "Clear" }
+ }
+ }
+ }
+}
+
+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>))
+ }
+}
+impl markup::Render for FilterProperty {
+ fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
+ writer.write_fmt(format_args!("{}", self as &dyn UriDisplay<Query>))
+ }
+}
+impl RenderAttributeValue for SortOrder {}
+impl RenderAttributeValue for FilterProperty {}
+impl RenderAttributeValue for SortProperty {}