diff --git a/src/app/radarr.rs b/src/app/radarr.rs index 03a1fb9..d299b50 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -16,6 +16,7 @@ pub struct RadarrData { pub version: String, pub start_time: DateTime, pub movies: StatefulTable, + pub filtered_movies: StatefulTable, pub downloads: StatefulTable, pub quality_profile_map: HashMap, pub movie_details: ScrollableText, @@ -26,6 +27,7 @@ pub struct RadarrData { pub movie_cast: StatefulTable, pub movie_crew: StatefulTable, pub collections: StatefulTable, + pub filtered_collections: StatefulTable, pub collection_movies: StatefulTable, pub main_tabs: TabState, pub movie_info_tabs: TabState, @@ -43,6 +45,8 @@ impl RadarrData { self.is_searching = false; self.search = String::default(); self.filter = String::default(); + self.filtered_movies = StatefulTable::default(); + self.filtered_collections = StatefulTable::default(); } pub fn reset_movie_info_tabs(&mut self) { @@ -68,6 +72,7 @@ impl Default for RadarrData { version: String::default(), start_time: DateTime::default(), movies: StatefulTable::default(), + filtered_movies: StatefulTable::default(), downloads: StatefulTable::default(), quality_profile_map: HashMap::default(), file_details: String::default(), @@ -78,6 +83,7 @@ impl Default for RadarrData { movie_cast: StatefulTable::default(), movie_crew: StatefulTable::default(), collections: StatefulTable::default(), + filtered_collections: StatefulTable::default(), collection_movies: StatefulTable::default(), search: String::default(), filter: String::default(), @@ -86,7 +92,7 @@ impl Default for RadarrData { TabRoute { title: "Library".to_owned(), route: ActiveRadarrBlock::Movies.into(), - help: "<↑↓> scroll | search | filter | details | ←→ change tab " + help: "<↑↓> scroll | search | filter | details | cancel filter | ←→ change tab " .to_owned(), }, TabRoute { @@ -97,7 +103,7 @@ impl Default for RadarrData { TabRoute { title: "Collections".to_owned(), route: ActiveRadarrBlock::Collections.into(), - help: "<↑↓> scroll | search | filter | details | ←→ change tab " + help: "<↑↓> scroll | search | filter | details | cancel filter | ←→ change tab " .to_owned(), }, ]), @@ -245,14 +251,27 @@ impl App { } async fn populate_movie_collection_table(&mut self) { - self.data.radarr_data.collection_movies.set_items( + let collection_movies = if !self.data.radarr_data.filtered_collections.items.is_empty() { + self + .data + .radarr_data + .filtered_collections + .current_selection_clone() + .movies + .unwrap_or_default() + } else { self .data .radarr_data .collections .current_selection_clone() .movies - .unwrap_or_default(), - ); + .unwrap_or_default() + }; + self + .data + .radarr_data + .collection_movies + .set_items(collection_movies); } } diff --git a/src/handlers/radarr_handlers/mod.rs b/src/handlers/radarr_handlers/mod.rs index 2a4eede..0708fe5 100644 --- a/src/handlers/radarr_handlers/mod.rs +++ b/src/handlers/radarr_handlers/mod.rs @@ -1,11 +1,9 @@ -use regex::Regex; - use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::radarr::ActiveRadarrBlock; use crate::handlers::radarr_handlers::collection_details_handler::CollectionDetailsHandler; use crate::handlers::radarr_handlers::movie_details_handler::MovieDetailsHandler; use crate::handlers::{handle_clear_errors, KeyEventHandler}; -use crate::models::radarr_models::Movie; +use crate::models::radarr_models::{Collection, Movie}; use crate::models::Scrollable; use crate::utils::strip_non_alphanumeric_characters; use crate::{App, Key}; @@ -55,16 +53,15 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { fn handle_scroll_up(&mut self) { match self.active_radarr_block { ActiveRadarrBlock::Collections => { - if !self.app.data.radarr_data.filter.is_empty() { - self - .app - .data - .radarr_data - .collections - .scroll_up_with_filter(|&collection| { - strip_non_alphanumeric_characters(&collection.title) - .starts_with(&self.app.data.radarr_data.filter) - }); + if !self + .app + .data + .radarr_data + .filtered_collections + .items + .is_empty() + { + self.app.data.radarr_data.filtered_collections.scroll_up(); } else { self.app.data.radarr_data.collections.scroll_up() } @@ -73,16 +70,8 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { self.app.data.radarr_data.collection_movies.scroll_up() } ActiveRadarrBlock::Movies => { - if !self.app.data.radarr_data.filter.is_empty() { - self - .app - .data - .radarr_data - .movies - .scroll_up_with_filter(|&movie| { - strip_non_alphanumeric_characters(&movie.title) - .starts_with(&self.app.data.radarr_data.filter) - }); + if !self.app.data.radarr_data.filtered_movies.items.is_empty() { + self.app.data.radarr_data.filtered_movies.scroll_up(); } else { self.app.data.radarr_data.movies.scroll_up() } @@ -95,16 +84,15 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { fn handle_scroll_down(&mut self) { match self.active_radarr_block { ActiveRadarrBlock::Collections => { - if !self.app.data.radarr_data.filter.is_empty() { - self - .app - .data - .radarr_data - .collections - .scroll_down_with_filter(|&collection| { - strip_non_alphanumeric_characters(&collection.title) - .starts_with(&self.app.data.radarr_data.filter) - }); + if !self + .app + .data + .radarr_data + .filtered_collections + .items + .is_empty() + { + self.app.data.radarr_data.filtered_collections.scroll_down(); } else { self.app.data.radarr_data.collections.scroll_down() } @@ -113,16 +101,8 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { self.app.data.radarr_data.collection_movies.scroll_down() } ActiveRadarrBlock::Movies => { - if !self.app.data.radarr_data.filter.is_empty() { - self - .app - .data - .radarr_data - .movies - .scroll_down_with_filter(|&movie| { - strip_non_alphanumeric_characters(&movie.title) - .starts_with(&self.app.data.radarr_data.filter) - }); + if !self.app.data.radarr_data.filtered_movies.items.is_empty() { + self.app.data.radarr_data.filtered_movies.scroll_down(); } else { self.app.data.radarr_data.movies.scroll_down() } @@ -192,7 +172,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { .items .iter() .position(|movie| { - strip_non_alphanumeric_characters(&movie.title).starts_with(&search_string) + strip_non_alphanumeric_characters(&movie.title).contains(&search_string) }); self.app.data.radarr_data.is_searching = false; @@ -220,7 +200,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { .items .iter() .position(|collection| { - strip_non_alphanumeric_characters(&collection.title).starts_with(&search_string) + strip_non_alphanumeric_characters(&collection.title).contains(&search_string) }); self.app.data.radarr_data.is_searching = false; @@ -236,47 +216,63 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> { } } ActiveRadarrBlock::FilterMovies => { - self.app.data.radarr_data.filter = - strip_non_alphanumeric_characters(&self.app.data.radarr_data.filter); - let filter_string = &self.app.data.radarr_data.filter; - let filter_matches = self - .app - .data - .radarr_data - .movies - .items + let filter = strip_non_alphanumeric_characters( + &self + .app + .data + .radarr_data + .filter + .drain(..) + .collect::(), + ); + let movie_list = self.app.data.radarr_data.movies.items.clone(); + let filter_matches = movie_list .iter() - .filter(|&movie| { - strip_non_alphanumeric_characters(&movie.title).starts_with(filter_string) - }) - .count(); + .filter(|&movie| strip_non_alphanumeric_characters(&movie.title).contains(&filter)) + .map(|movie| movie.to_owned()) + .collect::>(); self.app.data.radarr_data.is_searching = false; - if filter_matches > 0 { + if !filter_matches.is_empty() { self.app.pop_navigation_stack(); + self + .app + .data + .radarr_data + .filtered_movies + .set_items(filter_matches); } } ActiveRadarrBlock::FilterCollections => { - self.app.data.radarr_data.filter = - strip_non_alphanumeric_characters(&self.app.data.radarr_data.filter); - let filter_string = &self.app.data.radarr_data.filter; - let filter_matches = self - .app - .data - .radarr_data - .collections - .items + let filter = strip_non_alphanumeric_characters( + &self + .app + .data + .radarr_data + .filter + .drain(..) + .collect::(), + ); + let collection_list = self.app.data.radarr_data.collections.items.clone(); + let filter_matches = collection_list .iter() .filter(|&collection| { - strip_non_alphanumeric_characters(&collection.title).starts_with(filter_string) + strip_non_alphanumeric_characters(&collection.title).contains(&filter) }) - .count(); + .map(|collection| collection.to_owned()) + .collect::>(); self.app.data.radarr_data.is_searching = false; - if filter_matches > 0 { + if !filter_matches.is_empty() { self.app.pop_navigation_stack(); + self + .app + .data + .radarr_data + .filtered_collections + .set_items(filter_matches); } } _ => (), diff --git a/src/models/mod.rs b/src/models/mod.rs index b752e36..ee92b52 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,6 +1,7 @@ use std::cell::RefCell; -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display, Formatter}; +use log::debug; use serde::Deserialize; use tui::widgets::TableState; @@ -40,7 +41,7 @@ impl Default for StatefulTable { } } -impl StatefulTable { +impl StatefulTable { pub fn set_items(&mut self, items: Vec) { let items_len = items.len(); self.items = items; @@ -69,55 +70,6 @@ impl StatefulTable { pub fn select_index(&mut self, index: Option) { self.state.select(index); } - - pub fn scroll_up_with_filter(&mut self, filter: F) - where - F: FnMut(&&T) -> bool, - { - let filtered_list: Vec<&T> = self.items.iter().filter(filter).collect(); - - let element_position = filtered_list - .iter() - .position(|&item| item == self.current_selection()) - .unwrap(); - - if element_position == 0 { - let selected_index = self - .items - .iter() - .position(|item| item == filtered_list[filtered_list.len()]); - self.select_index(selected_index); - } else { - let selected_index = self - .items - .iter() - .position(|item| item == filtered_list[element_position - 1]); - self.select_index(selected_index) - } - } - - pub fn scroll_down_with_filter(&mut self, filter: F) - where - F: FnMut(&&T) -> bool, - { - let filtered_list: Vec<&T> = self.items.iter().filter(filter).collect(); - - let element_position = filtered_list - .iter() - .position(|&item| item == self.current_selection()) - .unwrap(); - - if element_position + 1 > filtered_list.len() { - let selected_index = self.items.iter().position(|item| item == filtered_list[0]); - self.select_index(selected_index); - } else { - let selected_index = self - .items - .iter() - .position(|item| item == filtered_list[element_position + 1]); - self.select_index(selected_index) - } - } } impl Scrollable for StatefulTable { diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 90079fe..2891526 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -1,3 +1,5 @@ +use std::ops::Deref; + use anyhow::anyhow; use indoc::formatdoc; use log::{debug, error}; @@ -195,6 +197,7 @@ impl<'a> Network<'a> { let status = get_movie_status(has_file, &app.data.radarr_data.downloads.items, id); let collection = collection.unwrap_or_default(); + debug!("title: {:?}", title); app.data.radarr_data.movie_details = ScrollableText::with_string(formatdoc!( "Title: {} @@ -394,18 +397,42 @@ impl<'a> Network<'a> { } async fn extract_movie_id(&self) -> u64 { - self + if !self .app .lock() .await .data .radarr_data - .movies - .current_selection() - .id - .clone() - .as_u64() - .unwrap() + .filtered_movies + .items + .is_empty() + { + self + .app + .lock() + .await + .data + .radarr_data + .filtered_movies + .current_selection() + .id + .clone() + .as_u64() + .unwrap() + } else { + self + .app + .lock() + .await + .data + .radarr_data + .movies + .current_selection() + .id + .clone() + .as_u64() + .unwrap() + } } async fn append_movie_id_param(&self, resource: &str) -> String { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9376fa5..341941e 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -201,18 +201,16 @@ pub struct TableProps<'a, T> { pub constraints: Vec, } -fn draw_table<'a, B, T, F, S>( +fn draw_table<'a, B, T, F>( f: &mut Frame<'_, B>, content_area: Rect, block: Block, table_props: TableProps<'a, T>, row_mapper: F, - filter_fn: S, is_loading: bool, ) where B: Backend, F: Fn(&T) -> Row<'a>, - S: FnMut(&&T) -> bool, { let TableProps { content, @@ -221,7 +219,7 @@ fn draw_table<'a, B, T, F, S>( } = table_props; if !content.items.is_empty() { - let rows = content.items.iter().filter(filter_fn).map(row_mapper); + let rows = content.items.iter().map(row_mapper); let headers = Row::new(table_headers) .style(style_default_bold()) diff --git a/src/ui/radarr_ui/collection_details_ui.rs b/src/ui/radarr_ui/collection_details_ui.rs index 5b91f89..266de8a 100644 --- a/src/ui/radarr_ui/collection_details_ui.rs +++ b/src/ui/radarr_ui/collection_details_ui.rs @@ -46,7 +46,15 @@ fn draw_collection_details(f: &mut Frame<'_, B>, app: &mut App, cont content_area, 1, ); - let collection_selection = app.data.radarr_data.collections.current_selection(); + let collection_selection = if !app.data.radarr_data.filtered_collections.items.is_empty() { + app + .data + .radarr_data + .filtered_collections + .current_selection() + } else { + app.data.radarr_data.collections.current_selection() + }; let quality_profile = app .data .radarr_data @@ -150,7 +158,6 @@ fn draw_collection_details(f: &mut Frame<'_, B>, app: &mut App, cont ]) .style(style_primary()) }, - |_| true, app.is_loading, ); } diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index 74fcdd7..a0efe17 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -2,7 +2,6 @@ use std::iter; use std::ops::Sub; use chrono::{Duration, Utc}; -use regex::Regex; use tui::backend::Backend; use tui::layout::{Alignment, Constraint, Rect}; use tui::style::{Color, Style}; @@ -13,7 +12,7 @@ use tui::Frame; use crate::app::radarr::{ActiveRadarrBlock, RadarrData}; use crate::app::App; use crate::logos::RADARR_LOGO; -use crate::models::radarr_models::{Collection, DiskSpace, DownloadRecord, Movie}; +use crate::models::radarr_models::{DiskSpace, DownloadRecord, Movie}; use crate::models::Route; use crate::ui::radarr_ui::collection_details_ui::draw_collection_details_popup; use crate::ui::radarr_ui::movie_details_ui::draw_movie_info; @@ -84,15 +83,12 @@ pub(super) fn draw_radarr_context_row(f: &mut Frame<'_, B>, app: &Ap fn draw_library(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let quality_profile_map = &app.data.radarr_data.quality_profile_map; let downloads_vec = &app.data.radarr_data.downloads.items; - let filter_fn: Box bool> = if !app.data.radarr_data.filter.is_empty() { - Box::new(|&movie| { - Regex::new(r"[^a-zA-Z0-9\s]") - .unwrap() - .replace_all(&movie.title.to_lowercase(), "") - .starts_with(&app.data.radarr_data.filter) - }) + let content = if !app.data.radarr_data.filtered_movies.items.is_empty() + && !app.data.radarr_data.is_searching + { + &mut app.data.radarr_data.filtered_movies } else { - Box::new(|_| true) + &mut app.data.radarr_data.movies }; draw_table( @@ -100,7 +96,7 @@ fn draw_library(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { area, layout_block_top_border(), TableProps { - content: &mut app.data.radarr_data.movies, + content, table_headers: vec![ "Title", "Year", @@ -141,7 +137,6 @@ fn draw_library(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { ]) .style(determine_row_style(downloads_vec, movie)) }, - filter_fn, app.is_loading, ); } @@ -166,19 +161,23 @@ fn draw_search_box(f: &mut Frame<'_, B>, app: &mut App, area: Rect) f.render_widget(input, chunks[0]); } else { - let block_title = match app.get_current_route() { + let (block_title, block_content) = match app.get_current_route() { Route::Radarr(active_radarr_block) => match active_radarr_block { - ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchCollection => "Search", - ActiveRadarrBlock::FilterMovies | ActiveRadarrBlock::FilterCollections => "Filter", - _ => "", + ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchCollection => { + ("Search", app.data.radarr_data.search.as_str()) + } + ActiveRadarrBlock::FilterMovies | ActiveRadarrBlock::FilterCollections => { + ("Filter", app.data.radarr_data.filter.as_str()) + } + _ => ("", ""), }, - _ => "", + _ => ("", ""), }; - let input = Paragraph::new(app.data.radarr_data.search.as_ref()) + let input = Paragraph::new(block_content) .style(style_default()) .block(title_block(block_title)); - show_cursor(f, chunks[0], &app.data.radarr_data.search); + show_cursor(f, chunks[0], block_content); f.render_widget(input, chunks[0]); } @@ -275,29 +274,25 @@ fn draw_downloads(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { ]) .style(style_primary()) }, - |_| true, app.is_loading, ); } fn draw_collections(f: &mut Frame<'_, B>, app: &mut App, area: Rect) { let quality_profile_map = &app.data.radarr_data.quality_profile_map; - let filter_fn: Box bool> = if !app.data.radarr_data.filter.is_empty() { - Box::new(|&collection| { - Regex::new(r"[^a-zA-Z0-9\s]") - .unwrap() - .replace_all(&collection.title.to_lowercase(), "") - .starts_with(&app.data.radarr_data.filter) - }) + let content = if !app.data.radarr_data.filtered_collections.items.is_empty() + && !app.data.radarr_data.is_searching + { + &mut app.data.radarr_data.filtered_collections } else { - Box::new(|_| true) + &mut app.data.radarr_data.collections }; draw_table( f, area, layout_block_top_border(), TableProps { - content: &mut app.data.radarr_data.collections, + content, table_headers: vec![ "Collection", "Search on Add?", @@ -324,7 +319,6 @@ fn draw_collections(f: &mut Frame<'_, B>, app: &mut App, area: Rect) ]) .style(style_primary()) }, - filter_fn, app.is_loading, ); } diff --git a/src/ui/radarr_ui/movie_details_ui.rs b/src/ui/radarr_ui/movie_details_ui.rs index 5e45606..a141d61 100644 --- a/src/ui/radarr_ui/movie_details_ui.rs +++ b/src/ui/radarr_ui/movie_details_ui.rs @@ -196,7 +196,6 @@ fn draw_movie_history( ]) .style(style_success()) }, - |_| true, app.is_loading, ); } @@ -229,7 +228,6 @@ fn draw_movie_cast( ]) .style(style_success()) }, - |_| true, app.is_loading, ) } @@ -264,7 +262,6 @@ fn draw_movie_crew( ]) .style(style_success()) }, - |_| true, app.is_loading, ); } diff --git a/src/ui/utils.rs b/src/ui/utils.rs index 8b9e457..97a10f0 100644 --- a/src/ui/utils.rs +++ b/src/ui/utils.rs @@ -206,6 +206,6 @@ pub fn line_gauge_with_label(title: &str, ratio: f64) -> LineGauge { .label(Spans::from(format!("{}: {:.0}%", title, ratio * 100.0))) } -pub fn show_cursor(f: &mut Frame<'_, B>, area: Rect, string: &String) { +pub fn show_cursor(f: &mut Frame<'_, B>, area: Rect, string: &str) { f.set_cursor(area.x + string.len() as u16 + 1, area.y + 1); }