From 08eabb24db957bea63f4f9e7f5b4db3f51d31001 Mon Sep 17 00:00:00 2001 From: Dark-Alex-17 Date: Tue, 8 Aug 2023 10:50:05 -0600 Subject: [PATCH] Added support for sorting when looking to manually add a release --- Makefile | 3 + src/app/key_binding.rs | 5 + src/app/radarr.rs | 22 +- .../radarr_handlers/movie_details_handler.rs | 322 +++++++++++++++++- src/main.rs | 2 + src/models/mod.rs | 2 +- src/models/radarr_models.rs | 22 +- src/ui/radarr_ui/movie_details_ui.rs | 57 +++- 8 files changed, 411 insertions(+), 24 deletions(-) diff --git a/Makefile b/Makefile index 99d4822..45d93f7 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,8 @@ +#!make + default: run +.PHONY: test test-cov build run lint lint-fix fmt analyze sonar release delete-tag test: @cargo test diff --git a/src/app/key_binding.rs b/src/app/key_binding.rs index e2c6b44..2236c36 100644 --- a/src/app/key_binding.rs +++ b/src/app/key_binding.rs @@ -17,6 +17,7 @@ generate_keybindings! { backspace, search, filter, + sort, refresh, home, end, @@ -64,6 +65,10 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings { key: Key::Char('f'), desc: "Filter", }, + sort: KeyBinding { + key: Key::Char('o'), + desc: "Sort", + }, refresh: KeyBinding { key: Key::Char('r'), desc: "Refresh", diff --git a/src/app/radarr.rs b/src/app/radarr.rs index 2d62d1d..f2ac83c 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -6,7 +6,7 @@ use chrono::{DateTime, Utc}; use crate::app::{App, Route}; use crate::models::radarr_models::{ AddMovieSearchResult, Collection, CollectionMovie, Credit, DiskSpace, DownloadRecord, - MinimumAvailability, Monitor, Movie, MovieHistoryItem, Release, RootFolder, + MinimumAvailability, Monitor, Movie, MovieHistoryItem, Release, ReleaseField, RootFolder, }; use crate::models::{ScrollableText, StatefulList, StatefulTable, TabRoute, TabState}; use crate::network::radarr_network::RadarrEvent; @@ -33,6 +33,7 @@ pub struct RadarrData { pub movie_cast: StatefulTable, pub movie_crew: StatefulTable, pub movie_releases: StatefulTable, + pub movie_releases_sort: StatefulList, pub collections: StatefulTable, pub filtered_collections: StatefulTable, pub collection_movies: StatefulTable, @@ -41,6 +42,7 @@ pub struct RadarrData { pub movie_info_tabs: TabState, pub search: String, pub filter: String, + pub sort_ascending: Option, pub prompt_confirm: bool, pub is_searching: bool, } @@ -68,6 +70,8 @@ impl RadarrData { self.movie_cast = StatefulTable::default(); self.movie_crew = StatefulTable::default(); self.movie_releases = StatefulTable::default(); + self.movie_releases_sort = StatefulList::default(); + self.sort_ascending = None; self.movie_info_tabs.index = 0; } @@ -102,12 +106,14 @@ impl Default for RadarrData { movie_cast: StatefulTable::default(), movie_crew: StatefulTable::default(), movie_releases: StatefulTable::default(), + movie_releases_sort: StatefulList::default(), collections: StatefulTable::default(), filtered_collections: StatefulTable::default(), collection_movies: StatefulTable::default(), prompt_confirm_action: None, search: String::default(), filter: String::default(), + sort_ascending: None, is_searching: false, prompt_confirm: false, main_tabs: TabState::new(vec![ @@ -166,7 +172,7 @@ impl Default for RadarrData { TabRoute { title: "Manual Search".to_owned(), route: ActiveRadarrBlock::ManualSearch.into(), - help: " refresh | auto search | close".to_owned(), + help: " refresh | sort | auto search | close".to_owned(), contextual_help: Some(" details".to_owned()) } ]), @@ -196,6 +202,7 @@ pub enum ActiveRadarrBlock { FilterCollections, FilterMovies, ManualSearch, + ManualSearchSortPrompt, ManualSearchConfirmPrompt, MovieDetails, MovieHistory, @@ -218,7 +225,7 @@ pub const ADD_MOVIE_BLOCKS: [ActiveRadarrBlock; 7] = [ ActiveRadarrBlock::AddMovieSelectQualityProfile, ActiveRadarrBlock::AddMovieAlreadyInLibrary, ]; -pub const MOVIE_DETAILS_BLOCKS: [ActiveRadarrBlock; 9] = [ +pub const MOVIE_DETAILS_BLOCKS: [ActiveRadarrBlock; 10] = [ ActiveRadarrBlock::MovieDetails, ActiveRadarrBlock::MovieHistory, ActiveRadarrBlock::FileInfo, @@ -227,6 +234,7 @@ pub const MOVIE_DETAILS_BLOCKS: [ActiveRadarrBlock; 9] = [ ActiveRadarrBlock::AutomaticallySearchMoviePrompt, ActiveRadarrBlock::RefreshAndScanPrompt, ActiveRadarrBlock::ManualSearch, + ActiveRadarrBlock::ManualSearchSortPrompt, ActiveRadarrBlock::ManualSearchConfirmPrompt, ]; pub const COLLECTION_DETAILS_BLOCKS: [ActiveRadarrBlock; 2] = [ @@ -424,7 +432,7 @@ pub mod radarr_test_utils { use crate::app::radarr::RadarrData; use crate::models::radarr_models::{ AddMovieSearchResult, Collection, CollectionMovie, Credit, MinimumAvailability, Monitor, Movie, - MovieHistoryItem, Release, + MovieHistoryItem, Release, ReleaseField, }; use crate::models::ScrollableText; @@ -457,6 +465,10 @@ pub mod radarr_test_utils { radarr_data .add_movie_quality_profile_list .set_items(vec![String::default()]); + radarr_data + .movie_releases_sort + .set_items(vec![ReleaseField::default()]); + radarr_data.sort_ascending = Some(true); radarr_data .filtered_movies .set_items(vec![Movie::default()]); @@ -503,6 +515,8 @@ pub mod radarr_test_utils { assert!($radarr_data.movie_cast.items.is_empty()); assert!($radarr_data.movie_crew.items.is_empty()); assert!($radarr_data.movie_releases.items.is_empty()); + assert!($radarr_data.movie_releases_sort.items.is_empty()); + assert!($radarr_data.sort_ascending.is_none()); assert_eq!($radarr_data.movie_info_tabs.index, 0); }; } diff --git a/src/handlers/radarr_handlers/movie_details_handler.rs b/src/handlers/radarr_handlers/movie_details_handler.rs index 630c967..089d3e6 100644 --- a/src/handlers/radarr_handlers/movie_details_handler.rs +++ b/src/handlers/radarr_handlers/movie_details_handler.rs @@ -1,8 +1,14 @@ +use std::cmp::Ordering; + +use serde_json::Number; +use strum::IntoEnumIterator; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::radarr::ActiveRadarrBlock; use crate::app::App; use crate::event::Key; use crate::handlers::{handle_prompt_toggle, KeyEventHandler}; +use crate::models::radarr_models::{Language, Release, ReleaseField}; use crate::models::Scrollable; use crate::network::radarr_network::RadarrEvent; @@ -36,6 +42,9 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for MovieDetailsHandler<'a> { ActiveRadarrBlock::Cast => self.app.data.radarr_data.movie_cast.scroll_up(), ActiveRadarrBlock::Crew => self.app.data.radarr_data.movie_crew.scroll_up(), ActiveRadarrBlock::ManualSearch => self.app.data.radarr_data.movie_releases.scroll_up(), + ActiveRadarrBlock::ManualSearchSortPrompt => { + self.app.data.radarr_data.movie_releases_sort.scroll_up() + } _ => (), } } @@ -47,6 +56,9 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for MovieDetailsHandler<'a> { ActiveRadarrBlock::Cast => self.app.data.radarr_data.movie_cast.scroll_down(), ActiveRadarrBlock::Crew => self.app.data.radarr_data.movie_crew.scroll_down(), ActiveRadarrBlock::ManualSearch => self.app.data.radarr_data.movie_releases.scroll_down(), + ActiveRadarrBlock::ManualSearchSortPrompt => { + self.app.data.radarr_data.movie_releases_sort.scroll_down() + } _ => (), } } @@ -58,6 +70,12 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for MovieDetailsHandler<'a> { ActiveRadarrBlock::Cast => self.app.data.radarr_data.movie_cast.scroll_to_top(), ActiveRadarrBlock::Crew => self.app.data.radarr_data.movie_crew.scroll_to_top(), ActiveRadarrBlock::ManualSearch => self.app.data.radarr_data.movie_releases.scroll_to_top(), + ActiveRadarrBlock::ManualSearchSortPrompt => self + .app + .data + .radarr_data + .movie_releases_sort + .scroll_to_top(), _ => (), } } @@ -71,6 +89,12 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for MovieDetailsHandler<'a> { ActiveRadarrBlock::ManualSearch => { self.app.data.radarr_data.movie_releases.scroll_to_bottom() } + ActiveRadarrBlock::ManualSearchSortPrompt => self + .app + .data + .radarr_data + .movie_releases_sort + .scroll_to_bottom(), _ => (), } } @@ -135,6 +159,29 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for MovieDetailsHandler<'a> { self.app.pop_navigation_stack(); } + ActiveRadarrBlock::ManualSearchSortPrompt => { + let movie_releases = self.app.data.radarr_data.movie_releases.items.clone(); + let field = self + .app + .data + .radarr_data + .movie_releases_sort + .current_selection(); + let sort_ascending = !self.app.data.radarr_data.sort_ascending.unwrap(); + self.app.data.radarr_data.sort_ascending = Some(sort_ascending); + + self + .app + .data + .radarr_data + .movie_releases + .set_items(sort_releases_by_selected_field( + movie_releases, + *field, + sort_ascending, + )); + self.app.pop_navigation_stack(); + } _ => (), } } @@ -152,7 +199,8 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for MovieDetailsHandler<'a> { } ActiveRadarrBlock::AutomaticallySearchMoviePrompt | ActiveRadarrBlock::RefreshAndScanPrompt - | ActiveRadarrBlock::ManualSearchConfirmPrompt => { + | ActiveRadarrBlock::ManualSearchConfirmPrompt + | ActiveRadarrBlock::ManualSearchSortPrompt => { self.app.pop_navigation_stack(); self.app.data.radarr_data.prompt_confirm = false; } @@ -179,6 +227,20 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for MovieDetailsHandler<'a> { .app .push_navigation_stack(ActiveRadarrBlock::RefreshAndScanPrompt.into()); } + _ if *key == DEFAULT_KEYBINDINGS.sort.key => { + self + .app + .data + .radarr_data + .movie_releases_sort + .set_items(Vec::from_iter(ReleaseField::iter())); + let sort_ascending = self.app.data.radarr_data.sort_ascending; + self.app.data.radarr_data.sort_ascending = + Some(sort_ascending.is_some() && sort_ascending.unwrap()); + self + .app + .push_navigation_stack(ActiveRadarrBlock::ManualSearchSortPrompt.into()); + } _ => (), }, _ => (), @@ -186,24 +248,100 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for MovieDetailsHandler<'a> { } } +fn sort_releases_by_selected_field( + mut releases: Vec, + field: ReleaseField, + sort_ascending: bool, +) -> Vec { + let cmp_fn: fn(&Release, &Release) -> Ordering = match field { + ReleaseField::Source => |release_a, release_b| release_a.protocol.cmp(&release_b.protocol), + ReleaseField::Age => |release_a, release_b| release_a.age.as_u64().cmp(&release_b.age.as_u64()), + ReleaseField::Rejected => |release_a, release_b| release_a.rejected.cmp(&release_b.rejected), + ReleaseField::Title => |release_a, release_b| { + release_a + .title + .stationary_style() + .cmp(&release_b.title.stationary_style()) + }, + ReleaseField::Indexer => |release_a, release_b| release_a.indexer.cmp(&release_b.indexer), + ReleaseField::Size => |release_a, release_b| { + release_a + .size + .as_u64() + .as_ref() + .unwrap() + .cmp(release_b.size.as_u64().as_ref().unwrap()) + }, + ReleaseField::Peers => |release_a, release_b| { + let default_number = Number::from(i64::MAX); + let seeder_a = release_a + .seeders + .as_ref() + .unwrap_or(&default_number) + .as_u64() + .unwrap(); + let seeder_b = release_b + .seeders + .as_ref() + .unwrap_or(&default_number) + .as_u64() + .unwrap(); + + seeder_a.cmp(&seeder_b) + }, + ReleaseField::Language => |release_a, release_b| { + let default_language_vec = vec![Language { + name: "_".to_owned(), + }]; + let language_a = &release_a + .languages + .as_ref() + .unwrap_or(&default_language_vec)[0]; + let language_b = &release_b + .languages + .as_ref() + .unwrap_or(&default_language_vec)[0]; + + language_a.cmp(language_b) + }, + ReleaseField::Quality => |release_a, release_b| release_a.quality.cmp(&release_b.quality), + }; + + if !sort_ascending { + releases.sort_by(|release_a, release_b| cmp_fn(release_a, release_b).reverse()); + } else { + releases.sort_by(cmp_fn); + } + + releases +} + #[cfg(test)] mod tests { use pretty_assertions::assert_str_eq; + use rstest::rstest; + use serde_json::Number; use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::radarr::ActiveRadarrBlock; use crate::app::App; use crate::event::Key; - use crate::handlers::radarr_handlers::movie_details_handler::MovieDetailsHandler; + use crate::handlers::radarr_handlers::movie_details_handler::{ + sort_releases_by_selected_field, MovieDetailsHandler, + }; use crate::handlers::KeyEventHandler; - use crate::models::radarr_models::{Credit, MovieHistoryItem, Release}; + use crate::models::radarr_models::{ + Credit, Language, MovieHistoryItem, Quality, QualityWrapper, Release, ReleaseField, + }; use crate::models::{HorizontallyScrollableText, ScrollableText}; mod test_handle_scroll_up_and_down { use pretty_assertions::assert_eq; use rstest::rstest; + use strum::IntoEnumIterator; - use crate::{simple_stateful_iterable_vec, test_iterable_scroll}; + use crate::models::radarr_models::ReleaseField; + use crate::{simple_stateful_iterable_vec, test_enum_scroll, test_iterable_scroll}; use super::*; @@ -270,10 +408,23 @@ mod tests { title, stationary_style ); + + test_enum_scroll!( + test_manual_search_sort_scroll, + MovieDetailsHandler, + ReleaseField, + movie_releases_sort, + ActiveRadarrBlock::ManualSearchSortPrompt + ); } mod test_handle_home_end { - use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end}; + use strum::IntoEnumIterator; + + use crate::models::radarr_models::ReleaseField; + use crate::{ + extended_stateful_iterable_vec, test_enum_home_and_end, test_iterable_home_and_end, + }; use super::*; @@ -340,6 +491,14 @@ mod tests { title, stationary_style ); + + test_enum_home_and_end!( + test_manual_search_sort_home_end, + MovieDetailsHandler, + ReleaseField, + movie_releases_sort, + ActiveRadarrBlock::ManualSearchSortPrompt + ); } mod test_handle_left_right_action { @@ -413,6 +572,7 @@ mod tests { use pretty_assertions::assert_eq; use rstest::rstest; + use crate::models::radarr_models::ReleaseField; use crate::network::radarr_network::RadarrEvent; use super::*; @@ -486,6 +646,36 @@ mod tests { ); assert_eq!(app.data.radarr_data.prompt_confirm_action, None); } + + #[test] + fn test_manual_search_sort_prompt_submit() { + let mut app = App::default(); + app.push_navigation_stack(ActiveRadarrBlock::ManualSearch.into()); + app.push_navigation_stack(ActiveRadarrBlock::ManualSearchSortPrompt.into()); + app.data.radarr_data.sort_ascending = Some(true); + app + .data + .radarr_data + .movie_releases_sort + .set_items(vec![ReleaseField::default()]); + app.data.radarr_data.movie_releases.set_items(release_vec()); + + let mut expected_vec = release_vec(); + expected_vec.reverse(); + + MovieDetailsHandler::with( + &SUBMIT_KEY, + &mut app, + &ActiveRadarrBlock::ManualSearchSortPrompt, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::ManualSearch.into() + ); + assert_eq!(app.data.radarr_data.movie_releases.items, expected_vec); + } } mod test_handle_esc { @@ -527,7 +717,8 @@ mod tests { #[values( ActiveRadarrBlock::AutomaticallySearchMoviePrompt, ActiveRadarrBlock::RefreshAndScanPrompt, - ActiveRadarrBlock::ManualSearchConfirmPrompt + ActiveRadarrBlock::ManualSearchConfirmPrompt, + ActiveRadarrBlock::ManualSearchSortPrompt )] prompt_block: ActiveRadarrBlock, ) { @@ -576,6 +767,26 @@ mod tests { ); } + #[test] + fn test_sort_key() { + let mut app = App::default(); + + MovieDetailsHandler::with( + &DEFAULT_KEYBINDINGS.sort.key, + &mut app, + &ActiveRadarrBlock::ManualSearch, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + &ActiveRadarrBlock::ManualSearchSortPrompt.into() + ); + assert!(!app.data.radarr_data.movie_releases_sort.items.is_empty()); + assert!(app.data.radarr_data.sort_ascending.is_some()); + assert_eq!(app.data.radarr_data.sort_ascending, Some(false)); + } + #[rstest] fn test_refresh_key( #[values( @@ -603,4 +814,103 @@ mod tests { ); } } + + #[rstest] + fn test_sort_releases_by_selected_field( + #[values( + ReleaseField::Source, + ReleaseField::Age, + ReleaseField::Title, + ReleaseField::Indexer, + ReleaseField::Size, + ReleaseField::Peers, + ReleaseField::Language, + ReleaseField::Quality + )] + field: ReleaseField, + ) { + let mut expected_vec = release_vec(); + + let sorted_releases = sort_releases_by_selected_field(release_vec(), field, true); + + assert_eq!(sorted_releases, expected_vec); + + let sorted_releases = sort_releases_by_selected_field(release_vec(), field, false); + + expected_vec.reverse(); + assert_eq!(sorted_releases, expected_vec); + } + + #[test] + fn test_sort_releases_by_selected_field_rejected() { + let mut expected_vec = Vec::from(&release_vec()[1..]); + expected_vec.push(release_vec()[0].clone()); + + let sorted_releases = + sort_releases_by_selected_field(release_vec(), ReleaseField::Rejected, true); + + assert_eq!(sorted_releases, expected_vec); + + let sorted_releases = + sort_releases_by_selected_field(release_vec(), ReleaseField::Rejected, false); + + assert_eq!(sorted_releases, release_vec()); + } + + fn release_vec() -> Vec { + let release_a = Release { + protocol: "Protocol A".to_owned(), + age: Number::from(1), + title: HorizontallyScrollableText::from("Title A".to_owned()), + indexer: "Indexer A".to_owned(), + size: Number::from(1), + rejected: true, + seeders: Some(Number::from(1)), + languages: Some(vec![Language { + name: "Language A".to_owned(), + }]), + quality: QualityWrapper { + quality: Quality { + name: "Quality A".to_owned(), + }, + }, + ..Release::default() + }; + let release_b = Release { + protocol: "Protocol B".to_owned(), + age: Number::from(2), + title: HorizontallyScrollableText::from("Title B".to_owned()), + indexer: "Indexer B".to_owned(), + size: Number::from(2), + rejected: false, + seeders: Some(Number::from(2)), + languages: Some(vec![Language { + name: "Language B".to_owned(), + }]), + quality: QualityWrapper { + quality: Quality { + name: "Quality B".to_owned(), + }, + }, + ..Release::default() + }; + let release_c = Release { + protocol: "Protocol C".to_owned(), + age: Number::from(3), + title: HorizontallyScrollableText::from("Title C".to_owned()), + indexer: "Indexer C".to_owned(), + size: Number::from(3), + rejected: false, + seeders: None, + languages: None, + quality: QualityWrapper { + quality: Quality { + name: "Quality C".to_owned(), + }, + }, + ..Release::default() + }; + + vec![release_a, release_b, release_c] + } } diff --git a/src/main.rs b/src/main.rs index 02e3882..bd36d6b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +extern crate core; + use std::io; use std::sync::Arc; diff --git a/src/models/mod.rs b/src/models/mod.rs index 85a8e5a..ed8effa 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -208,7 +208,7 @@ impl HorizontallyScrollableText { } pub fn scroll_or_reset(&self, width: usize, is_current_selection: bool) { - if is_current_selection && self.text.len() > width { + if is_current_selection && self.text.len() - 8 > width { self.scroll_text(); } else { self.reset_offset(); diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index 141f7b6..068b8e6 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -4,7 +4,7 @@ use chrono::{DateTime, Utc}; use derivative::Derivative; use serde::{Deserialize, Serialize}; use serde_json::Number; -use strum_macros::EnumIter; +use strum_macros::{Display, EnumIter}; use crate::models::HorizontallyScrollableText; @@ -181,17 +181,17 @@ pub struct MovieHistoryItem { pub event_type: String, } -#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct Language { pub name: String, } -#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct Quality { pub name: String, } -#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct QualityWrapper { pub quality: Quality, } @@ -237,6 +237,20 @@ pub struct Release { pub quality: QualityWrapper, } +#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, Display)] +pub enum ReleaseField { + #[default] + Source, + Age, + Rejected, + Title, + Indexer, + Size, + Peers, + Language, + Quality, +} + #[derive(Default, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct AddMovieBody { diff --git a/src/ui/radarr_ui/movie_details_ui.rs b/src/ui/radarr_ui/movie_details_ui.rs index 4b78b30..cd6c2b8 100644 --- a/src/ui/radarr_ui/movie_details_ui.rs +++ b/src/ui/radarr_ui/movie_details_ui.rs @@ -4,12 +4,12 @@ use tui::backend::Backend; use tui::layout::{Alignment, Constraint, Rect}; use tui::style::{Modifier, Style}; use tui::text::{Span, Spans, Text}; -use tui::widgets::{Cell, Paragraph, Row, Wrap}; +use tui::widgets::{Cell, ListItem, Paragraph, Row, Wrap}; use tui::Frame; use crate::app::radarr::ActiveRadarrBlock; use crate::app::App; -use crate::models::radarr_models::{Credit, MovieHistoryItem, Release}; +use crate::models::radarr_models::{Credit, MovieHistoryItem, Release, ReleaseField}; use crate::models::Route; use crate::ui::utils::{ borderless_block, get_width_with_margin, layout_block_bottom_border, layout_block_top_border, @@ -17,8 +17,8 @@ use crate::ui::utils::{ style_warning, vertical_chunks, }; use crate::ui::{ - draw_prompt_box, draw_prompt_box_with_content, draw_prompt_popup_over, draw_small_popup_over, - draw_table, draw_tabs, loading, TableProps, + draw_drop_down_list, draw_drop_down_popup, draw_prompt_box, draw_prompt_box_with_content, + draw_prompt_popup_over, draw_small_popup_over, draw_table, draw_tabs, loading, TableProps, }; use crate::utils::convert_to_gb; @@ -41,6 +41,20 @@ pub(super) fn draw_movie_info_popup(f: &mut Frame<'_, B>, app: &mut draw_movie_info, draw_refresh_and_scan_prompt, ), + ActiveRadarrBlock::ManualSearchSortPrompt => draw_drop_down_popup( + f, + app, + content_area, + draw_movie_info, + |f, app, content_area| { + draw_drop_down_list( + f, + content_area, + &mut app.data.radarr_data.movie_releases_sort, + |sort_option| ListItem::new(sort_option.to_string()), + ) + }, + ), ActiveRadarrBlock::ManualSearchConfirmPrompt => draw_small_popup_over( f, app, @@ -350,6 +364,33 @@ fn draw_movie_releases(f: &mut Frame<'_, B>, app: &mut App, content_ .current_selection_clone() }; let current_route = *app.get_current_route(); + let mut table_headers_vec = vec![ + "Source".to_owned(), + "Age".to_owned(), + "⛔".to_owned(), + "Title".to_owned(), + "Indexer".to_owned(), + "Size".to_owned(), + "Peers".to_owned(), + "Language".to_owned(), + "Quality".to_owned(), + ]; + + if let Some(ascending) = app.data.radarr_data.sort_ascending { + let direction = if ascending { " ▲" } else { " ▼" }; + + match app.data.radarr_data.movie_releases_sort.current_selection() { + ReleaseField::Source => table_headers_vec[0].push_str(direction), + ReleaseField::Age => table_headers_vec[1].push_str(direction), + ReleaseField::Rejected => table_headers_vec[2].push_str(direction), + ReleaseField::Title => table_headers_vec[3].push_str(direction), + ReleaseField::Indexer => table_headers_vec[4].push_str(direction), + ReleaseField::Size => table_headers_vec[5].push_str(direction), + ReleaseField::Peers => table_headers_vec[6].push_str(direction), + ReleaseField::Language => table_headers_vec[7].push_str(direction), + ReleaseField::Quality => table_headers_vec[8].push_str(direction), + } + } draw_table( f, @@ -358,9 +399,9 @@ fn draw_movie_releases(f: &mut Frame<'_, B>, app: &mut App, content_ TableProps { content: &mut app.data.radarr_data.movie_releases, constraints: vec![ - Constraint::Length(8), + Constraint::Length(9), Constraint::Length(10), - Constraint::Length(4), + Constraint::Length(5), Constraint::Percentage(30), Constraint::Percentage(18), Constraint::Length(12), @@ -368,9 +409,7 @@ fn draw_movie_releases(f: &mut Frame<'_, B>, app: &mut App, content_ Constraint::Percentage(7), Constraint::Percentage(10), ], - table_headers: vec![ - "Source", "Age", "⛔", "Title", "Indexer", "Size", "Peers", "Language", "Quality", - ], + table_headers: table_headers_vec.iter().map(|s| &**s).collect(), help: app .data .radarr_data