A series' overview renders as one line of a description box that does not scroll, so a long overview overflows with no way to read the rest. Pressing v on the series details Seasons tab now opens a popup holding the full overview. The popup owns its own ScrollableText, built fresh from the series' overview when it opens and torn down on Esc so a reopened popup starts at the top. Scrolling mutates only that owned state. The background paragraph renders the Series record's overview, which carries no scroll offset, so it cannot scroll along with the popup. Cite the existing view keybinding from the series details context clues and give the popup its own clue set so it does not inherit the hints of the view underneath. Enrich the shared series fixture to a multi-paragraph overview, and the raw response fixture that encodes the same series, so a scrolled popup is distinguishable from an unscrolled one.
383 lines
12 KiB
Rust
383 lines
12 KiB
Rust
use super::series_overview_handler::SeriesOverviewHandler;
|
|
use crate::app::App;
|
|
use crate::event::Key;
|
|
use crate::handlers::sonarr_handlers::history::history_sorting_options;
|
|
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
|
use crate::handlers::{KeyEventHandler, handle_prompt_toggle};
|
|
use crate::matches_key;
|
|
use crate::models::servarr_data::sonarr::modals::SeriesOverviewModal;
|
|
use crate::models::servarr_data::sonarr::sonarr_data::{
|
|
ActiveSonarrBlock, EDIT_SERIES_SELECTION_BLOCKS, SERIES_DETAILS_BLOCKS,
|
|
};
|
|
use crate::models::sonarr_models::{Season, SonarrHistoryItem};
|
|
use crate::models::{BlockSelectionState, Route, ScrollableText};
|
|
use crate::network::sonarr_network::SonarrEvent;
|
|
|
|
#[cfg(test)]
|
|
#[path = "series_details_handler_tests.rs"]
|
|
mod series_details_handler_tests;
|
|
|
|
pub(super) struct SeriesDetailsHandler<'a, 'b> {
|
|
key: Key,
|
|
app: &'a mut App<'b>,
|
|
active_sonarr_block: ActiveSonarrBlock,
|
|
_context: Option<ActiveSonarrBlock>,
|
|
}
|
|
|
|
impl SeriesDetailsHandler<'_, '_> {
|
|
fn extract_series_id_season_number_tuple(&self) -> (i64, i64) {
|
|
let series_id = self.app.data.sonarr_data.series.current_selection().id;
|
|
let season_number = self
|
|
.app
|
|
.data
|
|
.sonarr_data
|
|
.seasons
|
|
.current_selection()
|
|
.season_number;
|
|
|
|
(series_id, season_number)
|
|
}
|
|
|
|
fn extract_series_id(&self) -> i64 {
|
|
self.app.data.sonarr_data.series.current_selection().id
|
|
}
|
|
|
|
fn build_series_overview_modal(&mut self) {
|
|
let overview = self
|
|
.app
|
|
.data
|
|
.sonarr_data
|
|
.series
|
|
.current_selection()
|
|
.overview
|
|
.clone()
|
|
.unwrap_or_default();
|
|
|
|
self.app.data.sonarr_data.series_overview_modal = Some(SeriesOverviewModal {
|
|
overview: ScrollableText::with_string(overview),
|
|
});
|
|
}
|
|
}
|
|
|
|
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for SeriesDetailsHandler<'a, 'b> {
|
|
fn handle(&mut self) {
|
|
if SeriesOverviewHandler::accepts(self.active_sonarr_block) {
|
|
return SeriesOverviewHandler::new(
|
|
self.key,
|
|
self.app,
|
|
self.active_sonarr_block,
|
|
self._context,
|
|
)
|
|
.handle();
|
|
}
|
|
|
|
let season_table_handling_config =
|
|
TableHandlingConfig::new(ActiveSonarrBlock::SeriesDetails.into())
|
|
.searching_block(ActiveSonarrBlock::SearchSeason.into())
|
|
.search_error_block(ActiveSonarrBlock::SearchSeasonError.into())
|
|
.search_field_fn(|season: &Season| {
|
|
season
|
|
.title
|
|
.as_ref()
|
|
.expect("Season was not populated with title in handlers")
|
|
});
|
|
let series_history_table_handling_config =
|
|
TableHandlingConfig::new(ActiveSonarrBlock::SeriesHistory.into())
|
|
.sorting_block(ActiveSonarrBlock::SeriesHistorySortPrompt.into())
|
|
.sort_options(history_sorting_options())
|
|
.searching_block(ActiveSonarrBlock::SearchSeriesHistory.into())
|
|
.search_error_block(ActiveSonarrBlock::SearchSeriesHistoryError.into())
|
|
.search_field_fn(|history_item: &SonarrHistoryItem| &history_item.source_title.text)
|
|
.filtering_block(ActiveSonarrBlock::FilterSeriesHistory.into())
|
|
.filter_error_block(ActiveSonarrBlock::FilterSeriesHistoryError.into())
|
|
.filter_field_fn(|history_item: &SonarrHistoryItem| &history_item.source_title.text);
|
|
|
|
if !handle_table(
|
|
self,
|
|
|app| &mut app.data.sonarr_data.seasons,
|
|
season_table_handling_config,
|
|
) && !handle_table(
|
|
self,
|
|
|app| {
|
|
app
|
|
.data
|
|
.sonarr_data
|
|
.series_history
|
|
.as_mut()
|
|
.expect("Series history is undefined")
|
|
},
|
|
series_history_table_handling_config,
|
|
) {
|
|
self.handle_key_event();
|
|
}
|
|
}
|
|
|
|
fn accepts(active_block: ActiveSonarrBlock) -> bool {
|
|
SeriesOverviewHandler::accepts(active_block) || SERIES_DETAILS_BLOCKS.contains(&active_block)
|
|
}
|
|
|
|
fn ignore_special_keys(&self) -> bool {
|
|
self.app.ignore_special_keys_for_textbox_input
|
|
}
|
|
|
|
fn new(
|
|
key: Key,
|
|
app: &'a mut App<'b>,
|
|
active_block: ActiveSonarrBlock,
|
|
_context: Option<ActiveSonarrBlock>,
|
|
) -> SeriesDetailsHandler<'a, 'b> {
|
|
SeriesDetailsHandler {
|
|
key,
|
|
app,
|
|
active_sonarr_block: active_block,
|
|
_context,
|
|
}
|
|
}
|
|
|
|
fn get_key(&self) -> Key {
|
|
self.key
|
|
}
|
|
|
|
fn is_ready(&self) -> bool {
|
|
if self.active_sonarr_block == ActiveSonarrBlock::SeriesHistory {
|
|
!self.app.is_loading && self.app.data.sonarr_data.series_history.is_some()
|
|
} else {
|
|
!self.app.is_loading
|
|
}
|
|
}
|
|
|
|
fn handle_scroll_up(&mut self) {}
|
|
|
|
fn handle_scroll_down(&mut self) {}
|
|
|
|
fn handle_home(&mut self) {}
|
|
|
|
fn handle_end(&mut self) {}
|
|
|
|
fn handle_delete(&mut self) {}
|
|
|
|
fn handle_left_right_action(&mut self) {
|
|
match self.active_sonarr_block {
|
|
ActiveSonarrBlock::SeriesDetails | ActiveSonarrBlock::SeriesHistory => match self.key {
|
|
_ if matches_key!(left, self.key) => {
|
|
self.app.data.sonarr_data.series_info_tabs.previous();
|
|
self.app.pop_and_push_navigation_stack(
|
|
self
|
|
.app
|
|
.data
|
|
.sonarr_data
|
|
.series_info_tabs
|
|
.get_active_route(),
|
|
);
|
|
}
|
|
_ if matches_key!(right, self.key) => {
|
|
self.app.data.sonarr_data.series_info_tabs.next();
|
|
self.app.pop_and_push_navigation_stack(
|
|
self
|
|
.app
|
|
.data
|
|
.sonarr_data
|
|
.series_info_tabs
|
|
.get_active_route(),
|
|
);
|
|
}
|
|
_ => (),
|
|
},
|
|
ActiveSonarrBlock::UpdateAndScanSeriesPrompt
|
|
| ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => {
|
|
handle_prompt_toggle(self.app, self.key)
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
fn handle_submit(&mut self) {
|
|
match self.active_sonarr_block {
|
|
ActiveSonarrBlock::SeriesDetails if !self.app.data.sonarr_data.seasons.is_empty() => {
|
|
self
|
|
.app
|
|
.push_navigation_stack(ActiveSonarrBlock::SeasonDetails.into());
|
|
}
|
|
ActiveSonarrBlock::SeriesHistory
|
|
if !self
|
|
.app
|
|
.data
|
|
.sonarr_data
|
|
.series_history
|
|
.as_ref()
|
|
.expect("Series history should be Some")
|
|
.is_empty() =>
|
|
{
|
|
self
|
|
.app
|
|
.push_navigation_stack(ActiveSonarrBlock::SeriesHistoryDetails.into());
|
|
}
|
|
ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => {
|
|
if self.app.data.sonarr_data.prompt_confirm {
|
|
self.app.data.sonarr_data.prompt_confirm_action = Some(
|
|
SonarrEvent::TriggerAutomaticSeriesSearch(self.extract_series_id()),
|
|
);
|
|
}
|
|
|
|
self.app.pop_navigation_stack();
|
|
}
|
|
ActiveSonarrBlock::UpdateAndScanSeriesPrompt => {
|
|
if self.app.data.sonarr_data.prompt_confirm {
|
|
self.app.data.sonarr_data.prompt_confirm_action =
|
|
Some(SonarrEvent::UpdateAndScanSeries(self.extract_series_id()));
|
|
}
|
|
|
|
self.app.pop_navigation_stack();
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
fn handle_esc(&mut self) {
|
|
match self.active_sonarr_block {
|
|
ActiveSonarrBlock::UpdateAndScanSeriesPrompt
|
|
| ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => {
|
|
self.app.pop_navigation_stack();
|
|
self.app.data.sonarr_data.prompt_confirm = false;
|
|
}
|
|
ActiveSonarrBlock::SeriesHistoryDetails => {
|
|
self.app.pop_navigation_stack();
|
|
}
|
|
ActiveSonarrBlock::SeriesHistory => {
|
|
if self
|
|
.app
|
|
.data
|
|
.sonarr_data
|
|
.series_history
|
|
.as_ref()
|
|
.expect("Series history is not populated")
|
|
.filtered_items
|
|
.is_some()
|
|
{
|
|
self
|
|
.app
|
|
.data
|
|
.sonarr_data
|
|
.series_history
|
|
.as_mut()
|
|
.expect("Series history is not populated")
|
|
.reset_filter();
|
|
} else {
|
|
self.app.pop_navigation_stack();
|
|
self.app.data.sonarr_data.reset_series_info_tabs();
|
|
}
|
|
}
|
|
ActiveSonarrBlock::SeriesDetails => {
|
|
self.app.pop_navigation_stack();
|
|
self.app.data.sonarr_data.reset_series_info_tabs();
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
fn handle_char_key_event(&mut self) {
|
|
let key = self.key;
|
|
match self.active_sonarr_block {
|
|
ActiveSonarrBlock::SeriesDetails => match self.key {
|
|
_ if matches_key!(refresh, key) => self
|
|
.app
|
|
.pop_and_push_navigation_stack(self.active_sonarr_block.into()),
|
|
_ if matches_key!(auto_search, key) => {
|
|
self
|
|
.app
|
|
.push_navigation_stack(ActiveSonarrBlock::AutomaticallySearchSeriesPrompt.into());
|
|
}
|
|
_ if matches_key!(update, key) => {
|
|
self
|
|
.app
|
|
.push_navigation_stack(ActiveSonarrBlock::UpdateAndScanSeriesPrompt.into());
|
|
}
|
|
_ if matches_key!(edit, key) => {
|
|
self.app.push_navigation_stack(
|
|
(
|
|
ActiveSonarrBlock::EditSeriesPrompt,
|
|
Some(self.active_sonarr_block),
|
|
)
|
|
.into(),
|
|
);
|
|
self.app.data.sonarr_data.edit_series_modal = Some((&self.app.data.sonarr_data).into());
|
|
self.app.data.sonarr_data.selected_block =
|
|
BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS);
|
|
}
|
|
_ if matches_key!(view, key) => {
|
|
self.build_series_overview_modal();
|
|
self
|
|
.app
|
|
.push_navigation_stack(ActiveSonarrBlock::SeriesOverview.into());
|
|
}
|
|
_ if matches_key!(toggle_monitoring, key) => {
|
|
self.app.data.sonarr_data.prompt_confirm = true;
|
|
let (series_id, season_number) = self.extract_series_id_season_number_tuple();
|
|
self.app.data.sonarr_data.prompt_confirm_action = Some(
|
|
SonarrEvent::ToggleSeasonMonitoring(series_id, season_number),
|
|
);
|
|
|
|
self
|
|
.app
|
|
.pop_and_push_navigation_stack(self.active_sonarr_block.into());
|
|
}
|
|
_ => (),
|
|
},
|
|
ActiveSonarrBlock::SeriesHistory => match self.key {
|
|
_ if matches_key!(refresh, key) => self
|
|
.app
|
|
.pop_and_push_navigation_stack(self.active_sonarr_block.into()),
|
|
_ if matches_key!(auto_search, key) => {
|
|
self
|
|
.app
|
|
.push_navigation_stack(ActiveSonarrBlock::AutomaticallySearchSeriesPrompt.into());
|
|
}
|
|
_ if matches_key!(edit, key) => {
|
|
self.app.push_navigation_stack(
|
|
(
|
|
ActiveSonarrBlock::EditSeriesPrompt,
|
|
Some(self.active_sonarr_block),
|
|
)
|
|
.into(),
|
|
);
|
|
self.app.data.sonarr_data.edit_series_modal = Some((&self.app.data.sonarr_data).into());
|
|
self.app.data.sonarr_data.selected_block =
|
|
BlockSelectionState::new(EDIT_SERIES_SELECTION_BLOCKS);
|
|
}
|
|
_ if matches_key!(update, key) => {
|
|
self
|
|
.app
|
|
.push_navigation_stack(ActiveSonarrBlock::UpdateAndScanSeriesPrompt.into());
|
|
}
|
|
_ => (),
|
|
},
|
|
ActiveSonarrBlock::AutomaticallySearchSeriesPrompt => {
|
|
if matches_key!(confirm, key) {
|
|
self.app.data.sonarr_data.prompt_confirm = true;
|
|
self.app.data.sonarr_data.prompt_confirm_action = Some(
|
|
SonarrEvent::TriggerAutomaticSeriesSearch(self.extract_series_id()),
|
|
);
|
|
|
|
self.app.pop_navigation_stack();
|
|
}
|
|
}
|
|
ActiveSonarrBlock::UpdateAndScanSeriesPrompt => {
|
|
self.app.data.sonarr_data.prompt_confirm = true;
|
|
self.app.data.sonarr_data.prompt_confirm_action =
|
|
Some(SonarrEvent::UpdateAndScanSeries(self.extract_series_id()));
|
|
|
|
self.app.pop_navigation_stack();
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
fn app_mut(&mut self) -> &mut App<'b> {
|
|
self.app
|
|
}
|
|
|
|
fn current_route(&self) -> Route {
|
|
self.app.get_current_route()
|
|
}
|
|
}
|