From c8a06f360166dad756b0230366614e5681052437 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Mon, 5 Jan 2026 09:49:03 -0700 Subject: [PATCH] refactored managarr table initializer so a mutable app reference can be passed --- src/app/mod.rs | 2 +- src/main.rs | 16 +- src/ui/mod.rs | 21 +- src/ui/radarr_ui/blocklist/mod.rs | 10 +- .../collections/collection_details_ui.rs | 31 +- src/ui/radarr_ui/collections/mod.rs | 77 ++--- src/ui/radarr_ui/downloads/mod.rs | 9 +- src/ui/radarr_ui/indexers/mod.rs | 13 +- .../indexers/test_all_indexers_ui.rs | 6 +- src/ui/radarr_ui/library/add_movie_ui.rs | 33 +- src/ui/radarr_ui/library/mod.rs | 90 +++--- src/ui/radarr_ui/library/movie_details_ui.rs | 295 +++++++++--------- src/ui/radarr_ui/root_folders/mod.rs | 4 +- src/ui/radarr_ui/system/mod.rs | 19 +- src/ui/radarr_ui/system/system_details_ui.rs | 13 +- ..._tests__radarr_ui_renders_library_tab.snap | 6 +- ...rr_ui_renders_library_tab_error_popup.snap | 62 ++-- ...arr_ui_renders_library_tab_with_error.snap | 6 +- src/ui/sonarr_ui/blocklist/mod.rs | 7 +- src/ui/sonarr_ui/downloads/mod.rs | 7 +- src/ui/sonarr_ui/history/mod.rs | 46 +-- src/ui/sonarr_ui/indexers/mod.rs | 11 +- .../indexers/test_all_indexers_ui.rs | 7 +- src/ui/sonarr_ui/library/add_series_ui.rs | 40 ++- .../sonarr_ui/library/episode_details_ui.rs | 83 ++--- src/ui/sonarr_ui/library/mod.rs | 87 +++--- src/ui/sonarr_ui/library/season_details_ui.rs | 191 ++++++------ src/ui/sonarr_ui/library/series_details_ui.rs | 81 +++-- src/ui/sonarr_ui/root_folders/mod.rs | 4 +- src/ui/sonarr_ui/system/mod.rs | 19 +- src/ui/sonarr_ui/system/system_details_ui.rs | 30 +- src/ui/widgets/loading_block.rs | 25 +- src/ui/widgets/managarr_table.rs | 219 +++++++------ src/ui/widgets/managarr_table_tests.rs | 141 +++++---- src/ui/widgets/popup.rs | 19 +- 35 files changed, 955 insertions(+), 775 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index d12bf9b..efb166d 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -273,10 +273,10 @@ impl App<'_> { config: Some(ServarrConfig::default()), }, ]), + ..App::default() } } - pub fn test_default_fully_populated() -> Self { App { data: Data { diff --git a/src/main.rs b/src/main.rs index 3de2039..14360b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,38 +3,38 @@ extern crate assertables; use anyhow::Result; -use clap::{crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser}; +use clap::{CommandFactory, Parser, crate_authors, crate_description, crate_name, crate_version}; use clap_complete::generate; use crossterm::execute; use crossterm::terminal::{ - disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, + EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, }; use log::{debug, error, warn}; use network::NetworkTrait; -use ratatui::backend::CrosstermBackend; use ratatui::Terminal; +use ratatui::backend::CrosstermBackend; use reqwest::Client; use std::panic::PanicHookInfo; use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Instant; use std::{io, panic, process}; use tokio::select; use tokio::sync::mpsc::Receiver; -use tokio::sync::{mpsc, Mutex}; +use tokio::sync::{Mutex, mpsc}; use tokio_util::sync::CancellationToken; use utils::{ build_network_client, load_config, start_cli_no_spinner, start_cli_with_spinner, tail_logs, }; -use crate::app::{log_and_print_error, App}; +use crate::app::{App, log_and_print_error}; use crate::cli::Command; -use crate::event::input_event::{Events, InputEvent}; use crate::event::Key; +use crate::event::input_event::{Events, InputEvent}; use crate::network::{Network, NetworkEvent}; use crate::ui::theme::{Theme, ThemeDefinitionsWrapper}; -use crate::ui::{ui, THEME}; +use crate::ui::{THEME, ui}; use crate::utils::load_theme_config; mod app; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 218ce77..af78eab 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -165,18 +165,15 @@ pub fn draw_help_popup(f: &mut Frame<'_>, app: &mut App<'_>) { ]) .primary() }; - let keymapping_table = ManagarrTable::new( - Some(app.keymapping_table.as_mut().unwrap()), - keymap_row_mapping, - ) - .block(title_block("Keybindings")) - .loading(app.is_loading) - .headers(["Key", "Alt Key", "Description"]) - .constraints([ - Constraint::Ratio(1, 3), - Constraint::Ratio(1, 3), - Constraint::Ratio(1, 3), - ]); + let keymapping_table = + ManagarrTable::new(app, |app| app.keymapping_table.as_mut(), keymap_row_mapping) + .block(title_block("Keybindings")) + .headers(["Key", "Alt Key", "Description"]) + .constraints([ + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + ]); f.render_widget(Clear, table_area); f.render_widget(background_block(), table_area); f.render_widget(keymapping_table, table_area); diff --git a/src/ui/radarr_ui/blocklist/mod.rs b/src/ui/radarr_ui/blocklist/mod.rs index 4301221..ea48057 100644 --- a/src/ui/radarr_ui/blocklist/mod.rs +++ b/src/ui/radarr_ui/blocklist/mod.rs @@ -81,6 +81,8 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } else { app.data.radarr_data.blocklist.current_selection().clone() }; + let ui_scroll_tick_count = app.ui_scroll_tick_count; + let is_sorting = active_radarr_block == ActiveRadarrBlock::BlocklistSortPrompt; let blocklist_row_mapping = |blocklist_item: &BlocklistItem| { let BlocklistItem { @@ -96,7 +98,7 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { movie.title.scroll_left_or_reset( get_width_from_percentage(area, 20), current_selection == *blocklist_item, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); let languages_string = languages @@ -125,12 +127,12 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .primary() }; let blocklist_table = ManagarrTable::new( - Some(&mut app.data.radarr_data.blocklist), + app, + |app| Some(&mut app.data.radarr_data.blocklist), blocklist_row_mapping, ) .block(layout_block_top_border()) - .loading(app.is_loading) - .sorting(active_radarr_block == ActiveRadarrBlock::BlocklistSortPrompt) + .sorting(is_sorting) .headers([ "Movie Title", "Source Title", diff --git a/src/ui/radarr_ui/collections/collection_details_ui.rs b/src/ui/radarr_ui/collections/collection_details_ui.rs index 9567523..312e79e 100644 --- a/src/ui/radarr_ui/collections/collection_details_ui.rs +++ b/src/ui/radarr_ui/collections/collection_details_ui.rs @@ -56,7 +56,8 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) Layout::vertical([Constraint::Percentage(25), Constraint::Fill(0)]) .margin(1) .areas(area); - let collection_selection = app.data.radarr_data.collections.current_selection(); + + let collection_selection = app.data.radarr_data.collections.current_selection().clone(); let quality_profile = app .data .radarr_data @@ -74,15 +75,19 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) .current_selection() .clone() }; - let movie_row_mapper = |movie: &CollectionMovie| { - let in_library = if app - .data - .radarr_data - .movies - .items - .iter() - .any(|mov| mov.tmdb_id == movie.tmdb_id) - { + + let ui_scroll_tick_count = app.ui_scroll_tick_count; + let movie_tmdb_ids: Vec<_> = app + .data + .radarr_data + .movies + .items + .iter() + .map(|mov| mov.tmdb_id) + .collect(); + + let movie_row_mapper = move |movie: &CollectionMovie| { + let in_library = if movie_tmdb_ids.contains(&movie.tmdb_id) { "โœ”" } else { "" @@ -90,7 +95,7 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) movie.title.scroll_left_or_reset( get_width_from_percentage(table_area, 20), current_selection == *movie, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); let (hours, minutes) = convert_runtime(movie.runtime); let imdb_rating = movie @@ -179,11 +184,11 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) .block(borderless_block()) .wrap(Wrap { trim: false }); let movies_table = ManagarrTable::new( - Some(&mut app.data.radarr_data.collection_movies), + app, + |app| Some(&mut app.data.radarr_data.collection_movies), movie_row_mapper, ) .block(layout_block_top_border_with_title(title_style("Movies"))) - .loading(app.is_loading) .headers([ "โœ”", "Title", diff --git a/src/ui/radarr_ui/collections/mod.rs b/src/ui/radarr_ui/collections/mod.rs index 63ed8a8..b0a0571 100644 --- a/src/ui/radarr_ui/collections/mod.rs +++ b/src/ui/radarr_ui/collections/mod.rs @@ -63,14 +63,25 @@ pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) } else { Collection::default() }; - let quality_profile_map = &app.data.radarr_data.quality_profile_map; - let content = Some(&mut app.data.radarr_data.collections); + let quality_profile_map = app.data.radarr_data.quality_profile_map.clone(); + let ui_scroll_tick_count = app.ui_scroll_tick_count; + let is_loading = app.is_loading + || app.data.radarr_data.movies.is_empty() + || app.data.radarr_data.quality_profile_map.is_empty(); + let is_sorting = active_radarr_block == ActiveRadarrBlock::CollectionsSortPrompt; + let is_searching = active_radarr_block == ActiveRadarrBlock::SearchCollection; + let search_produced_empty_results = + active_radarr_block == ActiveRadarrBlock::SearchCollectionError; + let is_filtering = active_radarr_block == ActiveRadarrBlock::FilterCollections; + let filter_produced_empty_results = + active_radarr_block == ActiveRadarrBlock::FilterCollectionsError; + let collection_row_mapping = |collection: &Collection| { let number_of_movies = collection.movies.as_ref().unwrap_or(&Vec::new()).len(); collection.title.scroll_left_or_reset( get_width_from_percentage(area, 25), *collection == current_selection, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); let monitored = if collection.monitored { "๐Ÿท" } else { "" }; let search_on_add = if collection.search_on_add { @@ -100,38 +111,34 @@ pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) ]) .primary() }; - let collections_table = ManagarrTable::new(content, collection_row_mapping) - .loading( - app.is_loading - || app.data.radarr_data.movies.is_empty() - || app.data.radarr_data.quality_profile_map.is_empty(), - ) - .block(layout_block_top_border()) - .sorting(active_radarr_block == ActiveRadarrBlock::CollectionsSortPrompt) - .searching(active_radarr_block == ActiveRadarrBlock::SearchCollection) - .search_produced_empty_results( - active_radarr_block == ActiveRadarrBlock::SearchCollectionError, - ) - .filtering(active_radarr_block == ActiveRadarrBlock::FilterCollections) - .filter_produced_empty_results( - active_radarr_block == ActiveRadarrBlock::FilterCollectionsError, - ) - .headers([ - "Collection", - "Number of Movies", - "Root Folder Path", - "Quality Profile", - "Search on Add", - "Monitored", - ]) - .constraints([ - Constraint::Percentage(25), - Constraint::Percentage(15), - Constraint::Percentage(15), - Constraint::Percentage(15), - Constraint::Percentage(15), - Constraint::Percentage(15), - ]); + let collections_table = ManagarrTable::new( + app, + |app| Some(&mut app.data.radarr_data.collections), + collection_row_mapping, + ) + .loading(is_loading) + .block(layout_block_top_border()) + .sorting(is_sorting) + .searching(is_searching) + .search_produced_empty_results(search_produced_empty_results) + .filtering(is_filtering) + .filter_produced_empty_results(filter_produced_empty_results) + .headers([ + "Collection", + "Number of Movies", + "Root Folder Path", + "Quality Profile", + "Search on Add", + "Monitored", + ]) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(15), + Constraint::Percentage(15), + Constraint::Percentage(15), + Constraint::Percentage(15), + Constraint::Percentage(15), + ]); if [ ActiveRadarrBlock::SearchCollection, diff --git a/src/ui/radarr_ui/downloads/mod.rs b/src/ui/radarr_ui/downloads/mod.rs index 2e52d7b..70a3f46 100644 --- a/src/ui/radarr_ui/downloads/mod.rs +++ b/src/ui/radarr_ui/downloads/mod.rs @@ -71,8 +71,9 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } else { app.data.radarr_data.downloads.current_selection().clone() }; + let ui_scroll_tick_count = app.ui_scroll_tick_count; - let downloads_row_mapping = |download_record: &DownloadRecord| { + let downloads_row_mapping = move |download_record: &DownloadRecord| { let DownloadRecord { title, size, @@ -87,7 +88,7 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { output_path.as_ref().unwrap().scroll_left_or_reset( get_width_from_percentage(area, 18), current_selection == *download_record, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); } @@ -114,11 +115,11 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .primary() }; let downloads_table = ManagarrTable::new( - Some(&mut app.data.radarr_data.downloads), + app, + |app| Some(&mut app.data.radarr_data.downloads), downloads_row_mapping, ) .block(layout_block_top_border()) - .loading(app.is_loading) .headers([ "Title", "Percent Complete", diff --git a/src/ui/radarr_ui/indexers/mod.rs b/src/ui/radarr_ui/indexers/mod.rs index bc63c90..afa6661 100644 --- a/src/ui/radarr_ui/indexers/mod.rs +++ b/src/ui/radarr_ui/indexers/mod.rs @@ -111,7 +111,9 @@ impl DrawUi for IndexersUi { } fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - let indexers_row_mapping = |indexer: &'_ Indexer| { + let tags_map = app.data.radarr_data.tags_map.clone(); + + let indexers_row_mapping = move |indexer: &'_ Indexer| { let Indexer { name, enable_rss, @@ -136,10 +138,7 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let tags: String = tags .iter() .map(|tag_id| { - app - .data - .radarr_data - .tags_map + tags_map .get_by_left(&tag_id.as_i64().unwrap()) .unwrap_or(&empty_tag) .clone() @@ -158,11 +157,11 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .primary() }; let indexers_table = ManagarrTable::new( - Some(&mut app.data.radarr_data.indexers), + app, + |app| Some(&mut app.data.radarr_data.indexers), indexers_row_mapping, ) .block(layout_block_top_border()) - .loading(app.is_loading) .headers([ "Indexer", "RSS", diff --git a/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs b/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs index 734fc15..7328657 100644 --- a/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs +++ b/src/ui/radarr_ui/indexers/test_all_indexers_ui.rs @@ -33,6 +33,7 @@ impl DrawUi for TestAllIndexersUi { fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let is_loading = app.is_loading || app.data.radarr_data.indexer_test_all_results.is_none(); let block = title_block("Test All Indexers"); + let ui_scroll_tick_count = app.ui_scroll_tick_count; let current_selection = if let Some(test_all_results) = app.data.radarr_data.indexer_test_all_results.as_ref() { @@ -45,7 +46,7 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are result.validation_failures.scroll_left_or_reset( get_width_from_percentage(area, 86), *result == current_selection, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); let pass_fail = if result.is_valid { "โœ”" } else { "โŒ" }; let row = Row::new(vec![ @@ -62,7 +63,8 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are }; let indexers_test_results_table = ManagarrTable::new( - app.data.radarr_data.indexer_test_all_results.as_mut(), + app, + |app| app.data.radarr_data.indexer_test_all_results.as_mut(), test_results_row_mapping, ) .loading(is_loading) diff --git a/src/ui/radarr_ui/library/add_movie_ui.rs b/src/ui/radarr_ui/library/add_movie_ui.rs index f3ad937..748dacc 100644 --- a/src/ui/radarr_ui/library/add_movie_ui.rs +++ b/src/ui/radarr_ui/library/add_movie_ui.rs @@ -80,13 +80,14 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Layout::vertical([Constraint::Length(3), Constraint::Fill(0)]) .margin(1) .areas(area); - let block_content = &app + let block_content = app .data .radarr_data .add_movie_search .as_ref() .expect("add_movie_search must be populated") - .text; + .text + .clone(); let offset = app .data .radarr_data @@ -95,6 +96,16 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .expect("add_movie_search must be populated") .offset .load(Ordering::SeqCst); + let ui_scroll_tick_count = app.ui_scroll_tick_count; + let library_tmdb_ids: Vec = app + .data + .radarr_data + .movies + .items + .iter() + .map(|m| m.tmdb_id) + .collect(); + let search_results_row_mapping = |movie: &AddMovieSearchResult| { let (hours, minutes) = convert_runtime(movie.runtime); let imdb_rating = movie @@ -123,14 +134,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } else { format!("{rotten_tomatoes_rating}%") }; - let in_library = if app - .data - .radarr_data - .movies - .items - .iter() - .any(|mov| mov.tmdb_id == movie.tmdb_id) - { + let in_library = if library_tmdb_ids.contains(&movie.tmdb_id) { "โœ”" } else { "" @@ -139,7 +143,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { movie.title.scroll_left_or_reset( get_width_from_percentage(area, 27), *movie == current_selection, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); Row::new(vec![ @@ -157,7 +161,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { if let Route::Radarr(active_radarr_block, _) = app.get_current_route() { match active_radarr_block { ActiveRadarrBlock::AddMovieSearchInput => { - let search_box = InputBox::new(block_content) + let search_box = InputBox::new(&block_content) .offset(offset) .block(title_block_centered("Add Movie")); @@ -181,7 +185,8 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { | ActiveRadarrBlock::AddMovieAlreadyInLibrary | ActiveRadarrBlock::AddMovieTagsInput => { let search_results_table = ManagarrTable::new( - app.data.radarr_data.add_searched_movies.as_mut(), + app, + |app| app.data.radarr_data.add_searched_movies.as_mut(), search_results_row_mapping, ) .loading(is_loading) @@ -212,7 +217,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } f.render_widget( - InputBox::new(block_content) + InputBox::new(&block_content) .offset(offset) .block(title_block_centered("Add Movie")), search_box_area, diff --git a/src/ui/radarr_ui/library/mod.rs b/src/ui/radarr_ui/library/mod.rs index ad0b88d..6b3f43c 100644 --- a/src/ui/radarr_ui/library/mod.rs +++ b/src/ui/radarr_ui/library/mod.rs @@ -1,11 +1,12 @@ +use ratatui::Frame; use ratatui::layout::{Constraint, Rect}; use ratatui::widgets::{Cell, Row}; -use ratatui::Frame; use crate::app::App; +use crate::models::Route; use crate::models::radarr_models::Movie; use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, LIBRARY_BLOCKS}; -use crate::models::Route; +use crate::ui::DrawUi; use crate::ui::radarr_ui::decorate_with_row_style; use crate::ui::radarr_ui::library::add_movie_ui::AddMovieUi; use crate::ui::radarr_ui::library::delete_movie_ui::DeleteMovieUi; @@ -15,7 +16,6 @@ use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::popup::{Popup, Size}; -use crate::ui::DrawUi; use crate::utils::{convert_runtime, convert_to_gb}; mod add_movie_ui; @@ -83,16 +83,21 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } else { Movie::default() }; - let quality_profile_map = &app.data.radarr_data.quality_profile_map; - let tags_map = &app.data.radarr_data.tags_map; - let downloads_vec = &app.data.radarr_data.downloads.items; - let content = Some(&mut app.data.radarr_data.movies); + let quality_profile_map = app.data.radarr_data.quality_profile_map.clone(); + let tags_map = app.data.radarr_data.tags_map.clone(); + let downloads_vec = app.data.radarr_data.downloads.items.clone(); + let ui_scroll_tick_count = app.ui_scroll_tick_count; + let is_sorting = active_radarr_block == ActiveRadarrBlock::MoviesSortPrompt; + let is_searching = active_radarr_block == ActiveRadarrBlock::SearchMovie; + let search_produced_empty_results = active_radarr_block == ActiveRadarrBlock::SearchMovieError; + let is_filtering = active_radarr_block == ActiveRadarrBlock::FilterMovies; + let filter_produced_empty_results = active_radarr_block == ActiveRadarrBlock::FilterMoviesError; let library_table_row_mapping = |movie: &Movie| { movie.title.scroll_left_or_reset( get_width_from_percentage(area, 27), *movie == current_selection, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); let monitored = if movie.monitored { "๐Ÿท" } else { "" }; let studio = movie.studio.clone().unwrap_or_default(); @@ -114,7 +119,7 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .join(", "); decorate_with_row_style( - downloads_vec, + &downloads_vec, movie, Row::new(vec![ Cell::from(movie.title.to_string()), @@ -130,38 +135,41 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]), ) }; - let library_table = ManagarrTable::new(content, library_table_row_mapping) - .block(layout_block_top_border()) - .loading(app.is_loading) - .sorting(active_radarr_block == ActiveRadarrBlock::MoviesSortPrompt) - .searching(active_radarr_block == ActiveRadarrBlock::SearchMovie) - .search_produced_empty_results(active_radarr_block == ActiveRadarrBlock::SearchMovieError) - .filtering(active_radarr_block == ActiveRadarrBlock::FilterMovies) - .filter_produced_empty_results(active_radarr_block == ActiveRadarrBlock::FilterMoviesError) - .headers([ - "Title", - "Year", - "Studio", - "Runtime", - "Rating", - "Language", - "Size", - "Quality Profile", - "Monitored", - "Tags", - ]) - .constraints([ - Constraint::Percentage(27), - Constraint::Percentage(4), - Constraint::Percentage(17), - Constraint::Percentage(6), - Constraint::Percentage(6), - Constraint::Percentage(6), - Constraint::Percentage(6), - Constraint::Percentage(10), - Constraint::Percentage(6), - Constraint::Percentage(12), - ]); + let library_table = ManagarrTable::new( + app, + |app| Some(&mut app.data.radarr_data.movies), + library_table_row_mapping, + ) + .block(layout_block_top_border()) + .sorting(is_sorting) + .searching(is_searching) + .search_produced_empty_results(search_produced_empty_results) + .filtering(is_filtering) + .filter_produced_empty_results(filter_produced_empty_results) + .headers([ + "Title", + "Year", + "Studio", + "Runtime", + "Rating", + "Language", + "Size", + "Quality Profile", + "Monitored", + "Tags", + ]) + .constraints([ + Constraint::Percentage(27), + Constraint::Percentage(4), + Constraint::Percentage(17), + Constraint::Percentage(6), + Constraint::Percentage(6), + Constraint::Percentage(6), + Constraint::Percentage(6), + Constraint::Percentage(10), + Constraint::Percentage(6), + Constraint::Percentage(12), + ]); if [ ActiveRadarrBlock::SearchMovie, diff --git a/src/ui/radarr_ui/library/movie_details_ui.rs b/src/ui/radarr_ui/library/movie_details_ui.rs index 7823d2b..e4ff346 100644 --- a/src/ui/radarr_ui/library/movie_details_ui.rs +++ b/src/ui/radarr_ui/library/movie_details_ui.rs @@ -10,7 +10,7 @@ use serde_json::Number; use crate::app::App; use crate::models::Route; use crate::models::radarr_models::{Credit, MovieHistoryItem, RadarrRelease}; -use crate::models::servarr_data::radarr::modals::MovieDetailsModal; + use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, MOVIE_DETAILS_BLOCKS}; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{ @@ -225,135 +225,138 @@ fn draw_movie_details(f: &mut Frame<'_>, app: &App<'_>, area: Rect) { } fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - if let Some(movie_details_modal) = app.data.radarr_data.movie_details_modal.as_mut() { - let current_selection = if movie_details_modal.movie_history.items.is_empty() { + let current_selection = if let Some(modal) = app.data.radarr_data.movie_details_modal.as_ref() { + if modal.movie_history.items.is_empty() { MovieHistoryItem::default() } else { - movie_details_modal - .movie_history - .current_selection() - .clone() - }; - let history_row_mapping = |movie_history_item: &MovieHistoryItem| { - let MovieHistoryItem { - source_title, - quality, - languages, - date, - event_type, - } = movie_history_item; + modal.movie_history.current_selection().clone() + } + } else { + MovieHistoryItem::default() + }; + let ui_scroll_tick_count = app.ui_scroll_tick_count; - movie_history_item.source_title.scroll_left_or_reset( - get_width_from_percentage(area, 34), - current_selection == *movie_history_item, - app.ui_scroll_tick_count == 0, - ); + let history_row_mapping = |movie_history_item: &MovieHistoryItem| { + let MovieHistoryItem { + source_title, + quality, + languages, + date, + event_type, + } = movie_history_item; - Row::new(vec![ - Cell::from(source_title.to_string()), - Cell::from(event_type.to_owned()), - Cell::from( - languages - .iter() - .map(|language| language.name.to_owned()) - .collect::>() - .join(","), - ), - Cell::from(quality.quality.name.to_owned()), - Cell::from(date.to_string()), - ]) - .primary() - }; - let history_table = ManagarrTable::new( - Some(&mut movie_details_modal.movie_history), - history_row_mapping, - ) - .block(layout_block_top_border()) - .loading(app.is_loading) - .headers(["Source Title", "Event Type", "Languages", "Quality", "Date"]) - .constraints([ - Constraint::Percentage(34), - Constraint::Percentage(17), - Constraint::Percentage(14), - Constraint::Percentage(14), - Constraint::Percentage(21), - ]); + movie_history_item.source_title.scroll_left_or_reset( + get_width_from_percentage(area, 34), + current_selection == *movie_history_item, + ui_scroll_tick_count == 0, + ); - f.render_widget(history_table, area); - } + Row::new(vec![ + Cell::from(source_title.to_string()), + Cell::from(event_type.to_owned()), + Cell::from( + languages + .iter() + .map(|language| language.name.to_owned()) + .collect::>() + .join(","), + ), + Cell::from(quality.quality.name.to_owned()), + Cell::from(date.to_string()), + ]) + .primary() + }; + let history_table = ManagarrTable::new( + app, + |app| { + app + .data + .radarr_data + .movie_details_modal + .as_mut() + .map(|m| &mut m.movie_history) + }, + history_row_mapping, + ) + .block(layout_block_top_border()) + .headers(["Source Title", "Event Type", "Languages", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(34), + Constraint::Percentage(17), + Constraint::Percentage(14), + Constraint::Percentage(14), + Constraint::Percentage(21), + ]); + + f.render_widget(history_table, area); } fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - match app.data.radarr_data.movie_details_modal.as_mut() { - Some(movie_details_modal) if !app.is_loading => { - let cast_row_mapping = |cast_member: &Credit| { - let Credit { - person_name, - character, - .. - } = cast_member; + let cast_row_mapping = |cast_member: &Credit| { + let Credit { + person_name, + character, + .. + } = cast_member; - Row::new(vec![ - Cell::from(person_name.to_owned()), - Cell::from(character.clone().unwrap_or_default()), - ]) - .primary() - }; - let content = Some(&mut movie_details_modal.movie_cast); - let cast_table = ManagarrTable::new(content, cast_row_mapping) - .block(layout_block_top_border()) - .loading(app.is_loading) - .headers(["Cast Member", "Character"]) - .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); + Row::new(vec![ + Cell::from(person_name.to_owned()), + Cell::from(character.clone().unwrap_or_default()), + ]) + .primary() + }; + let cast_table = ManagarrTable::new( + app, + |app| { + app + .data + .radarr_data + .movie_details_modal + .as_mut() + .map(|m| &mut m.movie_cast) + }, + cast_row_mapping, + ) + .block(layout_block_top_border()) + .headers(["Cast Member", "Character"]) + .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]); - f.render_widget(cast_table, area); - } - _ => f.render_widget( - LoadingBlock::new( - app.is_loading || app.data.radarr_data.movie_details_modal.is_none(), - layout_block_top_border(), - ), - area, - ), - } + f.render_widget(cast_table, area); } fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { - match app.data.radarr_data.movie_details_modal.as_mut() { - Some(movie_details_modal) if !app.is_loading => { - let crew_row_mapping = |crew_member: &Credit| { - let Credit { - person_name, - job, - department, - .. - } = crew_member; + let crew_row_mapping = |crew_member: &Credit| { + let Credit { + person_name, + job, + department, + .. + } = crew_member; - Row::new(vec![ - Cell::from(person_name.to_owned()), - Cell::from(job.clone().unwrap_or_default()), - Cell::from(department.clone().unwrap_or_default()), - ]) - .primary() - }; - let content = Some(&mut movie_details_modal.movie_crew); - let crew_table = ManagarrTable::new(content, crew_row_mapping) - .block(layout_block_top_border()) - .loading(app.is_loading) - .headers(["Crew Member", "Job", "Department"]) - .constraints(iter::repeat_n(Constraint::Ratio(1, 3), 3)); + Row::new(vec![ + Cell::from(person_name.to_owned()), + Cell::from(job.clone().unwrap_or_default()), + Cell::from(department.clone().unwrap_or_default()), + ]) + .primary() + }; + let crew_table = ManagarrTable::new( + app, + |app| { + app + .data + .radarr_data + .movie_details_modal + .as_mut() + .map(|m| &mut m.movie_crew) + }, + crew_row_mapping, + ) + .block(layout_block_top_border()) + .headers(["Crew Member", "Job", "Department"]) + .constraints(iter::repeat_n(Constraint::Ratio(1, 3), 3)); - f.render_widget(crew_table, area); - } - - _ => f.render_widget( - LoadingBlock::new( - app.is_loading || app.data.radarr_data.movie_details_modal.is_none(), - layout_block_top_border(), - ), - area, - ), - } + f.render_widget(crew_table, area); } fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { @@ -369,16 +372,9 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { _ => (RadarrRelease::default(), true), }; let current_route = app.get_current_route(); - let mut default_movie_details_modal = MovieDetailsModal::default(); - let content = Some( - &mut app - .data - .radarr_data - .movie_details_modal - .as_mut() - .unwrap_or(&mut default_movie_details_modal) - .movie_releases, - ); + let ui_scroll_tick_count = app.ui_scroll_tick_count; + let is_sorting = active_radarr_block == ActiveRadarrBlock::ManualSearchSortPrompt; + let releases_row_mapping = |release: &RadarrRelease| { let RadarrRelease { protocol, @@ -398,7 +394,7 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { get_width_from_percentage(area, 30), current_selection == *release && current_route != ActiveRadarrBlock::ManualSearchConfirmPrompt.into(), - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); let size = convert_to_gb(*size); let rejected_str = if *rejected { "โ›”" } else { "" }; @@ -443,24 +439,35 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]) .primary() }; - let releases_table = ManagarrTable::new(content, releases_row_mapping) - .block(layout_block_top_border()) - .loading(app.is_loading || is_empty) - .sorting(active_radarr_block == ActiveRadarrBlock::ManualSearchSortPrompt) - .headers([ - "Source", "Age", "โ›”", "Title", "Indexer", "Size", "Peers", "Language", "Quality", - ]) - .constraints([ - Constraint::Length(9), - Constraint::Length(10), - Constraint::Length(5), - Constraint::Percentage(30), - Constraint::Percentage(18), - Constraint::Length(12), - Constraint::Length(12), - Constraint::Percentage(7), - Constraint::Percentage(10), - ]); + let releases_table = ManagarrTable::new( + app, + |app| { + app + .data + .radarr_data + .movie_details_modal + .as_mut() + .map(|m| &mut m.movie_releases) + }, + releases_row_mapping, + ) + .block(layout_block_top_border()) + .loading(is_empty) + .sorting(is_sorting) + .headers([ + "Source", "Age", "โ›”", "Title", "Indexer", "Size", "Peers", "Language", "Quality", + ]) + .constraints([ + Constraint::Length(9), + Constraint::Length(10), + Constraint::Length(5), + Constraint::Percentage(30), + Constraint::Percentage(18), + Constraint::Length(12), + Constraint::Length(12), + Constraint::Percentage(7), + Constraint::Percentage(10), + ]); f.render_widget(releases_table, area); } diff --git a/src/ui/radarr_ui/root_folders/mod.rs b/src/ui/radarr_ui/root_folders/mod.rs index 9c5ecfe..4ff3b0d 100644 --- a/src/ui/radarr_ui/root_folders/mod.rs +++ b/src/ui/radarr_ui/root_folders/mod.rs @@ -83,11 +83,11 @@ fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { }; let root_folders_table = ManagarrTable::new( - Some(&mut app.data.radarr_data.root_folders), + app, + |app| Some(&mut app.data.radarr_data.root_folders), root_folders_row_mapping, ) .block(layout_block_top_border()) - .loading(app.is_loading) .headers(["Path", "Free Space", "Unmapped Folders"]) .constraints([ Constraint::Ratio(3, 5), diff --git a/src/ui/radarr_ui/system/mod.rs b/src/ui/radarr_ui/system/mod.rs index 2939924..3a0e324 100644 --- a/src/ui/radarr_ui/system/mod.rs +++ b/src/ui/radarr_ui/system/mod.rs @@ -96,12 +96,15 @@ fn draw_tasks(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]) .primary() }; - let tasks_table = ManagarrTable::new(Some(&mut app.data.radarr_data.tasks), tasks_row_mapping) - .block(title_block("Tasks")) - .loading(app.is_loading) - .highlight_rows(false) - .headers(TASK_TABLE_HEADERS) - .constraints(TASK_TABLE_CONSTRAINTS); + let tasks_table = ManagarrTable::new( + app, + |app| Some(&mut app.data.radarr_data.tasks), + tasks_row_mapping, + ) + .block(title_block("Tasks")) + .highlight_rows(false) + .headers(TASK_TABLE_HEADERS) + .constraints(TASK_TABLE_CONSTRAINTS); f.render_widget(tasks_table, area); } @@ -144,11 +147,11 @@ pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rec .primary() }; let events_table = ManagarrTable::new( - Some(&mut app.data.radarr_data.queued_events), + app, + |app| Some(&mut app.data.radarr_data.queued_events), events_row_mapping, ) .block(title_block("Queued Events")) - .loading(app.is_loading) .highlight_rows(false) .headers(["Trigger", "Status", "Name", "Queued", "Started", "Duration"]) .constraints([ diff --git a/src/ui/radarr_ui/system/system_details_ui.rs b/src/ui/radarr_ui/system/system_details_ui.rs index bc8db27..08d8b60 100644 --- a/src/ui/radarr_ui/system/system_details_ui.rs +++ b/src/ui/radarr_ui/system/system_details_ui.rs @@ -93,11 +93,14 @@ fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]) .primary() }; - let tasks_table = ManagarrTable::new(Some(&mut app.data.radarr_data.tasks), tasks_row_mapping) - .loading(app.is_loading) - .margin(1) - .headers(TASK_TABLE_HEADERS) - .constraints(TASK_TABLE_CONSTRAINTS); + let tasks_table = ManagarrTable::new( + app, + |app| Some(&mut app.data.radarr_data.tasks), + tasks_row_mapping, + ) + .margin(1) + .headers(TASK_TABLE_HEADERS) + .constraints(TASK_TABLE_CONSTRAINTS); f.render_widget(title_block("Tasks"), area); f.render_widget(tasks_table, area); diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab.snap index 8f537b7..c58d1c0 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab.snap @@ -17,9 +17,9 @@ expression: output โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏโ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏโ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ•ญ Movies โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ Library โ”‚ Collections โ”‚ Downloads โ”‚ Blocklist โ”‚ Root Folders โ”‚ Indexers โ”‚ System โ”‚ -โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ -โ”‚ Title โ–ผ Year Studio Runtime Rating Language Size Quality Profile Monitored Tags โ”‚ -โ”‚=> Test 2023 21st Century Alex 2h 0m R English 3.30 GB HD - 1080p ๐Ÿท alex โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_error_popup.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_error_popup.snap index 72a40f2..9ed1604 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_error_popup.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_error_popup.snap @@ -17,37 +17,37 @@ expression: output โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ f filter โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏโ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ•ญ Movies โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ ctrl-r refresh โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ Library โ”‚ Collections โ”‚ Downloads โ”‚ u update all โ”‚ โ”‚ -โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ enter details โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ -โ”‚ Title โ–ผ โ”‚ esc cancel filter โ”‚ofile Monitored Tags โ”‚ -โ”‚=> Test โ”‚ โ†‘ k scroll up โ”‚ ๐Ÿท alex โ”‚ -โ”‚ โ”‚ โ†“ j scroll down โ”‚ โ”‚ -โ”‚ โ”‚ โ† h previous tab โ”‚ โ”‚ -โ”‚ โ”‚ โ†’ l next tab โ”‚ โ”‚ -โ”‚ โ”‚ pgUp ctrl-u page up โ”‚ โ”‚ -โ”‚ โ”‚ pgDown ctrl-d page down โ”‚ โ”‚ -โ”‚ โ”‚ tab next servarr โ”‚ โ”‚ -โ”‚ โ”‚ shift-tab previous servarr โ”‚ โ”‚ -โ”‚ โ”‚ q quit โ”‚ โ”‚ -โ”‚ โ”‚ ? show/hide keybindings โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ diff --git a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_with_error.snap b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_with_error.snap index 17602d3..9fcb373 100644 --- a/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_with_error.snap +++ b/src/ui/snapshots/managarr__ui__ui_tests__snapshot_tests__radarr_ui_renders_library_tab_with_error.snap @@ -20,9 +20,9 @@ expression: output โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏโ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏโ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ•ญ Movies โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ Library โ”‚ Collections โ”‚ Downloads โ”‚ Blocklist โ”‚ Root Folders โ”‚ Indexers โ”‚ System โ”‚ -โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ -โ”‚ Title โ–ผ Year Studio Runtime Rating Language Size Quality Profile Monitored Tags โ”‚ -โ”‚=> Test 2023 21st Century Alex 2h 0m R English 3.30 GB HD - 1080p ๐Ÿท alex โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ diff --git a/src/ui/sonarr_ui/blocklist/mod.rs b/src/ui/sonarr_ui/blocklist/mod.rs index 6eb7f07..5ca7999 100644 --- a/src/ui/sonarr_ui/blocklist/mod.rs +++ b/src/ui/sonarr_ui/blocklist/mod.rs @@ -103,13 +103,14 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]) .primary() }; + let is_sorting = active_sonarr_block == ActiveSonarrBlock::BlocklistSortPrompt; let blocklist_table = ManagarrTable::new( - Some(&mut app.data.sonarr_data.blocklist), + app, + |app| Some(&mut app.data.sonarr_data.blocklist), blocklist_row_mapping, ) .block(layout_block_top_border()) - .loading(app.is_loading) - .sorting(active_sonarr_block == ActiveSonarrBlock::BlocklistSortPrompt) + .sorting(is_sorting) .headers([ "Series Title", "Source Title", diff --git a/src/ui/sonarr_ui/downloads/mod.rs b/src/ui/sonarr_ui/downloads/mod.rs index 5706bd4..7ecc251 100644 --- a/src/ui/sonarr_ui/downloads/mod.rs +++ b/src/ui/sonarr_ui/downloads/mod.rs @@ -72,6 +72,7 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } else { app.data.sonarr_data.downloads.current_selection().clone() }; + let ui_scroll_tick_count = app.ui_scroll_tick_count; let downloads_row_mapping = |download_record: &DownloadRecord| { let DownloadRecord { @@ -88,7 +89,7 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { output_path.as_ref().unwrap().scroll_left_or_reset( get_width_from_percentage(area, 18), current_selection == *download_record, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); } @@ -120,11 +121,11 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .primary() }; let downloads_table = ManagarrTable::new( - Some(&mut app.data.sonarr_data.downloads), + app, + |app| Some(&mut app.data.sonarr_data.downloads), downloads_row_mapping, ) .block(layout_block_top_border()) - .loading(app.is_loading) .headers([ "Title", "Percent Complete", diff --git a/src/ui/sonarr_ui/history/mod.rs b/src/ui/sonarr_ui/history/mod.rs index c6d875e..c99fe1f 100644 --- a/src/ui/sonarr_ui/history/mod.rs +++ b/src/ui/sonarr_ui/history/mod.rs @@ -1,19 +1,19 @@ use crate::app::App; +use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS}; use crate::models::servarr_models::Language; use crate::models::sonarr_models::{SonarrHistoryEventType, SonarrHistoryItem}; -use crate::models::Route; +use crate::ui::DrawUi; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{get_width_from_percentage, layout_block_top_border}; use crate::ui::widgets::managarr_table::ManagarrTable; use crate::ui::widgets::message::Message; use crate::ui::widgets::popup::{Popup, Size}; -use crate::ui::DrawUi; +use ratatui::Frame; use ratatui::layout::{Alignment, Constraint, Rect}; use ratatui::style::Style; use ratatui::text::Text; use ratatui::widgets::{Cell, Row}; -use ratatui::Frame; use super::sonarr_ui_utils::{ create_download_failed_history_event_details, @@ -55,6 +55,8 @@ fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } else { app.data.sonarr_data.history.current_selection().clone() }; + let ui_scroll_tick_count = app.ui_scroll_tick_count; + if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { let history_row_mapping = |history_item: &SonarrHistoryItem| { let SonarrHistoryItem { @@ -69,7 +71,7 @@ fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { source_title.scroll_left_or_reset( get_width_from_percentage(area, 40), current_selection == *history_item, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); Row::new(vec![ @@ -93,23 +95,25 @@ fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]) .primary() }; - let history_table = - ManagarrTable::new(Some(&mut app.data.sonarr_data.history), history_row_mapping) - .block(layout_block_top_border()) - .loading(app.is_loading) - .sorting(active_sonarr_block == ActiveSonarrBlock::HistorySortPrompt) - .searching(active_sonarr_block == ActiveSonarrBlock::SearchHistory) - .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchHistoryError) - .filtering(active_sonarr_block == ActiveSonarrBlock::FilterHistory) - .filter_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::FilterHistoryError) - .headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) - .constraints([ - Constraint::Percentage(40), - Constraint::Percentage(15), - Constraint::Percentage(12), - Constraint::Percentage(13), - Constraint::Percentage(20), - ]); + let history_table = ManagarrTable::new( + app, + |app| Some(&mut app.data.sonarr_data.history), + history_row_mapping, + ) + .block(layout_block_top_border()) + .sorting(active_sonarr_block == ActiveSonarrBlock::HistorySortPrompt) + .searching(active_sonarr_block == ActiveSonarrBlock::SearchHistory) + .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchHistoryError) + .filtering(active_sonarr_block == ActiveSonarrBlock::FilterHistory) + .filter_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::FilterHistoryError) + .headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(40), + Constraint::Percentage(15), + Constraint::Percentage(12), + Constraint::Percentage(13), + Constraint::Percentage(20), + ]); if [ ActiveSonarrBlock::SearchHistory, diff --git a/src/ui/sonarr_ui/indexers/mod.rs b/src/ui/sonarr_ui/indexers/mod.rs index 86b08b0..5faf1df 100644 --- a/src/ui/sonarr_ui/indexers/mod.rs +++ b/src/ui/sonarr_ui/indexers/mod.rs @@ -111,6 +111,8 @@ impl DrawUi for IndexersUi { } fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let tags_map = app.data.sonarr_data.tags_map.clone(); + let indexers_row_mapping = |indexer: &'_ Indexer| { let Indexer { name, @@ -136,10 +138,7 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let tags: String = tags .iter() .map(|tag_id| { - app - .data - .sonarr_data - .tags_map + tags_map .get_by_left(&tag_id.as_i64().unwrap()) .unwrap_or(&empty_tag) .clone() @@ -158,11 +157,11 @@ fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .primary() }; let indexers_table = ManagarrTable::new( - Some(&mut app.data.sonarr_data.indexers), + app, + |app| Some(&mut app.data.sonarr_data.indexers), indexers_row_mapping, ) .block(layout_block_top_border()) - .loading(app.is_loading) .headers([ "Indexer", "RSS", diff --git a/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs b/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs index 8b0bf20..cd6cdfa 100644 --- a/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs +++ b/src/ui/sonarr_ui/indexers/test_all_indexers_ui.rs @@ -39,12 +39,14 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are } else { IndexerTestResultModalItem::default() }; + let ui_scroll_tick_count = app.ui_scroll_tick_count; + f.render_widget(title_block("Test All Indexers"), area); let test_results_row_mapping = |result: &IndexerTestResultModalItem| { result.validation_failures.scroll_left_or_reset( get_width_from_percentage(area, 86), *result == current_selection, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); let pass_fail = if result.is_valid { "โœ”" } else { "โŒ" }; let row = Row::new(vec![ @@ -61,7 +63,8 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are }; let indexers_test_results_table = ManagarrTable::new( - app.data.sonarr_data.indexer_test_all_results.as_mut(), + app, + |app| app.data.sonarr_data.indexer_test_all_results.as_mut(), test_results_row_mapping, ) .loading(is_loading) diff --git a/src/ui/sonarr_ui/library/add_series_ui.rs b/src/ui/sonarr_ui/library/add_series_ui.rs index 6d09c02..95c3869 100644 --- a/src/ui/sonarr_ui/library/add_series_ui.rs +++ b/src/ui/sonarr_ui/library/add_series_ui.rs @@ -75,13 +75,14 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Layout::vertical([Constraint::Length(3), Constraint::Fill(0)]) .margin(1) .areas(area); - let block_content = &app + let block_content = app .data .sonarr_data .add_series_search .as_ref() .expect("add_series_search must be populated") - .text; + .text + .clone(); let offset = app .data .sonarr_data @@ -90,6 +91,16 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .expect("add_series_search must be populated") .offset .load(Ordering::SeqCst); + let series_tvdb_ids: Vec<_> = app + .data + .sonarr_data + .series + .items + .iter() + .map(|s| s.tvdb_id) + .collect(); + let ui_scroll_tick_count = app.ui_scroll_tick_count; + let search_results_row_mapping = |series: &AddSeriesSearchResult| { let rating = series.ratings.clone().unwrap_or_default().value; let series_rating = if rating == 0.0 { @@ -97,14 +108,7 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } else { format!("{rating:.1}") }; - let in_library = if app - .data - .sonarr_data - .series - .items - .iter() - .any(|mov| mov.tvdb_id == series.tvdb_id) - { + let in_library = if series_tvdb_ids.contains(&series.tvdb_id) { "โœ”" } else { "" @@ -119,7 +123,7 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { series.title.scroll_left_or_reset( get_width_from_percentage(area, 27), *series == current_selection, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); Row::new(vec![ @@ -137,7 +141,7 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { match active_sonarr_block { ActiveSonarrBlock::AddSeriesSearchInput => { - let search_box = InputBox::new(block_content) + let search_box = InputBox::new(&block_content) .offset(offset) .block(title_block_centered("Add Series")); @@ -162,7 +166,8 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { | ActiveSonarrBlock::AddSeriesAlreadyInLibrary | ActiveSonarrBlock::AddSeriesTagsInput => { let search_results_table = ManagarrTable::new( - app.data.sonarr_data.add_searched_series.as_mut(), + app, + |app| app.data.sonarr_data.add_searched_series.as_mut(), search_results_row_mapping, ) .loading(is_loading) @@ -181,13 +186,20 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]); f.render_widget(search_results_table, results_area); + f.render_widget( + InputBox::new(&block_content) + .offset(offset) + .block(title_block_centered("Add Series")), + search_box_area, + ); + return; } _ => (), } } f.render_widget( - InputBox::new(block_content) + InputBox::new(&block_content) .offset(offset) .block(title_block_centered("Add Series")), search_box_area, diff --git a/src/ui/sonarr_ui/library/episode_details_ui.rs b/src/ui/sonarr_ui/library/episode_details_ui.rs index f5af16a..422d88f 100644 --- a/src/ui/sonarr_ui/library/episode_details_ui.rs +++ b/src/ui/sonarr_ui/library/episode_details_ui.rs @@ -267,6 +267,7 @@ fn draw_episode_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) .current_selection() .clone() }; + let ui_scroll_tick_count = app.ui_scroll_tick_count; let history_row_mapping = |history_item: &SonarrHistoryItem| { let SonarrHistoryItem { @@ -281,7 +282,7 @@ fn draw_episode_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) source_title.scroll_left_or_reset( get_width_from_percentage(area, 40), current_selection == *history_item, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); Row::new(vec![ @@ -305,28 +306,33 @@ fn draw_episode_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) ]) .primary() }; - let mut episode_history_table = &mut app - .data - .sonarr_data - .season_details_modal - .as_mut() - .expect("season_details_modal must exist in this context") - .episode_details_modal - .as_mut() - .expect("episode_details_modal must exist in this context") - .episode_history; - let history_table = - ManagarrTable::new(Some(&mut episode_history_table), history_row_mapping) - .block(layout_block_top_border()) - .loading(app.is_loading) - .headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) - .constraints([ - Constraint::Percentage(40), - Constraint::Percentage(15), - Constraint::Percentage(12), - Constraint::Percentage(13), - Constraint::Percentage(20), - ]); + let history_table = ManagarrTable::new( + app, + |app| { + Some( + &mut app + .data + .sonarr_data + .season_details_modal + .as_mut() + .expect("season_details_modal must exist in this context") + .episode_details_modal + .as_mut() + .expect("episode_details_modal must exist in this context") + .episode_history, + ) + }, + history_row_mapping, + ) + .block(layout_block_top_border()) + .headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(40), + Constraint::Percentage(15), + Constraint::Percentage(12), + Constraint::Percentage(13), + Constraint::Percentage(20), + ]); f.render_widget(history_table, area); } @@ -409,6 +415,7 @@ fn draw_episode_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { episode_details_modal.episode_releases.is_empty(), ) }; + let ui_scroll_tick_count = app.ui_scroll_tick_count; if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { let episode_release_row_mapping = |release: &SonarrRelease| { @@ -431,7 +438,7 @@ fn draw_episode_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { get_width_from_percentage(area, 30), current_selection == *release && active_sonarr_block != ActiveSonarrBlock::ManualEpisodeSearchConfirmPrompt, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); let size = convert_to_gb(*size); let rejected_str = if *rejected { "โ›”" } else { "" }; @@ -480,22 +487,26 @@ fn draw_episode_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]) .primary() }; - let mut episode_release_table = &mut app - .data - .sonarr_data - .season_details_modal - .as_mut() - .expect("season_details_modal must exist in this context") - .episode_details_modal - .as_mut() - .expect("episode_details_modal must exist in this context") - .episode_releases; let release_table = ManagarrTable::new( - Some(&mut episode_release_table), + app, + |app| { + Some( + &mut app + .data + .sonarr_data + .season_details_modal + .as_mut() + .expect("season_details_modal must exist in this context") + .episode_details_modal + .as_mut() + .expect("episode_details_modal must exist in this context") + .episode_releases, + ) + }, episode_release_row_mapping, ) .block(layout_block_top_border()) - .loading(app.is_loading || is_empty) + .loading(is_empty) .sorting(active_sonarr_block == ActiveSonarrBlock::ManualEpisodeSearchSortPrompt) .headers([ "Source", "Age", "โ›”", "Title", "Indexer", "Size", "Peers", "Language", "Quality", diff --git a/src/ui/sonarr_ui/library/mod.rs b/src/ui/sonarr_ui/library/mod.rs index a6e0fc4..9625d39 100644 --- a/src/ui/sonarr_ui/library/mod.rs +++ b/src/ui/sonarr_ui/library/mod.rs @@ -2,9 +2,9 @@ use add_series_ui::AddSeriesUi; use delete_series_ui::DeleteSeriesUi; use edit_series_ui::EditSeriesUi; use ratatui::{ + Frame, layout::{Constraint, Rect}, widgets::{Cell, Row}, - Frame, }; use series_details_ui::SeriesDetailsUi; @@ -16,15 +16,15 @@ use crate::utils::convert_to_gb; use crate::{ app::App, models::{ + Route, servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, LIBRARY_BLOCKS}, sonarr_models::{Series, SeriesStatus}, - Route, }, ui::{ + DrawUi, styles::ManagarrStyle, utils::{get_width_from_percentage, layout_block_top_border}, widgets::managarr_table::ManagarrTable, - DrawUi, }, }; @@ -86,16 +86,16 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } else { Series::default() }; - let quality_profile_map = &app.data.sonarr_data.quality_profile_map; - let language_profile_map = &app.data.sonarr_data.language_profiles_map; - let tags_map = &app.data.sonarr_data.tags_map; - let content = Some(&mut app.data.sonarr_data.series); + let quality_profile_map = app.data.sonarr_data.quality_profile_map.clone(); + let language_profile_map = app.data.sonarr_data.language_profiles_map.clone(); + let tags_map = app.data.sonarr_data.tags_map.clone(); + let ui_scroll_tick_count = app.ui_scroll_tick_count; let series_table_row_mapping = |series: &Series| { series.title.scroll_left_or_reset( get_width_from_percentage(area, 23), *series == current_selection, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); let monitored = if series.monitored { "๐Ÿท" } else { "" }; let certification = series.certification.clone().unwrap_or_default(); @@ -139,40 +139,43 @@ fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]), ) }; - let series_table = ManagarrTable::new(content, series_table_row_mapping) - .block(layout_block_top_border()) - .loading(app.is_loading) - .sorting(active_sonarr_block == ActiveSonarrBlock::SeriesSortPrompt) - .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeries) - .filtering(active_sonarr_block == ActiveSonarrBlock::FilterSeries) - .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchSeriesError) - .filter_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::FilterSeriesError) - .headers([ - "Title", - "Year", - "Network", - "Status", - "Rating", - "Type", - "Quality Profile", - "Language", - "Size", - "Monitored", - "Tags", - ]) - .constraints([ - Constraint::Percentage(20), - Constraint::Percentage(4), - Constraint::Percentage(14), - Constraint::Percentage(6), - Constraint::Percentage(6), - Constraint::Percentage(6), - Constraint::Percentage(11), - Constraint::Percentage(8), - Constraint::Percentage(7), - Constraint::Percentage(6), - Constraint::Percentage(12), - ]); + let series_table = ManagarrTable::new( + app, + |app| Some(&mut app.data.sonarr_data.series), + series_table_row_mapping, + ) + .block(layout_block_top_border()) + .sorting(active_sonarr_block == ActiveSonarrBlock::SeriesSortPrompt) + .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeries) + .filtering(active_sonarr_block == ActiveSonarrBlock::FilterSeries) + .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchSeriesError) + .filter_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::FilterSeriesError) + .headers([ + "Title", + "Year", + "Network", + "Status", + "Rating", + "Type", + "Quality Profile", + "Language", + "Size", + "Monitored", + "Tags", + ]) + .constraints([ + Constraint::Percentage(20), + Constraint::Percentage(4), + Constraint::Percentage(14), + Constraint::Percentage(6), + Constraint::Percentage(6), + Constraint::Percentage(6), + Constraint::Percentage(11), + Constraint::Percentage(8), + Constraint::Percentage(7), + Constraint::Percentage(6), + Constraint::Percentage(12), + ]); if [ ActiveSonarrBlock::SearchSeries, diff --git a/src/ui/sonarr_ui/library/season_details_ui.rs b/src/ui/sonarr_ui/library/season_details_ui.rs index b676c6f..043466d 100644 --- a/src/ui/sonarr_ui/library/season_details_ui.rs +++ b/src/ui/sonarr_ui/library/season_details_ui.rs @@ -162,16 +162,7 @@ fn draw_episodes_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .episode_files .items .clone(); - let content = Some( - &mut app - .data - .sonarr_data - .season_details_modal - .as_mut() - .expect("Season details modal is unpopulated") - .episodes, - ); - let downloads_vec = &app.data.sonarr_data.downloads.items; + let downloads_vec = app.data.sonarr_data.downloads.items.clone(); let episode_row_mapping = |episode: &Episode| { let Episode { @@ -202,7 +193,7 @@ fn draw_episodes_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { }; decorate_with_row_style( - downloads_vec, + &downloads_vec, episode, Row::new(vec![ Cell::from(episode_monitored.to_owned()), @@ -215,27 +206,40 @@ fn draw_episodes_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ) }; let is_searching = active_sonarr_block == ActiveSonarrBlock::SearchEpisodes; - let season_table = ManagarrTable::new(content, episode_row_mapping) - .block(layout_block_top_border()) - .loading(app.is_loading) - .searching(is_searching) - .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchEpisodesError) - .headers([ - "๐Ÿท", - "#", - "Title", - "Air Date", - "Size on Disk", - "Quality Profile", - ]) - .constraints([ - Constraint::Percentage(4), - Constraint::Percentage(4), - Constraint::Percentage(50), - Constraint::Percentage(19), - Constraint::Percentage(10), - Constraint::Percentage(12), - ]); + let season_table = ManagarrTable::new( + app, + |app| { + Some( + &mut app + .data + .sonarr_data + .season_details_modal + .as_mut() + .expect("Season details modal is unpopulated") + .episodes, + ) + }, + episode_row_mapping, + ) + .block(layout_block_top_border()) + .searching(is_searching) + .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchEpisodesError) + .headers([ + "๐Ÿท", + "#", + "Title", + "Air Date", + "Size on Disk", + "Quality Profile", + ]) + .constraints([ + Constraint::Percentage(4), + Constraint::Percentage(4), + Constraint::Percentage(50), + Constraint::Percentage(19), + Constraint::Percentage(10), + Constraint::Percentage(12), + ]); if is_searching { season_table.show_cursor(f, area); @@ -256,6 +260,7 @@ fn draw_season_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { .current_selection() .clone() }; + let ui_scroll_tick_count = app.ui_scroll_tick_count; if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { let history_row_mapping = |history_item: &SonarrHistoryItem| { @@ -271,7 +276,7 @@ fn draw_season_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { source_title.scroll_left_or_reset( get_width_from_percentage(area, 40), current_selection == *history_item, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); Row::new(vec![ @@ -295,34 +300,39 @@ fn draw_season_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]) .primary() }; - let mut season_history_table = &mut app - .data - .sonarr_data - .season_details_modal - .as_mut() - .expect("season_details_modal must exist in this context") - .season_history; - let history_table = - ManagarrTable::new(Some(&mut season_history_table), history_row_mapping) - .block(layout_block_top_border()) - .loading(app.is_loading) - .sorting(active_sonarr_block == ActiveSonarrBlock::SeasonHistorySortPrompt) - .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeasonHistory) - .search_produced_empty_results( - active_sonarr_block == ActiveSonarrBlock::SearchSeasonHistoryError, + let history_table = ManagarrTable::new( + app, + |app| { + Some( + &mut app + .data + .sonarr_data + .season_details_modal + .as_mut() + .expect("season_details_modal must exist in this context") + .season_history, ) - .filtering(active_sonarr_block == ActiveSonarrBlock::FilterSeasonHistory) - .filter_produced_empty_results( - active_sonarr_block == ActiveSonarrBlock::FilterSeasonHistoryError, - ) - .headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) - .constraints([ - Constraint::Percentage(40), - Constraint::Percentage(15), - Constraint::Percentage(12), - Constraint::Percentage(13), - Constraint::Percentage(20), - ]); + }, + history_row_mapping, + ) + .block(layout_block_top_border()) + .sorting(active_sonarr_block == ActiveSonarrBlock::SeasonHistorySortPrompt) + .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeasonHistory) + .search_produced_empty_results( + active_sonarr_block == ActiveSonarrBlock::SearchSeasonHistoryError, + ) + .filtering(active_sonarr_block == ActiveSonarrBlock::FilterSeasonHistory) + .filter_produced_empty_results( + active_sonarr_block == ActiveSonarrBlock::FilterSeasonHistoryError, + ) + .headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(40), + Constraint::Percentage(15), + Constraint::Percentage(12), + Constraint::Percentage(13), + Constraint::Percentage(20), + ]); if [ ActiveSonarrBlock::SearchSeriesHistory, @@ -360,6 +370,7 @@ fn draw_season_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { season_details_modal.season_releases.is_empty(), ) }; + let ui_scroll_tick_count = app.ui_scroll_tick_count; if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { let season_release_row_mapping = |release: &SonarrRelease| { @@ -382,7 +393,7 @@ fn draw_season_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { get_width_from_percentage(area, 30), current_selection == *release && active_sonarr_block != ActiveSonarrBlock::ManualSeasonSearchConfirmPrompt, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); let size = convert_to_gb(*size); let rejected_str = if *rejected { "โ›”" } else { "" }; @@ -431,32 +442,38 @@ fn draw_season_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]) .primary() }; - let mut season_release_table = &mut app - .data - .sonarr_data - .season_details_modal - .as_mut() - .expect("season_details_modal must exist in this context") - .season_releases; - let release_table = - ManagarrTable::new(Some(&mut season_release_table), season_release_row_mapping) - .block(layout_block_top_border()) - .loading(app.is_loading || is_empty) - .sorting(active_sonarr_block == ActiveSonarrBlock::ManualSeasonSearchSortPrompt) - .headers([ - "Source", "Age", "โ›”", "Title", "Indexer", "Size", "Peers", "Language", "Quality", - ]) - .constraints([ - Constraint::Length(9), - Constraint::Length(10), - Constraint::Length(5), - Constraint::Percentage(30), - Constraint::Percentage(18), - Constraint::Length(12), - Constraint::Length(12), - Constraint::Percentage(7), - Constraint::Percentage(10), - ]); + let release_table = ManagarrTable::new( + app, + |app| { + Some( + &mut app + .data + .sonarr_data + .season_details_modal + .as_mut() + .expect("season_details_modal must exist in this context") + .season_releases, + ) + }, + season_release_row_mapping, + ) + .block(layout_block_top_border()) + .loading(is_empty) + .sorting(active_sonarr_block == ActiveSonarrBlock::ManualSeasonSearchSortPrompt) + .headers([ + "Source", "Age", "โ›”", "Title", "Indexer", "Size", "Peers", "Language", "Quality", + ]) + .constraints([ + Constraint::Length(9), + Constraint::Length(10), + Constraint::Length(5), + Constraint::Percentage(30), + Constraint::Percentage(18), + Constraint::Length(12), + Constraint::Length(12), + Constraint::Percentage(7), + Constraint::Percentage(10), + ]); f.render_widget(release_table, area); } diff --git a/src/ui/sonarr_ui/library/series_details_ui.rs b/src/ui/sonarr_ui/library/series_details_ui.rs index 92364ae..96f7af0 100644 --- a/src/ui/sonarr_ui/library/series_details_ui.rs +++ b/src/ui/sonarr_ui/library/series_details_ui.rs @@ -228,7 +228,6 @@ pub fn draw_series_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { - let content = Some(&mut app.data.sonarr_data.seasons); let season_row_mapping = |season: &Season| { let Season { title, @@ -271,18 +270,21 @@ fn draw_seasons_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } }; let is_searching = active_sonarr_block == ActiveSonarrBlock::SearchSeason; - let season_table = ManagarrTable::new(content, season_row_mapping) - .block(layout_block_top_border()) - .loading(app.is_loading) - .searching(is_searching) - .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchSeasonError) - .headers(["Monitored", "Season", "Episode Count", "Size on Disk"]) - .constraints([ - Constraint::Percentage(6), - Constraint::Ratio(1, 3), - Constraint::Ratio(1, 3), - Constraint::Ratio(1, 3), - ]); + let season_table = ManagarrTable::new( + app, + |app| Some(&mut app.data.sonarr_data.seasons), + season_row_mapping, + ) + .block(layout_block_top_border()) + .searching(is_searching) + .search_produced_empty_results(active_sonarr_block == ActiveSonarrBlock::SearchSeasonError) + .headers(["Monitored", "Season", "Episode Count", "Size on Disk"]) + .constraints([ + Constraint::Percentage(6), + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + ]); if is_searching { season_table.show_cursor(f, area); @@ -300,6 +302,7 @@ fn draw_series_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { } else { series_history.current_selection().clone() }; + let ui_scroll_tick_count = app.ui_scroll_tick_count; if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() { let history_row_mapping = |history_item: &SonarrHistoryItem| { @@ -315,7 +318,7 @@ fn draw_series_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { source_title.scroll_left_or_reset( get_width_from_percentage(area, 40), current_selection == *history_item, - app.ui_scroll_tick_count == 0, + ui_scroll_tick_count == 0, ); Row::new(vec![ @@ -339,33 +342,29 @@ fn draw_series_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]) .primary() }; - let mut series_history_table = app - .data - .sonarr_data - .series_history - .as_mut() - .expect("series_history must be populated"); - let history_table = - ManagarrTable::new(Some(&mut series_history_table), history_row_mapping) - .block(layout_block_top_border()) - .loading(app.is_loading) - .sorting(active_sonarr_block == ActiveSonarrBlock::SeriesHistorySortPrompt) - .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeriesHistory) - .search_produced_empty_results( - active_sonarr_block == ActiveSonarrBlock::SearchSeriesHistoryError, - ) - .filtering(active_sonarr_block == ActiveSonarrBlock::FilterSeriesHistory) - .filter_produced_empty_results( - active_sonarr_block == ActiveSonarrBlock::FilterSeriesHistoryError, - ) - .headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) - .constraints([ - Constraint::Percentage(40), - Constraint::Percentage(15), - Constraint::Percentage(12), - Constraint::Percentage(13), - Constraint::Percentage(20), - ]); + let history_table = ManagarrTable::new( + app, + |app| app.data.sonarr_data.series_history.as_mut(), + history_row_mapping, + ) + .block(layout_block_top_border()) + .sorting(active_sonarr_block == ActiveSonarrBlock::SeriesHistorySortPrompt) + .searching(active_sonarr_block == ActiveSonarrBlock::SearchSeriesHistory) + .search_produced_empty_results( + active_sonarr_block == ActiveSonarrBlock::SearchSeriesHistoryError, + ) + .filtering(active_sonarr_block == ActiveSonarrBlock::FilterSeriesHistory) + .filter_produced_empty_results( + active_sonarr_block == ActiveSonarrBlock::FilterSeriesHistoryError, + ) + .headers(["Source Title", "Event Type", "Language", "Quality", "Date"]) + .constraints([ + Constraint::Percentage(40), + Constraint::Percentage(15), + Constraint::Percentage(12), + Constraint::Percentage(13), + Constraint::Percentage(20), + ]); if [ ActiveSonarrBlock::SearchSeriesHistory, diff --git a/src/ui/sonarr_ui/root_folders/mod.rs b/src/ui/sonarr_ui/root_folders/mod.rs index db98c24..d90e5a3 100644 --- a/src/ui/sonarr_ui/root_folders/mod.rs +++ b/src/ui/sonarr_ui/root_folders/mod.rs @@ -84,11 +84,11 @@ fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { }; let root_folders_table = ManagarrTable::new( - Some(&mut app.data.sonarr_data.root_folders), + app, + |app| Some(&mut app.data.sonarr_data.root_folders), root_folders_row_mapping, ) .block(layout_block_top_border()) - .loading(app.is_loading) .headers(["Path", "Free Space", "Unmapped Folders"]) .constraints([ Constraint::Ratio(3, 5), diff --git a/src/ui/sonarr_ui/system/mod.rs b/src/ui/sonarr_ui/system/mod.rs index 96d06ee..977e659 100644 --- a/src/ui/sonarr_ui/system/mod.rs +++ b/src/ui/sonarr_ui/system/mod.rs @@ -89,12 +89,15 @@ fn draw_tasks(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]) .primary() }; - let tasks_table = ManagarrTable::new(Some(&mut app.data.sonarr_data.tasks), tasks_row_mapping) - .block(title_block("Tasks")) - .loading(app.is_loading) - .highlight_rows(false) - .headers(TASK_TABLE_HEADERS) - .constraints(TASK_TABLE_CONSTRAINTS); + let tasks_table = ManagarrTable::new( + app, + |app| Some(&mut app.data.sonarr_data.tasks), + tasks_row_mapping, + ) + .block(title_block("Tasks")) + .highlight_rows(false) + .headers(TASK_TABLE_HEADERS) + .constraints(TASK_TABLE_CONSTRAINTS); f.render_widget(tasks_table, area); } @@ -137,11 +140,11 @@ pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rec .primary() }; let events_table = ManagarrTable::new( - Some(&mut app.data.sonarr_data.queued_events), + app, + |app| Some(&mut app.data.sonarr_data.queued_events), events_row_mapping, ) .block(title_block("Queued Events")) - .loading(app.is_loading) .highlight_rows(false) .headers(["Trigger", "Status", "Name", "Queued", "Started", "Duration"]) .constraints([ diff --git a/src/ui/sonarr_ui/system/system_details_ui.rs b/src/ui/sonarr_ui/system/system_details_ui.rs index f25c5e9..643f86e 100644 --- a/src/ui/sonarr_ui/system/system_details_ui.rs +++ b/src/ui/sonarr_ui/system/system_details_ui.rs @@ -92,27 +92,35 @@ fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { ]) .primary() }; - let tasks_table = ManagarrTable::new(Some(&mut app.data.sonarr_data.tasks), tasks_row_mapping) - .loading(app.is_loading) - .margin(1) - .headers(TASK_TABLE_HEADERS) - .constraints(TASK_TABLE_CONSTRAINTS); + let current_route = app.get_current_route(); + let prompt_confirm = app.data.sonarr_data.prompt_confirm; + let task_name = if app.data.sonarr_data.tasks.items.is_empty() { + String::new() + } else { + app.data.sonarr_data.tasks.current_selection().name.clone() + }; + + let tasks_table = ManagarrTable::new( + app, + |app| Some(&mut app.data.sonarr_data.tasks), + tasks_row_mapping, + ) + .margin(1) + .headers(TASK_TABLE_HEADERS) + .constraints(TASK_TABLE_CONSTRAINTS); f.render_widget(title_block("Tasks"), area); f.render_widget(tasks_table, area); if matches!( - app.get_current_route(), + current_route, Route::Sonarr(ActiveSonarrBlock::SystemTaskStartConfirmPrompt, _) ) { - let prompt = format!( - "Do you want to manually start this task: {}?", - app.data.sonarr_data.tasks.current_selection().name - ); + let prompt = format!("Do you want to manually start this task: {}?", task_name); let confirmation_prompt = ConfirmationPrompt::new() .title("Start Task") .prompt(&prompt) - .yes_no_value(app.data.sonarr_data.prompt_confirm); + .yes_no_value(prompt_confirm); f.render_widget( Popup::new(confirmation_prompt).size(Size::MediumPrompt), diff --git a/src/ui/widgets/loading_block.rs b/src/ui/widgets/loading_block.rs index 76cec5a..19e4865 100644 --- a/src/ui/widgets/loading_block.rs +++ b/src/ui/widgets/loading_block.rs @@ -7,7 +7,7 @@ use ratatui::prelude::Text; use ratatui::style::Style; use ratatui::widgets::{Block, Paragraph, Widget}; use tachyonfx::pattern::SweepPattern; -use tachyonfx::{fx, Interpolation}; +use tachyonfx::fx; #[cfg(test)] #[path = "loading_block_tests.rs"] @@ -40,15 +40,20 @@ impl<'a, 'b> LoadingBlock<'a, 'b> { self.block.render(area, buf); } if let Some(app) = self.app - && !app.has_active_effect { - let color = Style::new().failure().fg.expect("primary fg color is unset"); - let fx = - fx::repeating(fx::paint_fg(color, 1000) - .with_pattern(SweepPattern::left_to_right(10)) - .with_area(area)); - app.effects.add_effect(fx); - app.has_active_effect = true; - } + && !app.has_active_effect + { + let color = Style::new() + .failure() + .fg + .expect("primary fg color is unset"); + let fx = fx::repeating( + fx::paint_fg(color, 1000) + .with_pattern(SweepPattern::left_to_right(10)) + .with_area(area), + ); + app.effects.add_effect(fx); + app.has_active_effect = true; + } } } diff --git a/src/ui/widgets/managarr_table.rs b/src/ui/widgets/managarr_table.rs index 7aecb9f..cc805f5 100644 --- a/src/ui/widgets/managarr_table.rs +++ b/src/ui/widgets/managarr_table.rs @@ -1,6 +1,7 @@ use super::input_box_popup::InputBoxPopup; use super::message::Message; use super::popup::Size; +use crate::app::App; use crate::models::stateful_table::StatefulTable; use crate::ui::HIGHLIGHT_SYMBOL; use crate::ui::styles::ManagarrStyle; @@ -16,23 +17,28 @@ use ratatui::prelude::{Style, Stylize, Text}; use ratatui::widgets::{Block, ListItem, Row, StatefulWidget, Table, Widget, WidgetRef}; use std::fmt::Debug; use std::sync::atomic::Ordering; +use tachyonfx::{Interpolation, fx}; #[cfg(test)] #[path = "managarr_table_tests.rs"] mod managarr_table_tests; #[derive(Setters)] -pub struct ManagarrTable<'a, T, F> +pub struct ManagarrTable<'a, 'b, T, F, G> where F: Fn(&T) -> Row<'a>, T: Clone + PartialEq + Eq + Debug, + G: for<'c> FnMut(&'c mut App<'b>) -> Option<&'c mut StatefulTable>, { - #[setters(strip_option)] - content: Option<&'a mut StatefulTable>, + #[setters(skip)] + app: &'a mut App<'b>, + #[setters(skip)] + content_accessor: G, #[setters(skip)] table_headers: Vec, #[setters(skip)] constraints: Vec, + #[setters(skip)] row_mapper: F, block: Block<'a>, margin: u16, @@ -51,46 +57,69 @@ where search_box_offset: usize, filter_box_content_length: usize, filter_box_offset: usize, + #[setters(skip)] + sort_header_info: Option<(usize, bool)>, } -impl<'a, T, F> ManagarrTable<'a, T, F> +impl<'a, 'b, T, F, G> ManagarrTable<'a, 'b, T, F, G> where F: Fn(&T) -> Row<'a>, T: Clone + PartialEq + Eq + Debug, + G: for<'c> FnMut(&'c mut App<'b>) -> Option<&'c mut StatefulTable>, { - pub fn new(content: Option<&'a mut StatefulTable>, row_mapper: F) -> Self { - let mut managarr_table = Self { - content: None, + pub fn new(app: &'a mut App<'b>, mut content_accessor: G, row_mapper: F) -> Self { + let is_loading = app.is_loading; + + // Extract values from content in a scoped block so the borrow ends + let ( + search_box_content_length, + search_box_offset, + filter_box_content_length, + filter_box_offset, + sort_header_info, + ) = { + if let Some(content) = content_accessor(&mut *app) { + let (scl, sbo) = if let Some(search) = content.search.as_ref() { + (search.text.len(), search.offset.load(Ordering::SeqCst)) + } else { + (0, 0) + }; + let (fcl, fbo) = if let Some(filter) = content.filter.as_ref() { + (filter.text.len(), filter.offset.load(Ordering::SeqCst)) + } else { + (0, 0) + }; + let sort_info = content.sort.as_ref().map(|sort_list| { + let idx = sort_list.state.selected().unwrap_or(0); + (idx, content.sort_asc) + }); + (scl, sbo, fcl, fbo, sort_info) + } else { + (0, 0, 0, 0, None) + } + }; + + Self { + app, + content_accessor, table_headers: Vec::new(), constraints: Vec::new(), row_mapper, block: borderless_block(), margin: 0, - is_loading: false, + is_loading, highlight_rows: true, is_sorting: false, is_searching: false, search_produced_empty_results: false, is_filtering: false, filter_produced_empty_results: false, - search_box_content_length: 0, - search_box_offset: 0, - filter_box_content_length: 0, - filter_box_offset: 0, - }; - - if let Some(content) = content.as_ref() { - if let Some(search) = content.search.as_ref() { - managarr_table.search_box_content_length = search.text.len(); - managarr_table.search_box_offset = search.offset.load(Ordering::SeqCst); - } else if let Some(filter) = content.filter.as_ref() { - managarr_table.filter_box_content_length = filter.text.len(); - managarr_table.filter_box_offset = filter.offset.load(Ordering::SeqCst); - } + search_box_content_length, + search_box_offset, + filter_box_content_length, + filter_box_offset, + sort_header_info, } - - managarr_table.content = content; - managarr_table } pub fn headers(mut self, headers: I) -> Self @@ -111,9 +140,9 @@ where self } - fn render_table(self, area: Rect, buf: &mut Buffer) { + fn render_table(mut self, area: Rect, buf: &mut Buffer) { let table_headers = self.parse_headers(); - let table_area = { + let content_area = { let [content_area, _] = Layout::vertical([Constraint::Fill(1), Constraint::Fill(0)]) .margin(self.margin) .areas(area); @@ -122,87 +151,98 @@ where }; let loading_block = LoadingBlock::new(self.is_loading, self.block.clone()); - if let Some(content) = self.content - && !self.is_loading + // Render table content in a scoped block so the borrow ends before we access effects { - let (table_contents, table_state) = if content.filtered_items.is_some() { - ( - content.filtered_items.as_ref().unwrap(), - content.filtered_state.as_mut().unwrap(), - ) - } else { - (&content.items, &mut content.state) - }; - if !table_contents.is_empty() { - let rows = table_contents.iter().map(&self.row_mapper); + if let Some(content) = (self.content_accessor)(&mut *self.app) + && !self.is_loading + { + let (table_contents, table_state) = if content.filtered_items.is_some() { + ( + content.filtered_items.as_ref().unwrap(), + content.filtered_state.as_mut().unwrap(), + ) + } else { + (&content.items, &mut content.state) + }; + if !table_contents.is_empty() { + let rows = table_contents.iter().map(&self.row_mapper); - let headers = Row::new(table_headers).default().bold().bottom_margin(0); + let headers = Row::new(table_headers).default().bold().bottom_margin(0); - let mut table = Table::new(rows, &self.constraints) - .header(headers) - .block(self.block); + let mut table = Table::new(rows, &self.constraints) + .header(headers) + .block(self.block.clone()); - if self.highlight_rows { - table = table - .row_highlight_style(Style::new().highlight()) - .highlight_symbol(HIGHLIGHT_SYMBOL); - } + if self.highlight_rows { + table = table + .row_highlight_style(Style::new().highlight()) + .highlight_symbol(HIGHLIGHT_SYMBOL); + } - StatefulWidget::render(table, table_area, buf, table_state); + StatefulWidget::render(table, content_area, buf, table_state); - if content.sort.is_some() && self.is_sorting { - let selectable_list = SelectableList::new(content.sort.as_mut().unwrap(), |item| { - ListItem::new(Text::from(item.name)) - }); - Popup::new(selectable_list) - .dimensions(20, 50) - .render(table_area, buf); - } + if content.sort.is_some() && self.is_sorting { + let selectable_list = SelectableList::new(content.sort.as_mut().unwrap(), |item| { + ListItem::new(Text::from(item.name)) + }); + Popup::new(selectable_list) + .dimensions(20, 50) + .render(content_area, buf); + } - if self.is_searching { - let box_content = &content.search.as_ref().unwrap(); - InputBoxPopup::new(&box_content.text) - .offset(box_content.offset.load(Ordering::SeqCst)) - .block(title_block_centered("Search")) - .render_ref(table_area, buf); - } + if self.is_searching { + let box_content = &content.search.as_ref().unwrap(); + InputBoxPopup::new(&box_content.text) + .offset(box_content.offset.load(Ordering::SeqCst)) + .block(title_block_centered("Search")) + .render_ref(content_area, buf); + } - if self.is_filtering { - let box_content = &content.filter.as_ref().unwrap(); - InputBoxPopup::new(&box_content.text) - .offset(box_content.offset.load(Ordering::SeqCst)) - .block(title_block_centered("Filter")) - .render_ref(table_area, buf); - } + if self.is_filtering { + let box_content = &content.filter.as_ref().unwrap(); + InputBoxPopup::new(&box_content.text) + .offset(box_content.offset.load(Ordering::SeqCst)) + .block(title_block_centered("Filter")) + .render_ref(content_area, buf); + } - if self.search_produced_empty_results { - Popup::new(Message::new("No items found matching search")) - .size(Size::Message) - .render(table_area, buf); - } + if self.search_produced_empty_results { + Popup::new(Message::new("No items found matching search")) + .size(Size::Message) + .render(content_area, buf); + } - if self.filter_produced_empty_results { - Popup::new(Message::new("The given filter produced empty results")) - .size(Size::Message) - .render(table_area, buf); + if self.filter_produced_empty_results { + Popup::new(Message::new("The given filter produced empty results")) + .size(Size::Message) + .render(content_area, buf); + } + } else { + loading_block.render(content_area, buf); } } else { - loading_block.render(table_area, buf); + loading_block.render(content_area, buf); } - } else { - loading_block.render(table_area, buf); + } + + // Now the content borrow has ended, we can access app for effects + if !self.app.has_active_effect { + let timer = (100, Interpolation::Linear); + let fx = fx::coalesce(timer).with_area(content_area); + self.app.effects.add_effect(fx); + self.app.has_active_effect = true; } } fn parse_headers(&self) -> Vec> { - if let Some(ref content) = self.content - && let Some(ref sort_list) = content.sort + if let Some((idx, sort_asc)) = self.sort_header_info && !self.is_sorting { let mut new_headers = self.table_headers.clone(); - let idx = sort_list.state.selected().unwrap_or(0); - let direction = if content.sort_asc { " โ–ฒ" } else { " โ–ผ" }; - new_headers[idx].push_str(direction); + let direction = if sort_asc { " โ–ฒ" } else { " โ–ผ" }; + if idx < new_headers.len() { + new_headers[idx].push_str(direction); + } return new_headers.into_iter().map(Text::from).collect(); } @@ -238,10 +278,11 @@ where } } -impl<'a, T, F> Widget for ManagarrTable<'a, T, F> +impl<'a, 'b, T, F, G> Widget for ManagarrTable<'a, 'b, T, F, G> where F: Fn(&T) -> Row<'a>, T: Clone + PartialEq + Eq + Debug, + G: for<'c> FnMut(&'c mut App<'b>) -> Option<&'c mut StatefulTable>, { fn render(self, area: Rect, buf: &mut Buffer) { self.render_table(area, buf); diff --git a/src/ui/widgets/managarr_table_tests.rs b/src/ui/widgets/managarr_table_tests.rs index 199100f..9927891 100644 --- a/src/ui/widgets/managarr_table_tests.rs +++ b/src/ui/widgets/managarr_table_tests.rs @@ -1,7 +1,8 @@ #[cfg(test)] mod tests { + use crate::app::App; use crate::models::stateful_list::StatefulList; - use crate::models::stateful_table::{SortOption, StatefulTable}; + use crate::models::stateful_table::SortOption; use crate::models::{HorizontallyScrollableText, Scrollable}; use crate::ui::utils::borderless_block; use crate::ui::widgets::managarr_table::ManagarrTable; @@ -14,15 +15,19 @@ mod tests { #[test] fn test_managarr_table_new() { let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); + let mut app = App::test_default(); + app.data.radarr_data.movies.set_items(items.clone().into_iter().map(|s| { + let mut movie = crate::models::radarr_models::Movie::default(); + movie.title = crate::models::HorizontallyScrollableText::from(s); + movie + }).collect()); - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])); + let managarr_table = ManagarrTable::new( + &mut app, + |app| Some(&mut app.data.radarr_data.movies), + |movie: &crate::models::radarr_models::Movie| Row::new(vec![Cell::new(movie.title.to_string())]), + ); - let row_mapper = managarr_table.row_mapper; - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); assert_eq!(managarr_table.table_headers, Vec::::new()); assert_eq!(managarr_table.constraints, Vec::new()); assert_eq!(managarr_table.block, borderless_block()); @@ -43,20 +48,24 @@ mod tests { #[test] fn test_managarr_table_new_search_box_populated() { let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); + let mut app = App::test_default(); + app.data.radarr_data.movies.set_items(items.clone().into_iter().map(|s| { + let mut movie = crate::models::radarr_models::Movie::default(); + movie.title = crate::models::HorizontallyScrollableText::from(s); + movie + }).collect()); let horizontally_scrollable_test = HorizontallyScrollableText { text: "test".to_owned(), offset: AtomicUsize::new(3), }; - stateful_table.search = Some(horizontally_scrollable_test); + app.data.radarr_data.movies.search = Some(horizontally_scrollable_test); - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])); + let managarr_table = ManagarrTable::new( + &mut app, + |app| Some(&mut app.data.radarr_data.movies), + |movie: &crate::models::radarr_models::Movie| Row::new(vec![Cell::new(movie.title.to_string())]), + ); - let row_mapper = managarr_table.row_mapper; - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); assert_eq!(managarr_table.table_headers, Vec::::new()); assert_eq!(managarr_table.constraints, Vec::new()); assert_eq!(managarr_table.block, borderless_block()); @@ -77,20 +86,24 @@ mod tests { #[test] fn test_managarr_table_new_filter_box_populated() { let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); + let mut app = App::test_default(); + app.data.radarr_data.movies.set_items(items.clone().into_iter().map(|s| { + let mut movie = crate::models::radarr_models::Movie::default(); + movie.title = crate::models::HorizontallyScrollableText::from(s); + movie + }).collect()); let horizontally_scrollable_test = HorizontallyScrollableText { text: "test".to_owned(), offset: AtomicUsize::new(3), }; - stateful_table.filter = Some(horizontally_scrollable_test); + app.data.radarr_data.movies.filter = Some(horizontally_scrollable_test); - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])); + let managarr_table = ManagarrTable::new( + &mut app, + |app| Some(&mut app.data.radarr_data.movies), + |movie: &crate::models::radarr_models::Movie| Row::new(vec![Cell::new(movie.title.to_string())]), + ); - let row_mapper = managarr_table.row_mapper; - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); assert_eq!(managarr_table.table_headers, Vec::::new()); assert_eq!(managarr_table.constraints, Vec::new()); assert_eq!(managarr_table.block, borderless_block()); @@ -111,18 +124,22 @@ mod tests { #[test] fn test_managarr_table_headers() { let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); + let mut app = App::test_default(); + app.data.radarr_data.movies.set_items(items.clone().into_iter().map(|s| { + let mut movie = crate::models::radarr_models::Movie::default(); + movie.title = crate::models::HorizontallyScrollableText::from(s); + movie + }).collect()); let headers = ["column 1", "column 2"]; - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .headers(headers); + let managarr_table = ManagarrTable::new( + &mut app, + |app| Some(&mut app.data.radarr_data.movies), + |movie: &crate::models::radarr_models::Movie| Row::new(vec![Cell::new(movie.title.to_string())]), + ) + .headers(headers); - let row_mapper = managarr_table.row_mapper; assert_eq!(managarr_table.table_headers, headers); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); assert_eq!(managarr_table.constraints, Vec::new()); assert_eq!(managarr_table.block, borderless_block()); assert_eq!(managarr_table.margin, 0); @@ -142,18 +159,22 @@ mod tests { #[test] fn test_managarr_table_constraints() { let items = vec!["item1", "item2", "item3"]; - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); + let mut app = App::test_default(); + app.data.radarr_data.movies.set_items(items.clone().into_iter().map(|s| { + let mut movie = crate::models::radarr_models::Movie::default(); + movie.title = crate::models::HorizontallyScrollableText::from(s); + movie + }).collect()); let constraints = [Constraint::Length(1), Constraint::Fill(1)]; - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .constraints(constraints); + let managarr_table = ManagarrTable::new( + &mut app, + |app| Some(&mut app.data.radarr_data.movies), + |movie: &crate::models::radarr_models::Movie| Row::new(vec![Cell::new(movie.title.to_string())]), + ) + .constraints(constraints); - let row_mapper = managarr_table.row_mapper; assert_eq!(managarr_table.constraints, constraints); - assert_eq!(managarr_table.content.unwrap().items, items); - assert_eq!(row_mapper(&"item1"), Row::new(vec![Cell::new("item1")])); assert_eq!(managarr_table.table_headers, Vec::::new()); assert_eq!(managarr_table.block, borderless_block()); assert_eq!(managarr_table.margin, 0); @@ -185,14 +206,21 @@ mod tests { }, ]); sort_list.scroll_down(); - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - stateful_table.sort = Some(sort_list); + let mut app = App::test_default(); + app.data.radarr_data.movies.set_items(items.clone().into_iter().map(|s| { + let mut movie = crate::models::radarr_models::Movie::default(); + movie.title = crate::models::HorizontallyScrollableText::from(s); + movie + }).collect()); + app.data.radarr_data.movies.sort = Some(sort_list); let headers = ["column 1", "column 2"]; - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .headers(headers); + let managarr_table = ManagarrTable::new( + &mut app, + |app| Some(&mut app.data.radarr_data.movies), + |movie: &crate::models::radarr_models::Movie| Row::new(vec![Cell::new(movie.title.to_string())]), + ) + .headers(headers); assert_eq!( managarr_table.parse_headers(), @@ -215,15 +243,22 @@ mod tests { }, ]); sort_list.scroll_down(); - let mut stateful_table = StatefulTable::default(); - stateful_table.set_items(items.clone()); - stateful_table.sort = Some(sort_list); - stateful_table.sort_asc = true; + let mut app = App::test_default(); + app.data.radarr_data.movies.set_items(items.clone().into_iter().map(|s| { + let mut movie = crate::models::radarr_models::Movie::default(); + movie.title = crate::models::HorizontallyScrollableText::from(s); + movie + }).collect()); + app.data.radarr_data.movies.sort = Some(sort_list); + app.data.radarr_data.movies.sort_asc = true; let headers = ["column 1", "column 2"]; - let managarr_table = - ManagarrTable::new(Some(&mut stateful_table), |&s| Row::new(vec![Cell::new(s)])) - .headers(headers); + let managarr_table = ManagarrTable::new( + &mut app, + |app| Some(&mut app.data.radarr_data.movies), + |movie: &crate::models::radarr_models::Movie| Row::new(vec![Cell::new(movie.title.to_string())]), + ) + .headers(headers); assert_eq!( managarr_table.parse_headers(), diff --git a/src/ui/widgets/popup.rs b/src/ui/widgets/popup.rs index 25caeb6..c359cb5 100644 --- a/src/ui/widgets/popup.rs +++ b/src/ui/widgets/popup.rs @@ -4,7 +4,7 @@ use derive_setters::Setters; use ratatui::buffer::Buffer; use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::widgets::{Block, Clear, Widget}; -use tachyonfx::{fx, Interpolation}; +use tachyonfx::{Interpolation, fx}; #[cfg(test)] #[path = "popup_tests.rs"] @@ -78,7 +78,7 @@ where percent_y: 0, margin: 0, block: None, - app: None + app: None, } } @@ -119,14 +119,13 @@ where self.widget.render(content_area, buf); if let Some(app) = self.app - && !app.has_active_effect { - let timer = (100, Interpolation::Linear); - let fx = - fx::coalesce(timer) - .with_area(content_area); - app.effects.add_effect(fx); - app.has_active_effect = true; - } + && !app.has_active_effect + { + let timer = (100, Interpolation::Linear); + let fx = fx::coalesce(timer).with_area(content_area); + app.effects.add_effect(fx); + app.has_active_effect = true; + } } }