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; use crate::routes::ui::layout::trs; #[derive(FromForm, UriDisplayQuery, Default, Clone)] pub struct NodeFilterSort { pub sort_by: Option, pub filter_kind: Option>, pub sort_order: Option, } 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, "Local"), (FederationRemote, "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, 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 { @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 { @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, "Ascending"), (Descending, "Descending")] { 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)) } } 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)) } } 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)) } } impl RenderAttributeValue for SortOrder {} impl RenderAttributeValue for FilterProperty {} impl RenderAttributeValue for SortProperty {}