Refactored tables and loading blocks to use the new dedicated widgets for Tables and Loading blocks

This commit is contained in:
2024-02-10 19:23:19 -07:00
parent 68de986c48
commit 51b789fd0f
19 changed files with 1174 additions and 1150 deletions
+7 -102
View File
@@ -4,15 +4,13 @@ use ratatui::layout::{Alignment, Constraint, Flex, Layout, Rect};
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Text};
use ratatui::widgets::Paragraph;
use ratatui::widgets::Row;
use ratatui::widgets::Table;
use ratatui::widgets::Tabs;
use ratatui::widgets::{Block, Wrap};
use ratatui::widgets::Wrap;
use ratatui::widgets::{Clear, List, ListItem};
use ratatui::Frame;
use crate::app::App;
use crate::models::{HorizontallyScrollableText, Route, StatefulList, StatefulTable, TabState};
use crate::models::{HorizontallyScrollableText, Route, StatefulList, TabState};
use crate::ui::radarr_ui::RadarrUi;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{
@@ -22,6 +20,7 @@ use crate::ui::utils::{
use crate::ui::widgets::button::Button;
use crate::ui::widgets::checkbox::Checkbox;
use crate::ui::widgets::input_box::InputBox;
use crate::ui::widgets::loading_block::LoadingBlock;
mod radarr_ui;
mod styles;
@@ -275,14 +274,6 @@ fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) -
content_area
}
pub struct TableProps<'a, T> {
pub content: Option<&'a mut StatefulTable<T>>,
pub wrapped_content: Option<Option<&'a mut StatefulTable<T>>>,
pub table_headers: Vec<&'a str>,
pub constraints: Vec<Constraint>,
pub help: Option<String>,
}
pub struct ListProps<'a, T> {
pub content: &'a mut StatefulList<T>,
pub title: &'static str,
@@ -291,94 +282,6 @@ pub struct ListProps<'a, T> {
pub help: Option<String>,
}
fn draw_table<'a, T, F>(
f: &mut Frame<'_>,
area: Rect,
block: Block<'_>,
table_props: TableProps<'a, T>,
row_mapper: F,
is_loading: bool,
highlight: bool,
) where
F: Fn(&T) -> Row<'a>,
{
let TableProps {
content,
wrapped_content,
table_headers,
constraints,
help,
} = table_props;
let content_area = draw_help_footer_and_get_content_area(f, area, help);
#[allow(clippy::unnecessary_unwrap)]
if wrapped_content.is_some() && wrapped_content.as_ref().unwrap().is_some() {
draw_table_contents(
f,
block,
row_mapper,
highlight,
wrapped_content.unwrap().as_mut().unwrap(),
table_headers,
constraints,
content_area,
);
} else if content.is_some() && !content.as_ref().unwrap().items.is_empty() {
draw_table_contents(
f,
block,
row_mapper,
highlight,
content.unwrap(),
table_headers,
constraints,
content_area,
);
} else {
loading(f, block, content_area, is_loading);
}
}
#[allow(clippy::too_many_arguments)]
fn draw_table_contents<'a, T, F>(
f: &mut Frame<'_>,
block: Block<'_>,
row_mapper: F,
highlight: bool,
content: &mut StatefulTable<T>,
table_headers: Vec<&str>,
constraints: Vec<Constraint>,
area: Rect,
) where
F: Fn(&T) -> Row<'a>,
{
let rows = content.items.iter().map(row_mapper);
let headers = Row::new(table_headers).default().bold().bottom_margin(0);
let mut table = Table::new(rows, &constraints).header(headers).block(block);
if highlight {
table = table
.highlight_style(Style::new().highlight())
.highlight_symbol(HIGHLIGHT_SYMBOL);
}
f.render_stateful_widget(table, area, &mut content.state);
}
pub fn loading(f: &mut Frame<'_>, block: Block<'_>, area: Rect, is_loading: bool) {
if is_loading {
let paragraph = Paragraph::new(Text::from("\n\n Loading ...\n\n"))
.system_function()
.block(block);
f.render_widget(paragraph, area);
} else {
f.render_widget(block, area)
}
}
pub fn draw_prompt_box(
f: &mut Frame<'_>,
area: Rect,
@@ -532,7 +435,7 @@ pub fn draw_list_box<'a, T>(
f.render_stateful_widget(list, content_area, &mut content.state);
} else {
loading(f, block, content_area, is_loading);
f.render_widget(LoadingBlock::new(is_loading, block), content_area);
}
}
@@ -543,7 +446,9 @@ fn draw_help_footer_and_get_content_area(
) -> Rect {
if let Some(help_string) = help {
let [content_area, help_footer_area] =
Layout::vertical([Constraint::Fill(0), Constraint::Length(2)]).areas(area);
Layout::vertical([Constraint::Fill(0), Constraint::Length(2)])
.margin(1)
.areas(area);
let help_paragraph = Paragraph::new(Text::from(format!(" {help_string}").help()))
.block(layout_block_top_border())
@@ -18,7 +18,8 @@ use crate::ui::utils::{
borderless_block, get_width_from_percentage, layout_block_top_border_with_title, title_block,
title_style,
};
use crate::ui::{draw_large_popup_over, draw_small_popup_over, draw_table, DrawUi, TableProps};
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::{draw_large_popup_over, draw_small_popup_over, DrawUi};
use crate::utils::convert_runtime;
#[cfg(test)]
@@ -67,13 +68,10 @@ impl DrawUi for CollectionDetailsUi {
}
pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let [description_area, table_area, help_footer_area] = Layout::vertical([
Constraint::Percentage(25),
Constraint::Percentage(70),
Constraint::Percentage(5),
])
.margin(1)
.areas(area);
let [description_area, table_area] =
Layout::vertical([Constraint::Percentage(25), Constraint::Fill(0)])
.margin(1)
.areas(area);
let collection_selection =
if let Some(filtered_collections) = app.data.radarr_data.filtered_collections.as_ref() {
filtered_collections.current_selection()
@@ -97,13 +95,63 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
.current_selection()
.clone()
};
let help_text = Text::from(
format!(
"<↑↓> scroll table | {}",
build_context_clue_string(&COLLECTION_DETAILS_CONTEXT_CLUES)
)
.help(),
);
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)
{
""
} else {
""
};
movie.title.scroll_left_or_reset(
get_width_from_percentage(table_area, 20),
current_selection == *movie,
app.tick_count % app.ticks_until_scroll == 0,
);
let (hours, minutes) = convert_runtime(movie.runtime);
let imdb_rating = movie
.ratings
.imdb
.clone()
.unwrap_or_default()
.value
.as_f64()
.unwrap();
let rotten_tomatoes_rating = movie
.ratings
.rotten_tomatoes
.clone()
.unwrap_or_default()
.value
.as_u64()
.unwrap();
let imdb_rating = if imdb_rating == 0.0 {
String::new()
} else {
format!("{imdb_rating:.1}")
};
let rotten_tomatoes_rating = if rotten_tomatoes_rating == 0 {
String::new()
} else {
format!("{rotten_tomatoes_rating}%")
};
Row::new(vec![
Cell::from(in_library),
Cell::from(movie.title.to_string()),
Cell::from(movie.year.to_string()),
Cell::from(format!("{hours}h {minutes}m")),
Cell::from(imdb_rating),
Cell::from(rotten_tomatoes_rating),
Cell::from(movie.genres.join(", ")),
])
.primary()
};
let monitored = if collection_selection.monitored {
"Yes"
} else {
@@ -115,10 +163,14 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
"No"
};
let minimum_availability = collection_selection.minimum_availability.to_display_str();
let help_footer = format!(
"<↑↓> scroll table | {}",
build_context_clue_string(&COLLECTION_DETAILS_CONTEXT_CLUES)
);
let collection_description = Text::from(vec![
Line::from(vec![
"Overview ".primary().bold(),
"Overview: ".primary().bold(),
collection_selection
.overview
.clone()
@@ -151,102 +203,36 @@ pub fn draw_collection_details(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
let description_paragraph = Paragraph::new(collection_description)
.block(borderless_block())
.wrap(Wrap { trim: false });
let help_paragraph = Paragraph::new(help_text)
.block(borderless_block())
.alignment(Alignment::Center);
let movies_table = ManagarrTable::new(
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)
.footer_alignment(Alignment::Center)
.footer(Some(help_footer))
.headers([
"",
"Title",
"Year",
"Runtime",
"IMDB Rating",
"Rotten Tomatoes Rating",
"Genres",
])
.constraints([
Constraint::Percentage(2),
Constraint::Percentage(20),
Constraint::Percentage(8),
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(18),
Constraint::Percentage(28),
]);
f.render_widget(title_block(&collection_selection.title.text), area);
f.render_widget(description_paragraph, description_area);
f.render_widget(help_paragraph, help_footer_area);
draw_table(
f,
table_area,
layout_block_top_border_with_title(title_style("Movies")),
TableProps {
content: Some(&mut app.data.radarr_data.collection_movies),
wrapped_content: None,
table_headers: vec![
"",
"Title",
"Year",
"Runtime",
"IMDB Rating",
"Rotten Tomatoes Rating",
"Genres",
],
constraints: vec![
Constraint::Percentage(2),
Constraint::Percentage(20),
Constraint::Percentage(8),
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(18),
Constraint::Percentage(28),
],
help: None,
},
|movie| {
let in_library = if app
.data
.radarr_data
.movies
.items
.iter()
.any(|mov| mov.tmdb_id == movie.tmdb_id)
{
""
} else {
""
};
movie.title.scroll_left_or_reset(
get_width_from_percentage(table_area, 20),
current_selection == *movie,
app.tick_count % app.ticks_until_scroll == 0,
);
let (hours, minutes) = convert_runtime(movie.runtime);
let imdb_rating = movie
.ratings
.imdb
.clone()
.unwrap_or_default()
.value
.as_f64()
.unwrap();
let rotten_tomatoes_rating = movie
.ratings
.rotten_tomatoes
.clone()
.unwrap_or_default()
.value
.as_u64()
.unwrap();
let imdb_rating = if imdb_rating == 0.0 {
String::new()
} else {
format!("{imdb_rating:.1}")
};
let rotten_tomatoes_rating = if rotten_tomatoes_rating == 0 {
String::new()
} else {
format!("{rotten_tomatoes_rating}%")
};
Row::new(vec![
Cell::from(in_library),
Cell::from(movie.title.to_string()),
Cell::from(movie.year.to_string()),
Cell::from(format!("{hours}h {minutes}m")),
Cell::from(imdb_rating),
Cell::from(rotten_tomatoes_rating),
Cell::from(movie.genres.join(", ")),
])
.primary()
},
app.is_loading,
true,
);
f.render_widget(movies_table, table_area);
}
fn draw_movie_overview(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
+57 -61
View File
@@ -12,9 +12,10 @@ use crate::ui::radarr_ui::collections::collection_details_ui::CollectionDetailsU
use crate::ui::radarr_ui::collections::edit_collection_ui::EditCollectionUi;
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::{
draw_error_message_popup, draw_input_box_popup, draw_popup_over, draw_prompt_box,
draw_prompt_popup_over, draw_table, DrawUi, TableProps,
draw_prompt_popup_over, DrawUi,
};
mod collection_details_ui;
@@ -113,67 +114,62 @@ pub(super) fn draw_collections(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect)
Some(filtered_collections) if !app.data.radarr_data.is_filtering => Some(filtered_collections),
_ => Some(&mut app.data.radarr_data.collections),
};
draw_table(
f,
area,
layout_block_top_border(),
TableProps {
content,
wrapped_content: None,
table_headers: vec![
"Collection",
"Number of Movies",
"Root Folder Path",
"Quality Profile",
"Search on Add",
"Monitored",
],
constraints: vec![
Constraint::Percentage(25),
Constraint::Percentage(15),
Constraint::Percentage(15),
Constraint::Percentage(15),
Constraint::Percentage(15),
Constraint::Percentage(15),
],
help: app
.data
.radarr_data
.main_tabs
.get_active_tab_contextual_help(),
},
|collection| {
let number_of_movies = collection.movies.clone().unwrap_or_default().len();
collection.title.scroll_left_or_reset(
get_width_from_percentage(area, 25),
*collection == current_selection,
app.tick_count % app.ticks_until_scroll == 0,
);
let monitored = if collection.monitored { "🏷" } else { "" };
let search_on_add = if collection.search_on_add {
"Yes"
} else {
"No"
};
let collections_table_footer = app
.data
.radarr_data
.main_tabs
.get_active_tab_contextual_help();
let collection_row_mapping = |collection: &Collection| {
let number_of_movies = collection.movies.clone().unwrap_or_default().len();
collection.title.scroll_left_or_reset(
get_width_from_percentage(area, 25),
*collection == current_selection,
app.tick_count % app.ticks_until_scroll == 0,
);
let monitored = if collection.monitored { "🏷" } else { "" };
let search_on_add = if collection.search_on_add {
"Yes"
} else {
"No"
};
Row::new(vec![
Cell::from(collection.title.to_string()),
Cell::from(number_of_movies.to_string()),
Cell::from(collection.root_folder_path.clone().unwrap_or_default()),
Cell::from(
quality_profile_map
.get_by_left(&collection.quality_profile_id)
.unwrap()
.to_owned(),
),
Cell::from(search_on_add),
Cell::from(monitored),
])
.primary()
},
app.is_loading,
true,
);
Row::new(vec![
Cell::from(collection.title.to_string()),
Cell::from(number_of_movies.to_string()),
Cell::from(collection.root_folder_path.clone().unwrap_or_default()),
Cell::from(
quality_profile_map
.get_by_left(&collection.quality_profile_id)
.unwrap()
.to_owned(),
),
Cell::from(search_on_add),
Cell::from(monitored),
])
.primary()
};
let collections_table = ManagarrTable::new(content, collection_row_mapping)
.loading(app.is_loading)
.footer(collections_table_footer)
.block(layout_block_top_border())
.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),
]);
f.render_widget(collections_table, area);
}
fn draw_update_all_collections_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
+66 -67
View File
@@ -8,7 +8,8 @@ use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, DOWNLO
use crate::models::{HorizontallyScrollableText, Route};
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{get_width_from_percentage, layout_block_top_border};
use crate::ui::{draw_prompt_box, draw_prompt_popup_over, draw_table, DrawUi, TableProps};
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::{draw_prompt_box, draw_prompt_popup_over, DrawUi};
use crate::utils::convert_to_gb;
#[cfg(test)]
@@ -48,76 +49,74 @@ fn draw_downloads(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
} else {
app.data.radarr_data.downloads.current_selection().clone()
};
let downloads_table_footer = app
.data
.radarr_data
.main_tabs
.get_active_tab_contextual_help();
draw_table(
f,
area,
layout_block_top_border(),
TableProps {
content: Some(&mut app.data.radarr_data.downloads),
wrapped_content: None,
table_headers: vec![
"Title",
"Percent Complete",
"Size",
"Output Path",
"Indexer",
"Download Client",
],
constraints: vec![
Constraint::Percentage(30),
Constraint::Percentage(11),
Constraint::Percentage(11),
Constraint::Percentage(18),
Constraint::Percentage(17),
Constraint::Percentage(13),
],
help: app
.data
.radarr_data
.main_tabs
.get_active_tab_contextual_help(),
},
|download_record| {
let DownloadRecord {
title,
size,
sizeleft,
download_client,
indexer,
output_path,
..
} = download_record;
let downloads_row_mapping = |download_record: &DownloadRecord| {
let DownloadRecord {
title,
size,
sizeleft,
download_client,
indexer,
output_path,
..
} = download_record;
if output_path.is_some() {
output_path.as_ref().unwrap().scroll_left_or_reset(
get_width_from_percentage(area, 18),
current_selection == *download_record,
app.tick_count % app.ticks_until_scroll == 0,
);
}
if output_path.is_some() {
output_path.as_ref().unwrap().scroll_left_or_reset(
get_width_from_percentage(area, 18),
current_selection == *download_record,
app.tick_count % app.ticks_until_scroll == 0,
);
}
let percent = 1f64 - (*sizeleft as f64 / *size as f64);
let file_size: f64 = convert_to_gb(*size);
let percent = 1f64 - (*sizeleft as f64 / *size as f64);
let file_size: f64 = convert_to_gb(*size);
Row::new(vec![
Cell::from(title.to_owned()),
Cell::from(format!("{:.0}%", percent * 100.0)),
Cell::from(format!("{file_size:.2} GB")),
Cell::from(
output_path
.as_ref()
.unwrap_or(&HorizontallyScrollableText::default())
.to_string(),
),
Cell::from(indexer.to_owned()),
Cell::from(download_client.to_owned()),
])
.primary()
},
app.is_loading,
true,
);
Row::new(vec![
Cell::from(title.to_owned()),
Cell::from(format!("{:.0}%", percent * 100.0)),
Cell::from(format!("{file_size:.2} GB")),
Cell::from(
output_path
.as_ref()
.unwrap_or(&HorizontallyScrollableText::default())
.to_string(),
),
Cell::from(indexer.to_owned()),
Cell::from(download_client.to_owned()),
])
.primary()
};
let downloads_table = ManagarrTable::new(
Some(&mut app.data.radarr_data.downloads),
downloads_row_mapping,
)
.block(layout_block_top_border())
.loading(app.is_loading)
.footer(downloads_table_footer)
.headers([
"Title",
"Percent Complete",
"Size",
"Output Path",
"Indexer",
"Download Client",
])
.constraints([
Constraint::Percentage(30),
Constraint::Percentage(11),
Constraint::Percentage(11),
Constraint::Percentage(18),
Constraint::Percentage(17),
Constraint::Percentage(13),
]);
f.render_widget(downloads_table, area);
}
fn draw_delete_download_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
+3 -2
View File
@@ -8,7 +8,8 @@ use crate::ui::utils::title_block_centered;
use crate::ui::widgets::button::Button;
use crate::ui::widgets::checkbox::Checkbox;
use crate::ui::widgets::input_box::InputBox;
use crate::ui::{draw_popup_over, loading, DrawUi};
use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::{draw_popup_over, DrawUi};
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::Frame;
@@ -160,6 +161,6 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
f.render_widget(cancel_button, cancel_area);
}
} else {
loading(f, block, area, app.is_loading);
f.render_widget(LoadingBlock::new(app.is_loading, block), area);
}
}
@@ -13,7 +13,8 @@ use crate::ui::utils::title_block_centered;
use crate::ui::widgets::button::Button;
use crate::ui::widgets::checkbox::Checkbox;
use crate::ui::widgets::input_box::InputBox;
use crate::ui::{draw_popup_over, loading, DrawUi};
use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::{draw_popup_over, DrawUi};
#[cfg(test)]
#[path = "indexer_settings_ui_tests.rs"]
@@ -155,6 +156,6 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area:
f.render_widget(save_button, save_area);
f.render_widget(cancel_button, cancel_area);
} else {
loading(f, block, area, app.is_loading);
f.render_widget(LoadingBlock::new(app.is_loading, block), area);
}
}
+73 -74
View File
@@ -12,7 +12,8 @@ use crate::ui::radarr_ui::indexers::indexer_settings_ui::IndexerSettingsUi;
use crate::ui::radarr_ui::indexers::test_all_indexers_ui::TestAllIndexersUi;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::layout_block_top_border;
use crate::ui::{draw_prompt_box, draw_prompt_popup_over, draw_table, DrawUi, TableProps};
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::{draw_prompt_box, draw_prompt_popup_over, DrawUi};
mod edit_indexer_ui;
mod indexer_settings_ui;
@@ -59,83 +60,81 @@ impl DrawUi for IndexersUi {
}
fn draw_indexers(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
draw_table(
f,
area,
layout_block_top_border(),
TableProps {
content: Some(&mut app.data.radarr_data.indexers),
wrapped_content: None,
table_headers: vec![
"Indexer",
"RSS",
"Automatic Search",
"Interactive Search",
"Priority",
"Tags",
],
constraints: vec![
Constraint::Percentage(25),
Constraint::Percentage(13),
Constraint::Percentage(13),
Constraint::Percentage(13),
Constraint::Percentage(13),
Constraint::Percentage(23),
],
help: app
.data
.radarr_data
.main_tabs
.get_active_tab_contextual_help(),
},
|indexer: &'_ Indexer| {
let Indexer {
name,
enable_rss,
enable_automatic_search,
enable_interactive_search,
priority,
tags,
..
} = indexer;
let bool_to_text = |flag: bool| {
if flag {
return Text::from("Enabled").success();
}
let indexers_row_mapping = |indexer: &'_ Indexer| {
let Indexer {
name,
enable_rss,
enable_automatic_search,
enable_interactive_search,
priority,
tags,
..
} = indexer;
let bool_to_text = |flag: bool| {
if flag {
return Text::from("Enabled").success();
}
Text::from("Disabled").failure()
};
Text::from("Disabled").failure()
};
let rss = bool_to_text(*enable_rss);
let automatic_search = bool_to_text(*enable_automatic_search);
let interactive_search = bool_to_text(*enable_interactive_search);
let tags: String = tags
.iter()
.map(|tag_id| {
app
.data
.radarr_data
.tags_map
.get_by_left(&tag_id.as_i64().unwrap())
.unwrap()
.clone()
})
.collect::<Vec<String>>()
.join(", ");
let rss = bool_to_text(*enable_rss);
let automatic_search = bool_to_text(*enable_automatic_search);
let interactive_search = bool_to_text(*enable_interactive_search);
let tags: String = tags
.iter()
.map(|tag_id| {
app
.data
.radarr_data
.tags_map
.get_by_left(&tag_id.as_i64().unwrap())
.unwrap()
.clone()
})
.collect::<Vec<String>>()
.join(", ");
Row::new(vec![
Cell::from(name.clone().unwrap_or_default()),
Cell::from(rss),
Cell::from(automatic_search),
Cell::from(interactive_search),
Cell::from(priority.to_string()),
Cell::from(tags),
])
.primary()
},
app.is_loading,
true,
Row::new(vec![
Cell::from(name.clone().unwrap_or_default()),
Cell::from(rss),
Cell::from(automatic_search),
Cell::from(interactive_search),
Cell::from(priority.to_string()),
Cell::from(tags),
])
.primary()
};
let indexers_table_footer = app
.data
.radarr_data
.main_tabs
.get_active_tab_contextual_help();
let indexers_table = ManagarrTable::new(
Some(&mut app.data.radarr_data.indexers),
indexers_row_mapping,
)
.block(layout_block_top_border())
.footer(indexers_table_footer)
.loading(app.is_loading)
.headers([
"Indexer",
"RSS",
"Automatic Search",
"Interactive Search",
"Priority",
"Tags",
])
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(13),
Constraint::Percentage(13),
Constraint::Percentage(13),
Constraint::Percentage(13),
Constraint::Percentage(23),
]);
f.render_widget(indexers_table, area);
}
fn draw_delete_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
@@ -6,10 +6,9 @@ use crate::models::Route;
use crate::ui::radarr_ui::indexers::draw_indexers;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{borderless_block, get_width_from_percentage, title_block};
use crate::ui::{
draw_help_footer_and_get_content_area, draw_large_popup_over, draw_table, DrawUi, TableProps,
};
use ratatui::layout::{Constraint, Rect};
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::{draw_large_popup_over, DrawUi};
use ratatui::layout::{Alignment, Constraint, Rect};
use ratatui::widgets::{Cell, Row};
use ratatui::Frame;
@@ -47,47 +46,45 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are
IndexerTestResultModalItem::default()
};
f.render_widget(title_block("Test All Indexers"), area);
let help = Some(format!(
let help_footer = format!(
"<↑↓> scroll | {}",
build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES)
));
let content_area = draw_help_footer_and_get_content_area(f, area, help);
draw_table(
f,
content_area,
borderless_block(),
TableProps {
content: app.data.radarr_data.indexer_test_all_results.as_mut(),
wrapped_content: None,
table_headers: vec!["Indexer", "Pass/Fail", "Failure Messages"],
constraints: vec![
Constraint::Percentage(20),
Constraint::Percentage(10),
Constraint::Percentage(70),
],
help: None,
},
|result| {
result.validation_failures.scroll_left_or_reset(
get_width_from_percentage(area, 86),
*result == current_selection,
app.tick_count % app.ticks_until_scroll == 0,
);
let pass_fail = if result.is_valid { "" } else { "" };
let row = Row::new(vec![
Cell::from(result.name.to_owned()),
Cell::from(pass_fail.to_owned()),
Cell::from(result.validation_failures.to_string()),
]);
if result.is_valid {
row.success()
} else {
row.failure()
}
},
app.is_loading,
true,
);
let test_results_row_mapping = |result: &IndexerTestResultModalItem| {
result.validation_failures.scroll_left_or_reset(
get_width_from_percentage(area, 86),
*result == current_selection,
app.tick_count % app.ticks_until_scroll == 0,
);
let pass_fail = if result.is_valid { "" } else { "" };
let row = Row::new(vec![
Cell::from(result.name.to_owned()),
Cell::from(pass_fail.to_owned()),
Cell::from(result.validation_failures.to_string()),
]);
if result.is_valid {
row.success()
} else {
row.failure()
}
};
let indexers_test_results_table = ManagarrTable::new(
app.data.radarr_data.indexer_test_all_results.as_mut(),
test_results_row_mapping,
)
.block(borderless_block())
.loading(app.is_loading)
.footer(Some(help_footer))
.footer_alignment(Alignment::Center)
.margin(1)
.headers(["Indexer", "Pass/Fail", "Failure Messages"])
.constraints([
Constraint::Percentage(20),
Constraint::Percentage(10),
Constraint::Percentage(70),
]);
f.render_widget(indexers_test_results_table, area);
}
+92 -90
View File
@@ -18,9 +18,10 @@ use crate::ui::utils::{
};
use crate::ui::widgets::button::Button;
use crate::ui::widgets::input_box::InputBox;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::{
draw_drop_down_popup, draw_error_popup, draw_error_popup_over, draw_large_popup_over,
draw_medium_popup_over, draw_selectable_list, draw_table, DrawUi, TableProps,
draw_medium_popup_over, draw_selectable_list, DrawUi,
};
use crate::utils::convert_runtime;
use crate::{render_selectable_input_box, App};
@@ -116,6 +117,64 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.unwrap()
.offset
.borrow();
let search_results_row_mapping = |movie: &AddMovieSearchResult| {
let (hours, minutes) = convert_runtime(movie.runtime);
let imdb_rating = movie
.ratings
.imdb
.clone()
.unwrap_or_default()
.value
.as_f64()
.unwrap();
let rotten_tomatoes_rating = movie
.ratings
.rotten_tomatoes
.clone()
.unwrap_or_default()
.value
.as_u64()
.unwrap();
let imdb_rating = if imdb_rating == 0.0 {
String::new()
} else {
format!("{imdb_rating:.1}")
};
let rotten_tomatoes_rating = if rotten_tomatoes_rating == 0 {
String::new()
} else {
format!("{rotten_tomatoes_rating}%")
};
let in_library = if app
.data
.radarr_data
.movies
.items
.iter()
.any(|mov| mov.tmdb_id == movie.tmdb_id)
{
""
} else {
""
};
movie.title.scroll_left_or_reset(
get_width_from_percentage(area, 27),
*movie == current_selection,
app.tick_count % app.ticks_until_scroll == 0,
);
Row::new(vec![
Cell::from(in_library),
Cell::from(movie.title.to_string()),
Cell::from(movie.year.to_string()),
Cell::from(format!("{hours}h {minutes}m")),
Cell::from(imdb_rating),
Cell::from(rotten_tomatoes_rating),
Cell::from(movie.genres.join(", ")),
])
.primary()
};
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
match active_radarr_block {
@@ -134,8 +193,14 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
f.render_widget(help_paragraph, help_area);
}
ActiveRadarrBlock::AddMovieEmptySearchResults => {
let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help());
let help_paragraph = Paragraph::new(help_text)
.block(borderless_block())
.alignment(Alignment::Center);
f.render_widget(layout_block(), results_area);
draw_error_popup(f, "No movies found matching your query!");
f.render_widget(help_paragraph, help_area);
}
ActiveRadarrBlock::AddMovieSearchResults
| ActiveRadarrBlock::AddMoviePrompt
@@ -150,96 +215,33 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let help_paragraph = Paragraph::new(help_text)
.block(borderless_block())
.alignment(Alignment::Center);
let search_results_table = ManagarrTable::new(
app.data.radarr_data.add_searched_movies.as_mut(),
search_results_row_mapping,
)
.loading(is_loading)
.block(layout_block())
.headers([
"",
"Title",
"Year",
"Runtime",
"IMDB",
"Rotten Tomatoes",
"Genres",
])
.constraints([
Constraint::Percentage(2),
Constraint::Percentage(27),
Constraint::Percentage(8),
Constraint::Percentage(10),
Constraint::Percentage(8),
Constraint::Percentage(14),
Constraint::Percentage(28),
]);
f.render_widget(search_results_table, results_area);
f.render_widget(help_paragraph, help_area);
draw_table(
f,
results_area,
layout_block(),
TableProps {
content: None,
wrapped_content: Some(app.data.radarr_data.add_searched_movies.as_mut()),
table_headers: vec![
"",
"Title",
"Year",
"Runtime",
"IMDB",
"Rotten Tomatoes",
"Genres",
],
constraints: vec![
Constraint::Percentage(2),
Constraint::Percentage(27),
Constraint::Percentage(8),
Constraint::Percentage(10),
Constraint::Percentage(8),
Constraint::Percentage(14),
Constraint::Percentage(28),
],
help: None,
},
|movie| {
let (hours, minutes) = convert_runtime(movie.runtime);
let imdb_rating = movie
.ratings
.imdb
.clone()
.unwrap_or_default()
.value
.as_f64()
.unwrap();
let rotten_tomatoes_rating = movie
.ratings
.rotten_tomatoes
.clone()
.unwrap_or_default()
.value
.as_u64()
.unwrap();
let imdb_rating = if imdb_rating == 0.0 {
String::new()
} else {
format!("{imdb_rating:.1}")
};
let rotten_tomatoes_rating = if rotten_tomatoes_rating == 0 {
String::new()
} else {
format!("{rotten_tomatoes_rating}%")
};
let in_library = if app
.data
.radarr_data
.movies
.items
.iter()
.any(|mov| mov.tmdb_id == movie.tmdb_id)
{
""
} else {
""
};
movie.title.scroll_left_or_reset(
get_width_from_percentage(area, 27),
*movie == current_selection,
app.tick_count % app.ticks_until_scroll == 0,
);
Row::new(vec![
Cell::from(in_library),
Cell::from(movie.title.to_string()),
Cell::from(movie.year.to_string()),
Cell::from(format!("{hours}h {minutes}m")),
Cell::from(imdb_rating),
Cell::from(rotten_tomatoes_rating),
Cell::from(movie.genres.join(", ")),
])
.primary()
},
is_loading,
true,
);
}
_ => (),
}
+79 -83
View File
@@ -12,9 +12,10 @@ use crate::ui::radarr_ui::library::delete_movie_ui::DeleteMovieUi;
use crate::ui::radarr_ui::library::edit_movie_ui::EditMovieUi;
use crate::ui::radarr_ui::library::movie_details_ui::MovieDetailsUi;
use crate::ui::utils::{get_width_from_percentage, layout_block_top_border};
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::{
draw_error_message_popup, draw_input_box_popup, draw_popup_over, draw_prompt_box,
draw_prompt_popup_over, draw_table, DrawUi, TableProps,
draw_prompt_popup_over, DrawUi,
};
use crate::utils::{convert_runtime, convert_to_gb};
@@ -106,90 +107,85 @@ pub(super) fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
Some(filtered_movies) if !app.data.radarr_data.is_filtering => Some(filtered_movies),
_ => Some(&mut app.data.radarr_data.movies),
};
let help_footer = app
.data
.radarr_data
.main_tabs
.get_active_tab_contextual_help();
draw_table(
f,
area,
layout_block_top_border(),
TableProps {
content,
wrapped_content: None,
table_headers: vec![
"Title",
"Year",
"Studio",
"Runtime",
"Rating",
"Language",
"Size",
"Quality Profile",
"Monitored",
"Tags",
],
constraints: vec![
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),
],
help: app
.data
.radarr_data
.main_tabs
.get_active_tab_contextual_help(),
},
|movie| {
movie.title.scroll_left_or_reset(
get_width_from_percentage(area, 27),
*movie == current_selection,
app.tick_count % app.ticks_until_scroll == 0,
);
let monitored = if movie.monitored { "🏷" } else { "" };
let (hours, minutes) = convert_runtime(movie.runtime);
let file_size: f64 = convert_to_gb(movie.size_on_disk);
let certification = movie.certification.clone().unwrap_or_default();
let quality_profile = quality_profile_map
.get_by_left(&movie.quality_profile_id)
.unwrap()
.to_owned();
let tags = movie
.tags
.iter()
.map(|tag_id| {
tags_map
.get_by_left(&tag_id.as_i64().unwrap())
.unwrap()
.clone()
})
.collect::<Vec<String>>()
.join(", ");
let library_table_row_mapping = |movie: &Movie| {
movie.title.scroll_left_or_reset(
get_width_from_percentage(area, 27),
*movie == current_selection,
app.tick_count % app.ticks_until_scroll == 0,
);
let monitored = if movie.monitored { "🏷" } else { "" };
let (hours, minutes) = convert_runtime(movie.runtime);
let file_size: f64 = convert_to_gb(movie.size_on_disk);
let certification = movie.certification.clone().unwrap_or_default();
let quality_profile = quality_profile_map
.get_by_left(&movie.quality_profile_id)
.unwrap()
.to_owned();
let tags = movie
.tags
.iter()
.map(|tag_id| {
tags_map
.get_by_left(&tag_id.as_i64().unwrap())
.unwrap()
.clone()
})
.collect::<Vec<String>>()
.join(", ");
decorate_with_row_style(
downloads_vec,
movie,
Row::new(vec![
Cell::from(movie.title.to_string()),
Cell::from(movie.year.to_string()),
Cell::from(movie.studio.to_string()),
Cell::from(format!("{hours}h {minutes}m")),
Cell::from(certification),
Cell::from(movie.original_language.name.to_owned()),
Cell::from(format!("{file_size:.2} GB")),
Cell::from(quality_profile),
Cell::from(monitored.to_owned()),
Cell::from(tags),
]),
)
},
app.is_loading,
true,
);
decorate_with_row_style(
downloads_vec,
movie,
Row::new(vec![
Cell::from(movie.title.to_string()),
Cell::from(movie.year.to_string()),
Cell::from(movie.studio.to_string()),
Cell::from(format!("{hours}h {minutes}m")),
Cell::from(certification),
Cell::from(movie.original_language.name.to_owned()),
Cell::from(format!("{file_size:.2} GB")),
Cell::from(quality_profile),
Cell::from(monitored.to_owned()),
Cell::from(tags),
]),
)
};
let library_table = ManagarrTable::new(content, library_table_row_mapping)
.block(layout_block_top_border())
.loading(app.is_loading)
.footer(help_footer)
.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),
]);
f.render_widget(library_table, area);
}
fn draw_update_all_movies_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
+211 -222
View File
@@ -15,10 +15,11 @@ use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{
borderless_block, get_width_from_percentage, layout_block_bottom_border, layout_block_top_border,
};
use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::{
draw_drop_down_popup, draw_large_popup_over, draw_prompt_box, draw_prompt_box_with_content,
draw_prompt_popup_over, draw_selectable_list, draw_small_popup_over, draw_table, draw_tabs,
loading, DrawUi, TableProps,
draw_prompt_popup_over, draw_selectable_list, draw_small_popup_over, draw_tabs, DrawUi,
};
use crate::utils::convert_to_gb;
@@ -189,7 +190,10 @@ fn draw_file_info(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget(video_details_title_paragraph, video_details_title_area);
f.render_widget(video_details_paragraph, video_details_area);
}
_ => loading(f, layout_block_top_border(), area, app.is_loading),
_ => f.render_widget(
LoadingBlock::new(app.is_loading, layout_block_top_border()),
area,
),
}
}
@@ -230,11 +234,12 @@ fn draw_movie_details(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget(paragraph, area);
}
_ => loading(
f,
block,
_ => f.render_widget(
LoadingBlock::new(
app.is_loading || app.data.radarr_data.movie_details_modal.is_none(),
block,
),
area,
app.is_loading || app.data.radarr_data.movie_details_modal.is_none(),
),
}
}
@@ -249,148 +254,137 @@ fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
.current_selection()
.clone()
};
let history_row_mapping = |movie_history_item: &MovieHistoryItem| {
let MovieHistoryItem {
source_title,
quality,
languages,
date,
event_type,
} = movie_history_item;
draw_table(
f,
area,
layout_block_top_border(),
TableProps {
content: Some(&mut movie_details_modal.movie_history),
wrapped_content: None,
table_headers: vec!["Source Title", "Event Type", "Languages", "Quality", "Date"],
constraints: vec![
Constraint::Percentage(34),
Constraint::Percentage(17),
Constraint::Percentage(14),
Constraint::Percentage(14),
Constraint::Percentage(21),
],
help: app
.data
.radarr_data
.movie_info_tabs
.get_active_tab_contextual_help(),
},
|movie_history_item| {
let MovieHistoryItem {
source_title,
quality,
languages,
date,
event_type,
} = movie_history_item;
movie_history_item.source_title.scroll_left_or_reset(
get_width_from_percentage(area, 34),
current_selection == *movie_history_item,
app.tick_count % app.ticks_until_scroll == 0,
);
movie_history_item.source_title.scroll_left_or_reset(
get_width_from_percentage(area, 34),
current_selection == *movie_history_item,
app.tick_count % app.ticks_until_scroll == 0,
);
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()),
])
.success()
};
let help_footer = app
.data
.radarr_data
.movie_info_tabs
.get_active_tab_contextual_help();
let history_table = ManagarrTable::new(
Some(&mut movie_details_modal.movie_history),
history_row_mapping,
)
.block(layout_block_top_border())
.loading(app.is_loading)
.footer(help_footer)
.headers(["Source Title", "Event Type", "Languages", "Quality", "Date"])
.constraints([
Constraint::Percentage(34),
Constraint::Percentage(17),
Constraint::Percentage(14),
Constraint::Percentage(14),
Constraint::Percentage(21),
]);
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()),
])
.success()
},
app.is_loading,
true,
);
f.render_widget(history_table, area);
}
}
fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
draw_table(
f,
area,
layout_block_top_border(),
TableProps {
content: Some(
&mut app
.data
.radarr_data
.movie_details_modal
.as_mut()
.unwrap()
.movie_cast,
),
wrapped_content: None,
constraints: iter::repeat(Constraint::Ratio(1, 2)).take(2).collect(),
table_headers: vec!["Cast Member", "Character"],
help: app
.data
.radarr_data
.movie_info_tabs
.get_active_tab_contextual_help(),
},
|cast_member| {
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()),
])
.success()
},
app.is_loading,
true,
Row::new(vec![
Cell::from(person_name.to_owned()),
Cell::from(character.clone().unwrap_or_default()),
])
.success()
};
let content = Some(
&mut app
.data
.radarr_data
.movie_details_modal
.as_mut()
.unwrap()
.movie_cast,
);
let help_footer = app
.data
.radarr_data
.movie_info_tabs
.get_active_tab_contextual_help();
let cast_table = ManagarrTable::new(content, cast_row_mapping)
.block(layout_block_top_border())
.footer(help_footer)
.loading(app.is_loading)
.headers(["Cast Member", "Character"])
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
f.render_widget(cast_table, area);
}
fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
draw_table(
f,
area,
layout_block_top_border(),
TableProps {
content: Some(
&mut app
.data
.radarr_data
.movie_details_modal
.as_mut()
.unwrap()
.movie_crew,
),
wrapped_content: None,
constraints: iter::repeat(Constraint::Ratio(1, 3)).take(3).collect(),
table_headers: vec!["Crew Member", "Job", "Department"],
help: app
.data
.radarr_data
.movie_info_tabs
.get_active_tab_contextual_help(),
},
|crew_member| {
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()),
])
.success()
},
app.is_loading,
true,
Row::new(vec![
Cell::from(person_name.to_owned()),
Cell::from(job.clone().unwrap_or_default()),
Cell::from(department.clone().unwrap_or_default()),
])
.success()
};
let content = Some(
&mut app
.data
.radarr_data
.movie_details_modal
.as_mut()
.unwrap()
.movie_crew,
);
let help_footer = app
.data
.radarr_data
.movie_info_tabs
.get_active_tab_contextual_help();
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(Constraint::Ratio(1, 3)).take(3))
.footer(help_footer);
f.render_widget(crew_table, area);
}
fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
@@ -407,6 +401,11 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
_ => (Release::default(), true, None),
};
let current_route = *app.get_current_route();
let help_footer = app
.data
.radarr_data
.movie_info_tabs
.get_active_tab_contextual_help();
let mut table_headers_vec = vec![
"Source".to_owned(),
"Age".to_owned(),
@@ -442,99 +441,89 @@ fn draw_movie_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
ReleaseField::Quality => table_headers_vec[8].push_str(direction),
}
}
let content = Some(
&mut app
.data
.radarr_data
.movie_details_modal
.as_mut()
.unwrap()
.movie_releases,
);
let releases_row_mapping = |release: &Release| {
let Release {
protocol,
age,
title,
indexer,
size,
rejected,
seeders,
leechers,
languages,
quality,
..
} = release;
let age = format!("{age} days");
title.scroll_left_or_reset(
get_width_from_percentage(area, 30),
current_selection == *release
&& current_route != ActiveRadarrBlock::ManualSearchConfirmPrompt.into(),
app.tick_count % app.ticks_until_scroll == 0,
);
let size = convert_to_gb(*size);
let rejected_str = if *rejected { "" } else { "" };
let peers = if seeders.is_none() || leechers.is_none() {
Text::from("")
} else {
let seeders = seeders.clone().unwrap().as_u64().unwrap();
let leechers = leechers.clone().unwrap().as_u64().unwrap();
draw_table(
f,
area,
layout_block_top_border(),
TableProps {
content: Some(
&mut app
.data
.radarr_data
.movie_details_modal
.as_mut()
.unwrap()
.movie_releases,
),
wrapped_content: None,
constraints: vec![
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),
],
table_headers: table_headers_vec.iter().map(|s| &**s).collect(),
help: app
.data
.radarr_data
.movie_info_tabs
.get_active_tab_contextual_help(),
},
|release| {
let Release {
protocol,
age,
title,
indexer,
size,
rejected,
decorate_peer_style(
seeders,
leechers,
languages,
quality,
..
} = release;
let age = format!("{age} days");
title.scroll_left_or_reset(
get_width_from_percentage(area, 30),
current_selection == *release
&& current_route != ActiveRadarrBlock::ManualSearchConfirmPrompt.into(),
app.tick_count % app.ticks_until_scroll == 0,
);
let size = convert_to_gb(*size);
let rejected_str = if *rejected { "" } else { "" };
let peers = if seeders.is_none() || leechers.is_none() {
Text::from("")
} else {
let seeders = seeders.clone().unwrap().as_u64().unwrap();
let leechers = leechers.clone().unwrap().as_u64().unwrap();
Text::from(format!("{seeders} / {leechers}")),
)
};
decorate_peer_style(
seeders,
leechers,
Text::from(format!("{seeders} / {leechers}")),
)
};
let language = if languages.is_some() {
languages.clone().unwrap()[0].name.clone()
} else {
String::new()
};
let quality = quality.quality.name.clone();
let language = if languages.is_some() {
languages.clone().unwrap()[0].name.clone()
} else {
String::new()
};
let quality = quality.quality.name.clone();
Row::new(vec![
Cell::from(protocol.clone()),
Cell::from(age),
Cell::from(rejected_str),
Cell::from(title.to_string()),
Cell::from(indexer.clone()),
Cell::from(format!("{size:.1} GB")),
Cell::from(peers),
Cell::from(language),
Cell::from(quality),
])
.primary()
};
let releases_table = ManagarrTable::new(content, releases_row_mapping)
.block(layout_block_top_border())
.loading(app.is_loading || is_empty)
.footer(help_footer)
.headers(table_headers_vec.iter().map(|s| &**s))
.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),
]);
Row::new(vec![
Cell::from(protocol.clone()),
Cell::from(age),
Cell::from(rejected_str),
Cell::from(title.to_string()),
Cell::from(indexer.clone()),
Cell::from(format!("{size:.1} GB")),
Cell::from(peers),
Cell::from(language),
Cell::from(quality),
])
.primary()
},
app.is_loading || is_empty,
true,
);
f.render_widget(releases_table, area);
}
fn draw_manual_search_confirm_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
+3 -3
View File
@@ -13,7 +13,6 @@ use crate::models::radarr_models::{DiskSpace, DownloadRecord, Movie, RootFolder}
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
use crate::models::Route;
use crate::ui::draw_tabs;
use crate::ui::loading;
use crate::ui::radarr_ui::collections::CollectionsUi;
use crate::ui::radarr_ui::downloads::DownloadsUi;
use crate::ui::radarr_ui::indexers::IndexersUi;
@@ -24,6 +23,7 @@ use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{
borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block,
};
use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::DrawUi;
use crate::utils::convert_to_gb;
@@ -163,7 +163,7 @@ fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
)
}
} else {
loading(f, block, area, app.is_loading);
f.render_widget(LoadingBlock::new(app.is_loading, block), area);
}
}
@@ -195,7 +195,7 @@ fn draw_downloads_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
f.render_widget(download_gauge, download_item_areas[i]);
}
} else {
loading(f, block, area, app.is_loading);
f.render_widget(LoadingBlock::new(app.is_loading, block), area);
}
}
+44 -45
View File
@@ -8,9 +8,9 @@ use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ROOT_F
use crate::models::Route;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::layout_block_top_border;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::{
draw_input_box_popup, draw_popup_over, draw_prompt_box, draw_prompt_popup_over, draw_table,
DrawUi, TableProps,
draw_input_box_popup, draw_popup_over, draw_prompt_box, draw_prompt_popup_over, DrawUi,
};
use crate::utils::convert_to_gb;
@@ -56,51 +56,50 @@ impl DrawUi for RootFoldersUi {
}
fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
draw_table(
f,
area,
layout_block_top_border(),
TableProps {
content: Some(&mut app.data.radarr_data.root_folders),
wrapped_content: None,
table_headers: vec!["Path", "Free Space", "Unmapped Folders"],
constraints: vec![
Constraint::Percentage(60),
Constraint::Percentage(20),
Constraint::Percentage(20),
],
help: app
.data
.radarr_data
.main_tabs
.get_active_tab_contextual_help(),
},
|root_folders| {
let RootFolder {
path,
free_space,
unmapped_folders,
..
} = root_folders;
let help_footer = app
.data
.radarr_data
.main_tabs
.get_active_tab_contextual_help();
let root_folders_row_mapping = |root_folders: &RootFolder| {
let RootFolder {
path,
free_space,
unmapped_folders,
..
} = root_folders;
let space: f64 = convert_to_gb(*free_space);
let space: f64 = convert_to_gb(*free_space);
Row::new(vec![
Cell::from(path.to_owned()),
Cell::from(format!("{space:.2} GB")),
Cell::from(
unmapped_folders
.as_ref()
.unwrap_or(&Vec::new())
.len()
.to_string(),
),
])
.primary()
},
app.is_loading,
true,
);
Row::new(vec![
Cell::from(path.to_owned()),
Cell::from(format!("{space:.2} GB")),
Cell::from(
unmapped_folders
.as_ref()
.unwrap_or(&Vec::new())
.len()
.to_string(),
),
])
.primary()
};
let root_folders_table = ManagarrTable::new(
Some(&mut app.data.radarr_data.root_folders),
root_folders_row_mapping,
)
.block(layout_block_top_border())
.loading(app.is_loading)
.footer(help_footer)
.headers(["Path", "Free Space", "Unmapped Folders"])
.constraints([
Constraint::Ratio(3, 5),
Constraint::Ratio(1, 5),
Constraint::Ratio(1, 5),
]);
f.render_widget(root_folders_table, area);
}
fn draw_add_root_folder_prompt_box(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
+72 -80
View File
@@ -11,13 +11,14 @@ use ratatui::{
};
use crate::app::App;
use crate::models::radarr_models::Task;
use crate::models::radarr_models::{QueueEvent, Task};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::ui::radarr_ui::radarr_ui_utils::{convert_to_minutes_hours_days, style_log_list_item};
use crate::ui::radarr_ui::system::system_details_ui::SystemDetailsUi;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::layout_block_top_border;
use crate::ui::{draw_table, ListProps, TableProps};
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::ListProps;
use crate::{
models::Route,
ui::{draw_list_box, utils::title_block, DrawUi},
@@ -87,92 +88,83 @@ pub(super) fn draw_system_ui_layout(f: &mut Frame<'_>, app: &mut App<'_>, area:
}
fn draw_tasks(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
draw_table(
f,
area,
title_block("Tasks"),
TableProps {
content: Some(&mut app.data.radarr_data.tasks),
wrapped_content: None,
table_headers: TASK_TABLE_HEADERS.to_vec(),
constraints: TASK_TABLE_CONSTRAINTS.to_vec(),
help: None,
},
|task| {
let task_props = extract_task_props(task);
let tasks_row_mapping = |task: &Task| {
let task_props = extract_task_props(task);
Row::new(vec![
Cell::from(task_props.name),
Cell::from(task_props.interval),
Cell::from(task_props.last_execution),
Cell::from(task_props.last_duration),
Cell::from(task_props.next_execution),
])
.primary()
},
app.is_loading,
false,
);
Row::new(vec![
Cell::from(task_props.name),
Cell::from(task_props.interval),
Cell::from(task_props.last_execution),
Cell::from(task_props.last_duration),
Cell::from(task_props.next_execution),
])
.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);
f.render_widget(tasks_table, area);
}
pub(super) fn draw_queued_events(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
draw_table(
f,
area,
title_block("Queued Events"),
TableProps {
content: Some(&mut app.data.radarr_data.queued_events),
wrapped_content: None,
table_headers: vec!["Trigger", "Status", "Name", "Queued", "Started", "Duration"],
constraints: vec![
Constraint::Percentage(13),
Constraint::Percentage(13),
Constraint::Percentage(30),
Constraint::Percentage(16),
Constraint::Percentage(14),
Constraint::Percentage(14),
],
help: None,
},
|event| {
let queued = convert_to_minutes_hours_days(Utc::now().sub(event.queued).num_minutes());
let queued_string = if queued != "now" {
format!("{queued} ago")
} else {
queued
};
let started_string = if event.started.is_some() {
let started =
convert_to_minutes_hours_days(Utc::now().sub(event.started.unwrap()).num_minutes());
let events_row_mapping = |event: &QueueEvent| {
let queued = convert_to_minutes_hours_days(Utc::now().sub(event.queued).num_minutes());
let queued_string = if queued != "now" {
format!("{queued} ago")
} else {
queued
};
let started_string = if event.started.is_some() {
let started =
convert_to_minutes_hours_days(Utc::now().sub(event.started.unwrap()).num_minutes());
if started != "now" {
format!("{started} ago")
} else {
started
}
if started != "now" {
format!("{started} ago")
} else {
String::new()
};
started
}
} else {
String::new()
};
let duration = if event.duration.is_some() {
&event.duration.as_ref().unwrap()[..8]
} else {
""
};
let duration = if event.duration.is_some() {
&event.duration.as_ref().unwrap()[..8]
} else {
""
};
Row::new(vec![
Cell::from(event.trigger.clone()),
Cell::from(event.status.clone()),
Cell::from(event.command_name.clone()),
Cell::from(queued_string),
Cell::from(started_string),
Cell::from(duration.to_owned()),
])
.primary()
},
app.is_loading,
false,
);
Row::new(vec![
Cell::from(event.trigger.clone()),
Cell::from(event.status.clone()),
Cell::from(event.command_name.clone()),
Cell::from(queued_string),
Cell::from(started_string),
Cell::from(duration.to_owned()),
])
.primary()
};
let events_table = ManagarrTable::new(
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([
Constraint::Percentage(13),
Constraint::Percentage(13),
Constraint::Percentage(30),
Constraint::Percentage(16),
Constraint::Percentage(14),
Constraint::Percentage(14),
]);
f.render_widget(events_table, area);
}
fn draw_logs(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
+34 -37
View File
@@ -1,4 +1,4 @@
use ratatui::layout::Rect;
use ratatui::layout::{Alignment, Rect};
use ratatui::text::{Span, Text};
use ratatui::widgets::{Cell, ListItem, Paragraph, Row};
use ratatui::Frame;
@@ -6,6 +6,7 @@ use ratatui::Frame;
use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES};
use crate::app::radarr::radarr_context_clues::SYSTEM_TASKS_CONTEXT_CLUES;
use crate::app::App;
use crate::models::radarr_models::Task;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, SYSTEM_DETAILS_BLOCKS};
use crate::models::Route;
use crate::ui::radarr_ui::radarr_ui_utils::style_log_list_item;
@@ -15,10 +16,11 @@ use crate::ui::radarr_ui::system::{
};
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{borderless_block, title_block};
use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::{
draw_help_footer_and_get_content_area, draw_large_popup_over, draw_list_box,
draw_medium_popup_over, draw_prompt_box, draw_prompt_popup_over, draw_table, loading, DrawUi,
ListProps, TableProps,
draw_medium_popup_over, draw_prompt_box, draw_prompt_popup_over, DrawUi, ListProps,
};
#[cfg(test)]
@@ -82,40 +84,35 @@ fn draw_logs_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
let tasks_popup_table = |f: &mut Frame<'_>, app: &mut App<'_>, area: Rect| {
let help_footer = Some(build_context_clue_string(&SYSTEM_TASKS_CONTEXT_CLUES));
// let context_area = draw_help_footer_and_get_content_area(
// f,
// area,
// help_footer,
// );
let tasks_row_mapping = |task: &Task| {
let task_props = extract_task_props(task);
Row::new(vec![
Cell::from(task_props.name),
Cell::from(task_props.interval),
Cell::from(task_props.last_execution),
Cell::from(task_props.last_duration),
Cell::from(task_props.next_execution),
])
.primary()
};
let tasks_table = ManagarrTable::new(Some(&mut app.data.radarr_data.tasks), tasks_row_mapping)
.block(borderless_block())
.loading(app.is_loading)
.margin(1)
.footer(help_footer)
.footer_alignment(Alignment::Center)
.headers(TASK_TABLE_HEADERS)
.constraints(TASK_TABLE_CONSTRAINTS);
f.render_widget(title_block("Tasks"), area);
let context_area = draw_help_footer_and_get_content_area(
f,
area,
Some(build_context_clue_string(&SYSTEM_TASKS_CONTEXT_CLUES)),
);
draw_table(
f,
context_area,
borderless_block(),
TableProps {
content: Some(&mut app.data.radarr_data.tasks),
wrapped_content: None,
table_headers: TASK_TABLE_HEADERS.to_vec(),
constraints: TASK_TABLE_CONSTRAINTS.to_vec(),
help: None,
},
|task| {
let task_props = extract_task_props(task);
Row::new(vec![
Cell::from(task_props.name),
Cell::from(task_props.interval),
Cell::from(task_props.last_execution),
Cell::from(task_props.last_duration),
Cell::from(task_props.next_execution),
])
.primary()
},
app.is_loading,
true,
)
f.render_widget(tasks_table, area);
};
if matches!(
@@ -163,6 +160,6 @@ fn draw_updates_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
f.render_widget(updates_paragraph, content_area);
} else {
loading(f, block, content_area, app.is_loading);
f.render_widget(LoadingBlock::new(app.is_loading, block), content_area);
}
}
+33
View File
@@ -0,0 +1,33 @@
use crate::ui::styles::ManagarrStyle;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::prelude::Text;
use ratatui::widgets::{Block, Paragraph, Widget};
pub struct LoadingBlock<'a> {
is_loading: bool,
block: Block<'a>,
}
impl<'a> LoadingBlock<'a> {
pub fn new(is_loading: bool, block: Block<'a>) -> Self {
Self { is_loading, block }
}
fn render_loading_block(&self, area: Rect, buf: &mut Buffer) {
if self.is_loading {
Paragraph::new(Text::from("\n\n Loading ...\n\n"))
.system_function()
.block(self.block.clone())
.render(area, buf);
} else {
self.block.clone().render(area, buf);
}
}
}
impl<'a> Widget for LoadingBlock<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_loading_block(area, buf);
}
}
+148
View File
@@ -0,0 +1,148 @@
use crate::models::StatefulTable;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::layout_block_top_border;
use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::HIGHLIGHT_SYMBOL;
use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::prelude::{Style, Stylize, Text};
use ratatui::widgets::{Block, Paragraph, Row, StatefulWidget, Table, Widget};
pub struct ManagarrTable<'a, T, F>
where
F: Fn(&T) -> Row<'a>,
{
content: Option<&'a mut StatefulTable<T>>,
table_headers: Vec<Text<'a>>,
constraints: Vec<Constraint>,
row_mapper: F,
footer: Option<String>,
footer_alignment: Alignment,
block: Block<'a>,
margin: u16,
is_loading: bool,
highlight_rows: bool,
}
impl<'a, T, F> ManagarrTable<'a, T, F>
where
F: Fn(&T) -> Row<'a>,
{
pub fn new(content: Option<&'a mut StatefulTable<T>>, row_mapper: F) -> Self {
Self {
content,
table_headers: Vec::new(),
constraints: Vec::new(),
row_mapper,
footer: None,
footer_alignment: Alignment::Left,
block: Block::new(),
margin: 0,
is_loading: false,
highlight_rows: true,
}
}
pub fn headers<I>(mut self, headers: I) -> Self
where
I: IntoIterator,
I::Item: Into<Text<'a>>,
{
self.table_headers = headers.into_iter().map(Into::into).collect();
self
}
pub fn constraints<I>(mut self, constraints: I) -> Self
where
I: IntoIterator,
I::Item: Into<Constraint>,
{
self.constraints = constraints.into_iter().map(Into::into).collect();
self
}
pub fn footer(mut self, footer: Option<String>) -> Self {
self.footer = footer;
self
}
pub fn footer_alignment(mut self, alignment: Alignment) -> Self {
self.footer_alignment = alignment;
self
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = block;
self
}
pub fn margin(mut self, margin: u16) -> Self {
self.margin = margin;
self
}
pub fn loading(mut self, is_loading: bool) -> Self {
self.is_loading = is_loading;
self
}
pub fn highlight_rows(mut self, hightlight_rows: bool) -> Self {
self.highlight_rows = hightlight_rows;
self
}
fn render_table(&mut self, area: Rect, buf: &mut Buffer) {
let table_area = if let Some(ref footer) = self.footer {
let [content_area, footer_area] =
Layout::vertical([Constraint::Fill(0), Constraint::Length(2)])
.margin(self.margin)
.areas(area);
Paragraph::new(Text::from(format!(" {footer}").help()))
.block(layout_block_top_border())
.alignment(self.footer_alignment)
.render(footer_area, buf);
content_area
} else {
area
};
let loading_block = LoadingBlock::new(self.is_loading, self.block.clone());
if let Some(ref mut content) = self.content {
if !content.items.is_empty() {
let rows = content.items.iter().map(&self.row_mapper);
let headers = Row::new(self.table_headers.clone())
.default()
.bold()
.bottom_margin(0);
let mut table = Table::new(rows, &self.constraints)
.header(headers)
.block(self.block.clone());
if self.highlight_rows {
table = table
.highlight_style(Style::new().highlight())
.highlight_symbol(HIGHLIGHT_SYMBOL);
}
StatefulWidget::render(table, table_area, buf, &mut content.state);
} else {
loading_block.render(table_area, buf);
}
} else {
loading_block.render(table_area, buf);
}
}
}
impl<'a, T, F> Widget for ManagarrTable<'a, T, F>
where
F: Fn(&T) -> Row<'a>,
{
fn render(mut self, area: Rect, buf: &mut Buffer) {
self.render_table(area, buf);
}
}
+3 -1
View File
@@ -1,3 +1,5 @@
pub(super) mod button;
pub(super) mod input_box;
pub(super) mod checkbox;
pub(super) mod input_box;
pub(super) mod loading_block;
pub(super) mod managarr_table;