Compare commits

..

3 Commits

Author SHA1 Message Date
c8a06f3601 refactored managarr table initializer so a mutable app reference can be passed 2026-01-05 09:49:03 -07:00
50d4ddfa28 Merge branch 'refs/heads/main' into tachyonfx
# Conflicts:
#	src/app/mod.rs
#	src/main.rs
2025-12-19 13:45:35 -07:00
368f7505ff feat: Improved UI speed and responsiveness
Check / stable / fmt (push) Has been cancelled
Check / beta / clippy (push) Has been cancelled
Check / stable / clippy (push) Has been cancelled
Check / nightly / doc (push) Has been cancelled
Check / 1.89.0 / check (push) Has been cancelled
Test Suite / ubuntu / beta (push) Has been cancelled
Test Suite / ubuntu / stable (push) Has been cancelled
Test Suite / macos-latest / stable (push) Has been cancelled
Test Suite / windows-latest / stable (push) Has been cancelled
Test Suite / ubuntu / stable / coverage (push) Has been cancelled
2025-12-19 13:41:14 -07:00
35 changed files with 955 additions and 775 deletions
+1 -1
View File
@@ -273,10 +273,10 @@ impl App<'_> {
config: Some(ServarrConfig::default()),
},
]),
..App::default()
}
}
pub fn test_default_fully_populated() -> Self {
App {
data: Data {
+8 -8
View File
@@ -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;
+9 -12
View File
@@ -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);
+6 -4
View File
@@ -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",
@@ -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",
+42 -35
View File
@@ -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,
+5 -4
View File
@@ -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",
+6 -7
View File
@@ -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",
@@ -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)
+19 -14
View File
@@ -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<i64> = 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,
+49 -41
View File
@@ -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,
+151 -144
View File
@@ -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::<Vec<String>>()
.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::<Vec<String>>()
.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);
}
+2 -2
View File
@@ -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),
+11 -8
View File
@@ -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([
+8 -5
View File
@@ -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);
@@ -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
│ │
│ │
│ │
@@ -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 │
╰─────────────────────────────────────────────────────────────────────────────────────────╯
│ │
│ │
│ │
@@ -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
│ │
│ │
│ │
+4 -3
View File
@@ -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",
+4 -3
View File
@@ -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",
+25 -21
View File
@@ -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,
+5 -6
View File
@@ -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",
@@ -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)
+26 -14
View File
@@ -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,
+47 -36
View File
@@ -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",
+45 -42
View File
@@ -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,
+104 -87
View File
@@ -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);
}
+40 -41
View File
@@ -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,
+2 -2
View File
@@ -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),
+11 -8
View File
@@ -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([
+19 -11
View File
@@ -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),
+15 -10
View File
@@ -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;
}
}
}
+130 -89
View File
@@ -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<T>>,
{
#[setters(strip_option)]
content: Option<&'a mut StatefulTable<T>>,
#[setters(skip)]
app: &'a mut App<'b>,
#[setters(skip)]
content_accessor: G,
#[setters(skip)]
table_headers: Vec<String>,
#[setters(skip)]
constraints: Vec<Constraint>,
#[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<T>>,
{
pub fn new(content: Option<&'a mut StatefulTable<T>>, 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<I>(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<Text<'a>> {
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<T>>,
{
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_table(area, buf);
+88 -53
View File
@@ -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::<String>::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::<String>::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::<String>::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::<String>::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(),
+9 -10
View File
@@ -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;
}
}
}