feat(ui): Add series support Sonarr
This commit is contained in:
@@ -40,6 +40,11 @@ pub static BLOCKLIST_CONTEXT_CLUES: [ContextClue; 5] = [
|
||||
(DEFAULT_KEYBINDINGS.clear, "clear blocklist"),
|
||||
];
|
||||
|
||||
pub static CONFIRMATION_PROMPT_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.confirm, "submit"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel"),
|
||||
];
|
||||
|
||||
pub static DOWNLOADS_CONTEXT_CLUES: [ContextClue; 3] = [
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
|
||||
@@ -3,9 +3,9 @@ mod test {
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
|
||||
use crate::app::context_clues::{
|
||||
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES,
|
||||
INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, SERVARR_CONTEXT_CLUES,
|
||||
SYSTEM_CONTEXT_CLUES,
|
||||
BARE_POPUP_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
|
||||
DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES,
|
||||
SERVARR_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::{context_clues::build_context_clue_string, key_binding::DEFAULT_KEYBINDINGS};
|
||||
|
||||
@@ -106,6 +106,22 @@ mod test {
|
||||
assert_eq!(blocklist_context_clues_iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_confirmation_prompt_context_clues() {
|
||||
let mut confirmation_prompt_context_clues_iter = CONFIRMATION_PROMPT_CONTEXT_CLUES.iter();
|
||||
|
||||
let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap();
|
||||
|
||||
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.confirm);
|
||||
assert_str_eq!(*description, "submit");
|
||||
|
||||
let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap();
|
||||
|
||||
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
|
||||
assert_str_eq!(*description, "cancel");
|
||||
assert_eq!(confirmation_prompt_context_clues_iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_root_folders_context_clues() {
|
||||
let mut root_folders_context_clues_iter = ROOT_FOLDERS_CONTEXT_CLUES.iter();
|
||||
|
||||
@@ -66,11 +66,6 @@ pub static ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.esc, "edit search"),
|
||||
];
|
||||
|
||||
pub static CONFIRMATION_PROMPT_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.confirm, "submit"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel"),
|
||||
];
|
||||
|
||||
pub static SYSTEM_TASKS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.submit, "start task"),
|
||||
(DEFAULT_KEYBINDINGS.esc, DEFAULT_KEYBINDINGS.esc.desc),
|
||||
|
||||
@@ -5,7 +5,7 @@ mod tests {
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::radarr::radarr_context_clues::{
|
||||
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES,
|
||||
COLLECTION_DETAILS_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES,
|
||||
COLLECTION_DETAILS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES,
|
||||
MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES,
|
||||
MOVIE_DETAILS_CONTEXT_CLUES, SYSTEM_TASKS_CONTEXT_CLUES,
|
||||
};
|
||||
@@ -213,22 +213,6 @@ mod tests {
|
||||
assert_eq!(add_movie_search_results_context_clues_iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_confirmation_prompt_context_clues() {
|
||||
let mut confirmation_prompt_context_clues_iter = CONFIRMATION_PROMPT_CONTEXT_CLUES.iter();
|
||||
|
||||
let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap();
|
||||
|
||||
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.confirm);
|
||||
assert_str_eq!(*description, "submit");
|
||||
|
||||
let (key_binding, description) = confirmation_prompt_context_clues_iter.next().unwrap();
|
||||
|
||||
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
|
||||
assert_str_eq!(*description, "cancel");
|
||||
assert_eq!(confirmation_prompt_context_clues_iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_tasks_context_clues() {
|
||||
let mut system_tasks_context_clues_iter = SYSTEM_TASKS_CONTEXT_CLUES.iter();
|
||||
|
||||
@@ -108,6 +108,11 @@ impl<'a> App<'a> {
|
||||
.dispatch_network_event(SonarrEvent::GetLogs(None).into())
|
||||
.await;
|
||||
}
|
||||
ActiveSonarrBlock::AddSeriesSearchResults => {
|
||||
self
|
||||
.dispatch_network_event(SonarrEvent::SearchNewSeries(None).into())
|
||||
.await;
|
||||
}
|
||||
ActiveSonarrBlock::SystemUpdates => {
|
||||
self
|
||||
.dispatch_network_event(SonarrEvent::GetUpdates.into())
|
||||
|
||||
@@ -4,6 +4,11 @@ use crate::app::{context_clues::ContextClue, key_binding::DEFAULT_KEYBINDINGS};
|
||||
#[path = "sonarr_context_clues_tests.rs"]
|
||||
mod sonarr_context_clues_tests;
|
||||
|
||||
pub static ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES: [ContextClue; 2] = [
|
||||
(DEFAULT_KEYBINDINGS.submit, "details"),
|
||||
(DEFAULT_KEYBINDINGS.esc, "edit search"),
|
||||
];
|
||||
|
||||
pub static SERIES_CONTEXT_CLUES: [ContextClue; 10] = [
|
||||
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
|
||||
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
|
||||
|
||||
@@ -5,13 +5,30 @@ mod tests {
|
||||
use crate::app::{
|
||||
key_binding::DEFAULT_KEYBINDINGS,
|
||||
sonarr::sonarr_context_clues::{
|
||||
EPISODE_DETAILS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES,
|
||||
MANUAL_EPISODE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXT_CLUES,
|
||||
MANUAL_SEASON_SEARCH_CONTEXT_CLUES, SEASON_DETAILS_CONTEXT_CLUES, SERIES_CONTEXT_CLUES,
|
||||
SERIES_DETAILS_CONTEXT_CLUES,
|
||||
ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES, EPISODE_DETAILS_CONTEXT_CLUES,
|
||||
HISTORY_CONTEXT_CLUES, MANUAL_EPISODE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
|
||||
MANUAL_EPISODE_SEARCH_CONTEXT_CLUES, MANUAL_SEASON_SEARCH_CONTEXT_CLUES,
|
||||
SEASON_DETAILS_CONTEXT_CLUES, SERIES_CONTEXT_CLUES, SERIES_DETAILS_CONTEXT_CLUES,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_add_series_search_results_context_clues() {
|
||||
let mut add_series_search_results_context_clues_iter =
|
||||
ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES.iter();
|
||||
|
||||
let (key_binding, description) = add_series_search_results_context_clues_iter.next().unwrap();
|
||||
|
||||
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
|
||||
assert_str_eq!(*description, "details");
|
||||
|
||||
let (key_binding, description) = add_series_search_results_context_clues_iter.next().unwrap();
|
||||
|
||||
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.esc);
|
||||
assert_str_eq!(*description, "edit search");
|
||||
assert_eq!(add_series_search_results_context_clues_iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_series_context_clues() {
|
||||
let mut series_context_clues_iter = SERIES_CONTEXT_CLUES.iter();
|
||||
|
||||
@@ -348,6 +348,23 @@ mod tests {
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_add_movie_search_results_block() {
|
||||
let (mut app, mut sync_network_rx) = construct_app_unit();
|
||||
|
||||
app
|
||||
.dispatch_by_sonarr_block(&ActiveSonarrBlock::AddSeriesSearchResults)
|
||||
.await;
|
||||
|
||||
assert!(app.is_loading);
|
||||
assert_eq!(
|
||||
sync_network_rx.recv().await.unwrap(),
|
||||
SonarrEvent::SearchNewSeries(None).into()
|
||||
);
|
||||
assert!(!app.data.sonarr_data.prompt_confirm);
|
||||
assert_eq!(app.tick_count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_check_for_sonarr_prompt_action_no_prompt_confirm() {
|
||||
let mut app = App::default();
|
||||
|
||||
@@ -5,8 +5,7 @@ use ratatui::text::Text;
|
||||
use ratatui::widgets::{ListItem, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::context_clues::build_context_clue_string;
|
||||
use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES;
|
||||
use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES};
|
||||
use crate::app::App;
|
||||
use crate::models::servarr_data::radarr::modals::EditCollectionModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::app::context_clues::build_context_clue_string;
|
||||
use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES;
|
||||
use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES};
|
||||
use crate::app::App;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
|
||||
use crate::models::Route;
|
||||
|
||||
@@ -5,8 +5,7 @@ use ratatui::text::Text;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::context_clues::build_context_clue_string;
|
||||
use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES;
|
||||
use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES};
|
||||
use crate::app::App;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
ActiveRadarrBlock, INDEXER_SETTINGS_BLOCKS,
|
||||
|
||||
@@ -5,10 +5,10 @@ use ratatui::text::Text;
|
||||
use ratatui::widgets::{Cell, ListItem, Paragraph, Row};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::context_clues::{build_context_clue_string, BARE_POPUP_CONTEXT_CLUES};
|
||||
use crate::app::radarr::radarr_context_clues::{
|
||||
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
|
||||
use crate::app::context_clues::{
|
||||
build_context_clue_string, BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::radarr::radarr_context_clues::ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES;
|
||||
use crate::models::radarr_models::AddMovieSearchResult;
|
||||
use crate::models::servarr_data::radarr::modals::AddMovieModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, ADD_MOVIE_BLOCKS};
|
||||
|
||||
@@ -6,8 +6,7 @@ use ratatui::text::Text;
|
||||
use ratatui::widgets::{ListItem, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::context_clues::build_context_clue_string;
|
||||
use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES;
|
||||
use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES};
|
||||
use crate::app::App;
|
||||
use crate::models::servarr_data::radarr::modals::EditMovieModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::text::Text;
|
||||
use ratatui::widgets::{Cell, ListItem, Paragraph, Row};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::context_clues::{
|
||||
build_context_clue_string, BARE_POPUP_CONTEXT_CLUES, CONFIRMATION_PROMPT_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::app::sonarr::sonarr_context_clues::ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES;
|
||||
use crate::models::servarr_data::sonarr::modals::AddSeriesModal;
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ADD_SERIES_BLOCKS};
|
||||
use crate::models::sonarr_models::AddSeriesSearchResult;
|
||||
use crate::models::{EnumDisplayStyle, Route};
|
||||
use crate::ui::sonarr_ui::library::draw_library;
|
||||
use crate::ui::styles::ManagarrStyle;
|
||||
use crate::ui::utils::{
|
||||
borderless_block, get_width_from_percentage, layout_block, layout_paragraph_borderless,
|
||||
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::widgets::managarr_table::ManagarrTable;
|
||||
use crate::ui::widgets::message::Message;
|
||||
use crate::ui::widgets::popup::{Popup, Size};
|
||||
use crate::ui::widgets::selectable_list::SelectableList;
|
||||
use crate::ui::{draw_popup_over, DrawUi};
|
||||
use crate::{render_selectable_input_box, App};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "add_series_ui_tests.rs"]
|
||||
mod add_series_ui_tests;
|
||||
|
||||
pub(super) struct AddSeriesUi;
|
||||
|
||||
impl DrawUi for AddSeriesUi {
|
||||
fn accepts(route: Route) -> bool {
|
||||
if let Route::Sonarr(active_sonarr_block, _) = route {
|
||||
return ADD_SERIES_BLOCKS.contains(&active_sonarr_block);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
|
||||
let draw_add_series_search_popup =
|
||||
|f: &mut Frame<'_>, app: &mut App<'_>, area: Rect| match active_sonarr_block {
|
||||
ActiveSonarrBlock::AddSeriesSearchInput
|
||||
| ActiveSonarrBlock::AddSeriesSearchResults
|
||||
| ActiveSonarrBlock::AddSeriesEmptySearchResults => {
|
||||
draw_add_series_search(f, app, area);
|
||||
}
|
||||
ActiveSonarrBlock::AddSeriesPrompt
|
||||
| ActiveSonarrBlock::AddSeriesSelectMonitor
|
||||
| ActiveSonarrBlock::AddSeriesSelectSeriesType
|
||||
| ActiveSonarrBlock::AddSeriesSelectQualityProfile
|
||||
| ActiveSonarrBlock::AddSeriesSelectLanguageProfile
|
||||
| ActiveSonarrBlock::AddSeriesSelectRootFolder
|
||||
| ActiveSonarrBlock::AddSeriesTagsInput => {
|
||||
draw_popup_over(
|
||||
f,
|
||||
app,
|
||||
area,
|
||||
draw_add_series_search,
|
||||
draw_confirmation_popup,
|
||||
Size::Long,
|
||||
);
|
||||
}
|
||||
ActiveSonarrBlock::AddSeriesAlreadyInLibrary => {
|
||||
draw_add_series_search(f, app, area);
|
||||
f.render_widget(
|
||||
Popup::new(Message::new("This film is already in your library")).size(Size::Message),
|
||||
f.area(),
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
match active_sonarr_block {
|
||||
_ if ADD_SERIES_BLOCKS.contains(&active_sonarr_block) => draw_popup_over(
|
||||
f,
|
||||
app,
|
||||
area,
|
||||
draw_library,
|
||||
draw_add_series_search_popup,
|
||||
Size::Large,
|
||||
),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||
let is_loading = app.is_loading || app.data.sonarr_data.add_searched_series.is_none();
|
||||
let current_selection =
|
||||
if let Some(add_searched_series) = app.data.sonarr_data.add_searched_series.as_ref() {
|
||||
add_searched_series.current_selection().clone()
|
||||
} else {
|
||||
AddSeriesSearchResult::default()
|
||||
};
|
||||
|
||||
let [search_box_area, results_area, help_area] = Layout::vertical([
|
||||
Constraint::Length(3),
|
||||
Constraint::Fill(0),
|
||||
Constraint::Length(3),
|
||||
])
|
||||
.margin(1)
|
||||
.areas(area);
|
||||
let block_content = &app
|
||||
.data
|
||||
.sonarr_data
|
||||
.add_series_search
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.text;
|
||||
let offset = app
|
||||
.data
|
||||
.sonarr_data
|
||||
.add_series_search
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.offset
|
||||
.load(Ordering::SeqCst);
|
||||
let search_results_row_mapping = |series: &AddSeriesSearchResult| {
|
||||
let rating = series.ratings.clone().unwrap_or_default().value;
|
||||
let series_rating = if rating == 0.0 {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{rating:.1}")
|
||||
};
|
||||
let in_library = if app
|
||||
.data
|
||||
.sonarr_data
|
||||
.series
|
||||
.items
|
||||
.iter()
|
||||
.any(|mov| mov.tvdb_id == series.tvdb_id)
|
||||
{
|
||||
"✔"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let network = series.network.clone().unwrap_or_default();
|
||||
let seasons = if let Some(ref stats) = series.statistics {
|
||||
format!("{}", stats.season_count)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
series.title.scroll_left_or_reset(
|
||||
get_width_from_percentage(area, 27),
|
||||
*series == current_selection,
|
||||
app.tick_count % app.ticks_until_scroll == 0,
|
||||
);
|
||||
|
||||
Row::new(vec![
|
||||
Cell::from(in_library),
|
||||
Cell::from(series.title.to_string()),
|
||||
Cell::from(series.year.to_string()),
|
||||
Cell::from(network),
|
||||
Cell::from(series_rating),
|
||||
Cell::from(seasons),
|
||||
Cell::from(series.genres.join(", ")),
|
||||
])
|
||||
.primary()
|
||||
};
|
||||
|
||||
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
|
||||
match active_sonarr_block {
|
||||
ActiveSonarrBlock::AddSeriesSearchInput => {
|
||||
let search_box = InputBox::new(block_content)
|
||||
.offset(offset)
|
||||
.block(title_block_centered("Add Series"));
|
||||
let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help());
|
||||
let help_paragraph = Paragraph::new(help_text)
|
||||
.block(borderless_block())
|
||||
.centered();
|
||||
|
||||
search_box.show_cursor(f, search_box_area);
|
||||
f.render_widget(layout_block(), results_area);
|
||||
f.render_widget(search_box, search_box_area);
|
||||
f.render_widget(help_paragraph, help_area);
|
||||
}
|
||||
ActiveSonarrBlock::AddSeriesEmptySearchResults => {
|
||||
let help_text = Text::from(build_context_clue_string(&BARE_POPUP_CONTEXT_CLUES).help());
|
||||
let help_paragraph = Paragraph::new(help_text)
|
||||
.block(borderless_block())
|
||||
.centered();
|
||||
let error_message = Message::new("No series found matching your query!");
|
||||
let error_message_popup = Popup::new(error_message).size(Size::Message);
|
||||
|
||||
f.render_widget(layout_block(), results_area);
|
||||
f.render_widget(error_message_popup, f.area());
|
||||
f.render_widget(help_paragraph, help_area);
|
||||
}
|
||||
ActiveSonarrBlock::AddSeriesSearchResults
|
||||
| ActiveSonarrBlock::AddSeriesPrompt
|
||||
| ActiveSonarrBlock::AddSeriesSelectMonitor
|
||||
| ActiveSonarrBlock::AddSeriesSelectSeriesType
|
||||
| ActiveSonarrBlock::AddSeriesSelectQualityProfile
|
||||
| ActiveSonarrBlock::AddSeriesSelectLanguageProfile
|
||||
| ActiveSonarrBlock::AddSeriesSelectRootFolder
|
||||
| ActiveSonarrBlock::AddSeriesAlreadyInLibrary
|
||||
| ActiveSonarrBlock::AddSeriesTagsInput => {
|
||||
let help_text =
|
||||
Text::from(build_context_clue_string(&ADD_SERIES_SEARCH_RESULTS_CONTEXT_CLUES).help());
|
||||
let help_paragraph = Paragraph::new(help_text)
|
||||
.block(borderless_block())
|
||||
.centered();
|
||||
let search_results_table = ManagarrTable::new(
|
||||
app.data.sonarr_data.add_searched_series.as_mut(),
|
||||
search_results_row_mapping,
|
||||
)
|
||||
.loading(is_loading)
|
||||
.block(layout_block())
|
||||
.headers([
|
||||
"✔", "Title", "Year", "Network", "Seasons", "Rating", "Genres",
|
||||
])
|
||||
.constraints([
|
||||
Constraint::Percentage(2),
|
||||
Constraint::Percentage(27),
|
||||
Constraint::Percentage(9),
|
||||
Constraint::Percentage(13),
|
||||
Constraint::Percentage(9),
|
||||
Constraint::Percentage(9),
|
||||
Constraint::Percentage(28),
|
||||
]);
|
||||
|
||||
f.render_widget(search_results_table, results_area);
|
||||
f.render_widget(help_paragraph, help_area);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
f.render_widget(
|
||||
InputBox::new(block_content)
|
||||
.offset(offset)
|
||||
.block(title_block_centered("Add Series")),
|
||||
search_box_area,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_confirmation_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
|
||||
match active_sonarr_block {
|
||||
ActiveSonarrBlock::AddSeriesSelectMonitor => {
|
||||
draw_confirmation_prompt(f, app, area);
|
||||
draw_add_series_select_monitor_popup(f, app);
|
||||
}
|
||||
ActiveSonarrBlock::AddSeriesSelectSeriesType => {
|
||||
draw_confirmation_prompt(f, app, area);
|
||||
draw_add_series_select_series_type_popup(f, app);
|
||||
}
|
||||
ActiveSonarrBlock::AddSeriesSelectQualityProfile => {
|
||||
draw_confirmation_prompt(f, app, area);
|
||||
draw_add_series_select_quality_profile_popup(f, app);
|
||||
}
|
||||
ActiveSonarrBlock::AddSeriesSelectLanguageProfile => {
|
||||
draw_confirmation_prompt(f, app, area);
|
||||
draw_add_series_select_language_profile_popup(f, app);
|
||||
}
|
||||
ActiveSonarrBlock::AddSeriesSelectRootFolder => {
|
||||
draw_confirmation_prompt(f, app, area);
|
||||
draw_add_series_select_root_folder_popup(f, app);
|
||||
}
|
||||
ActiveSonarrBlock::AddSeriesPrompt | ActiveSonarrBlock::AddSeriesTagsInput => {
|
||||
draw_confirmation_prompt(f, app, area)
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||
let (series_title, series_overview) = (
|
||||
&app
|
||||
.data
|
||||
.sonarr_data
|
||||
.add_searched_series
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.current_selection()
|
||||
.title
|
||||
.text,
|
||||
app
|
||||
.data
|
||||
.sonarr_data
|
||||
.add_searched_series
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.current_selection()
|
||||
.overview
|
||||
.clone()
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
let title = format!("Add Series - {series_title}");
|
||||
let prompt = series_overview;
|
||||
let yes_no_value = app.data.sonarr_data.prompt_confirm;
|
||||
let selected_block = app.data.sonarr_data.selected_block.get_active_block();
|
||||
let highlight_yes_no = selected_block == ActiveSonarrBlock::AddSeriesConfirmPrompt;
|
||||
let AddSeriesModal {
|
||||
monitor_list,
|
||||
series_type_list,
|
||||
quality_profile_list,
|
||||
language_profile_list,
|
||||
root_folder_list,
|
||||
use_season_folder,
|
||||
tags,
|
||||
..
|
||||
} = app.data.sonarr_data.add_series_modal.as_ref().unwrap();
|
||||
|
||||
let selected_monitor = monitor_list.current_selection();
|
||||
let selected_series_type = series_type_list.current_selection();
|
||||
let selected_quality_profile = quality_profile_list.current_selection();
|
||||
let selected_language_profile = language_profile_list.current_selection();
|
||||
let selected_root_folder = root_folder_list.current_selection();
|
||||
|
||||
f.render_widget(title_block_centered(&title), area);
|
||||
|
||||
let [paragraph_area, root_folder_area, monitor_area, quality_profile_area, language_profile_area, series_type_area, season_folder_area, tags_area, _, buttons_area, help_area] =
|
||||
Layout::vertical([
|
||||
Constraint::Length(7),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.margin(1)
|
||||
.areas(area);
|
||||
|
||||
let prompt_paragraph = layout_paragraph_borderless(&prompt);
|
||||
let help_text = Text::from(build_context_clue_string(&CONFIRMATION_PROMPT_CONTEXT_CLUES).help());
|
||||
let help_paragraph = Paragraph::new(help_text).centered();
|
||||
f.render_widget(prompt_paragraph, paragraph_area);
|
||||
f.render_widget(help_paragraph, help_area);
|
||||
|
||||
let [add_area, cancel_area] =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.areas(buttons_area);
|
||||
|
||||
let use_season_folder_checkbox = Checkbox::new("Season Folder")
|
||||
.checked(*use_season_folder)
|
||||
.highlighted(selected_block == ActiveSonarrBlock::AddSeriesToggleUseSeasonFolder);
|
||||
let root_folder_drop_down_button = Button::new()
|
||||
.title(&selected_root_folder.path)
|
||||
.label("Root Folder")
|
||||
.icon("▼")
|
||||
.selected(selected_block == ActiveSonarrBlock::AddSeriesSelectRootFolder);
|
||||
let monitor_drop_down_button = Button::new()
|
||||
.title(selected_monitor.to_display_str())
|
||||
.label("Monitor")
|
||||
.icon("▼")
|
||||
.selected(selected_block == ActiveSonarrBlock::AddSeriesSelectMonitor);
|
||||
let series_type_drop_down_button = Button::new()
|
||||
.title(selected_series_type.to_display_str())
|
||||
.label("Series Type")
|
||||
.icon("▼")
|
||||
.selected(selected_block == ActiveSonarrBlock::AddSeriesSelectSeriesType);
|
||||
let quality_profile_drop_down_button = Button::new()
|
||||
.title(selected_quality_profile)
|
||||
.label("Quality Profile")
|
||||
.icon("▼")
|
||||
.selected(selected_block == ActiveSonarrBlock::AddSeriesSelectQualityProfile);
|
||||
let language_profile_drop_down_button = Button::new()
|
||||
.title(selected_language_profile)
|
||||
.label("Language Profile")
|
||||
.icon("▼")
|
||||
.selected(selected_block == ActiveSonarrBlock::AddSeriesSelectLanguageProfile);
|
||||
|
||||
f.render_widget(root_folder_drop_down_button, root_folder_area);
|
||||
f.render_widget(monitor_drop_down_button, monitor_area);
|
||||
f.render_widget(quality_profile_drop_down_button, quality_profile_area);
|
||||
f.render_widget(language_profile_drop_down_button, language_profile_area);
|
||||
f.render_widget(series_type_drop_down_button, series_type_area);
|
||||
f.render_widget(use_season_folder_checkbox, season_folder_area);
|
||||
|
||||
if let Route::Sonarr(active_sonarr_block, _) = app.get_current_route() {
|
||||
let tags_input_box = InputBox::new(&tags.text)
|
||||
.offset(tags.offset.load(Ordering::SeqCst))
|
||||
.label("Tags")
|
||||
.highlighted(selected_block == ActiveSonarrBlock::AddSeriesTagsInput)
|
||||
.selected(active_sonarr_block == ActiveSonarrBlock::AddSeriesTagsInput);
|
||||
render_selectable_input_box!(tags_input_box, f, tags_area);
|
||||
}
|
||||
|
||||
let add_button = Button::new()
|
||||
.title("Add")
|
||||
.selected(yes_no_value && highlight_yes_no);
|
||||
let cancel_button = Button::new()
|
||||
.title("Cancel")
|
||||
.selected(!yes_no_value && highlight_yes_no);
|
||||
|
||||
f.render_widget(add_button, add_area);
|
||||
f.render_widget(cancel_button, cancel_area);
|
||||
}
|
||||
|
||||
fn draw_add_series_select_monitor_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
|
||||
let monitor_list = SelectableList::new(
|
||||
&mut app
|
||||
.data
|
||||
.sonarr_data
|
||||
.add_series_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.monitor_list,
|
||||
|monitor| ListItem::new(monitor.to_display_str().to_owned()),
|
||||
);
|
||||
let popup = Popup::new(monitor_list).size(Size::Dropdown);
|
||||
|
||||
f.render_widget(popup, f.area());
|
||||
}
|
||||
|
||||
fn draw_add_series_select_series_type_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
|
||||
let series_type_list = SelectableList::new(
|
||||
&mut app
|
||||
.data
|
||||
.sonarr_data
|
||||
.add_series_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.series_type_list,
|
||||
|series_type| ListItem::new(series_type.to_display_str().to_owned()),
|
||||
);
|
||||
let popup = Popup::new(series_type_list).size(Size::Dropdown);
|
||||
|
||||
f.render_widget(popup, f.area());
|
||||
}
|
||||
|
||||
fn draw_add_series_select_quality_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
|
||||
let quality_profile_list = SelectableList::new(
|
||||
&mut app
|
||||
.data
|
||||
.sonarr_data
|
||||
.add_series_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.quality_profile_list,
|
||||
|quality_profile| ListItem::new(quality_profile.clone()),
|
||||
);
|
||||
let popup = Popup::new(quality_profile_list).size(Size::Dropdown);
|
||||
|
||||
f.render_widget(popup, f.area());
|
||||
}
|
||||
|
||||
fn draw_add_series_select_language_profile_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
|
||||
let language_profile_list = SelectableList::new(
|
||||
&mut app
|
||||
.data
|
||||
.sonarr_data
|
||||
.add_series_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.language_profile_list,
|
||||
|language_profile| ListItem::new(language_profile.clone()),
|
||||
);
|
||||
let popup = Popup::new(language_profile_list).size(Size::Dropdown);
|
||||
|
||||
f.render_widget(popup, f.area());
|
||||
}
|
||||
|
||||
fn draw_add_series_select_root_folder_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
|
||||
let root_folder_list = SelectableList::new(
|
||||
&mut app
|
||||
.data
|
||||
.sonarr_data
|
||||
.add_series_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.root_folder_list,
|
||||
|root_folder| ListItem::new(root_folder.path.to_owned()),
|
||||
);
|
||||
let popup = Popup::new(root_folder_list).size(Size::Dropdown);
|
||||
|
||||
f.render_widget(popup, f.area());
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ADD_SERIES_BLOCKS};
|
||||
use crate::ui::sonarr_ui::library::add_series_ui::AddSeriesUi;
|
||||
use crate::ui::DrawUi;
|
||||
|
||||
#[test]
|
||||
fn test_add_series_ui_accepts() {
|
||||
ActiveSonarrBlock::iter().for_each(|active_sonarr_block| {
|
||||
if ADD_SERIES_BLOCKS.contains(&active_sonarr_block) {
|
||||
assert!(AddSeriesUi::accepts(active_sonarr_block.into()));
|
||||
} else {
|
||||
assert!(!AddSeriesUi::accepts(active_sonarr_block.into()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, DELETE_SERIES_BLOCKS};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{
|
||||
ActiveSonarrBlock, ADD_SERIES_BLOCKS, DELETE_SERIES_BLOCKS,
|
||||
};
|
||||
use crate::models::{
|
||||
servarr_data::sonarr::sonarr_data::LIBRARY_BLOCKS, sonarr_models::SeriesStatus,
|
||||
};
|
||||
@@ -21,6 +23,7 @@ mod tests {
|
||||
fn test_library_ui_accepts() {
|
||||
let mut library_ui_blocks = Vec::new();
|
||||
library_ui_blocks.extend(LIBRARY_BLOCKS);
|
||||
library_ui_blocks.extend(ADD_SERIES_BLOCKS);
|
||||
library_ui_blocks.extend(DELETE_SERIES_BLOCKS);
|
||||
|
||||
ActiveSonarrBlock::iter().for_each(|active_radarr_block| {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use add_series_ui::AddSeriesUi;
|
||||
use delete_series_ui::DeleteSeriesUi;
|
||||
use ratatui::{
|
||||
layout::{Constraint, Rect},
|
||||
@@ -26,6 +27,7 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
mod add_series_ui;
|
||||
mod delete_series_ui;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -37,7 +39,9 @@ pub(super) struct LibraryUi;
|
||||
impl DrawUi for LibraryUi {
|
||||
fn accepts(route: Route) -> bool {
|
||||
if let Route::Sonarr(active_sonarr_block, _) = route {
|
||||
return DeleteSeriesUi::accepts(route) || LIBRARY_BLOCKS.contains(&active_sonarr_block);
|
||||
return AddSeriesUi::accepts(route)
|
||||
|| DeleteSeriesUi::accepts(route)
|
||||
|| LIBRARY_BLOCKS.contains(&active_sonarr_block);
|
||||
}
|
||||
|
||||
false
|
||||
@@ -90,6 +94,7 @@ impl DrawUi for LibraryUi {
|
||||
};
|
||||
|
||||
match route {
|
||||
_ if AddSeriesUi::accepts(route) => AddSeriesUi::draw(f, app, area),
|
||||
_ if DeleteSeriesUi::accepts(route) => DeleteSeriesUi::draw(f, app, area),
|
||||
Route::Sonarr(active_sonarr_block, _) if LIBRARY_BLOCKS.contains(&active_sonarr_block) => {
|
||||
series_ui_matchers(active_sonarr_block)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::app::context_clues::build_context_clue_string;
|
||||
use crate::app::radarr::radarr_context_clues::CONFIRMATION_PROMPT_CONTEXT_CLUES;
|
||||
use crate::app::context_clues::{build_context_clue_string, CONFIRMATION_PROMPT_CONTEXT_CLUES};
|
||||
use crate::ui::styles::ManagarrStyle;
|
||||
use crate::ui::utils::{layout_paragraph_borderless, title_block_centered};
|
||||
use crate::ui::widgets::button::Button;
|
||||
|
||||
@@ -21,6 +21,7 @@ pub enum Size {
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
Long,
|
||||
}
|
||||
|
||||
impl Size {
|
||||
@@ -37,6 +38,7 @@ impl Size {
|
||||
Size::Small => (40, 40),
|
||||
Size::Medium => (60, 60),
|
||||
Size::Large => (75, 75),
|
||||
Size::Long => (65, 80),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ mod tests {
|
||||
assert_eq!(Size::Small.to_percent(), (40, 40));
|
||||
assert_eq!(Size::Medium.to_percent(), (60, 60));
|
||||
assert_eq!(Size::Large.to_percent(), (75, 75));
|
||||
assert_eq!(Size::Long.to_percent(), (65, 80));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user