Mostly completed tags implementation; still need to add the UI option for the Add Movie popup, and I still need to fix the REALLY FAST horizontal scrolling issue (I'm thinking just %2 everything to slow it down). Oh, and also need to convert the quality profile Hashmap into a BiMap

This commit is contained in:
2023-08-08 10:50:05 -06:00
parent f92042fb21
commit 207b8a8c80
21 changed files with 948 additions and 344 deletions
+7 -3
View File
@@ -1,7 +1,7 @@
use std::time::Duration;
use anyhow::anyhow;
use log::error;
use log::{debug, error};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::Sender;
@@ -21,6 +21,7 @@ pub struct App {
network_tx: Option<Sender<NetworkEvent>>,
pub server_tabs: TabState,
pub error: HorizontallyScrollableText,
pub response: String,
pub client: Client,
pub title: &'static str,
pub tick_until_poll: u64,
@@ -45,6 +46,8 @@ impl App {
}
pub async fn dispatch_network_event(&mut self, action: NetworkEvent) {
debug!("Dispatching network event: {:?}", action);
if let Some(network_tx) = &self.network_tx {
if let Err(e) = network_tx.send(action).await {
self.is_loading = false;
@@ -115,6 +118,7 @@ impl Default for App {
navigation_stack: vec![DEFAULT_ROUTE],
network_tx: None,
error: HorizontallyScrollableText::default(),
response: String::default(),
server_tabs: TabState::new(vec![
TabRoute {
title: "Radarr".to_owned(),
@@ -260,11 +264,11 @@ mod tests {
app.handle_error(anyhow!(test_string));
assert_eq!(app.error.stationary_style(), test_string);
assert_eq!(app.error.text, test_string);
app.handle_error(anyhow!("Testing a different error"));
assert_eq!(app.error.stationary_style(), test_string);
assert_eq!(app.error.text, test_string);
}
#[tokio::test]
+91 -38
View File
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::time::Duration;
use bimap::BiMap;
use chrono::{DateTime, Utc};
use strum::IntoEnumIterator;
@@ -9,7 +10,9 @@ use crate::models::radarr_models::{
AddMovieSearchResult, Collection, CollectionMovie, Credit, DiskSpace, DownloadRecord,
MinimumAvailability, Monitor, Movie, MovieHistoryItem, Release, ReleaseField, RootFolder,
};
use crate::models::{ScrollableText, StatefulList, StatefulTable, TabRoute, TabState};
use crate::models::{
HorizontallyScrollableText, ScrollableText, StatefulList, StatefulTable, TabRoute, TabState,
};
use crate::network::radarr_network::RadarrEvent;
pub struct RadarrData {
@@ -26,6 +29,7 @@ pub struct RadarrData {
pub selected_block: ActiveRadarrBlock,
pub downloads: StatefulTable<DownloadRecord>,
pub quality_profile_map: HashMap<u64, String>,
pub tags_map: BiMap<u64, String>,
pub movie_details: ScrollableText,
pub file_details: String,
pub audio_details: String,
@@ -41,10 +45,10 @@ pub struct RadarrData {
pub prompt_confirm_action: Option<RadarrEvent>,
pub main_tabs: TabState,
pub movie_info_tabs: TabState,
pub search: String,
pub filter: String,
pub edit_path: String,
pub edit_tags: String,
pub search: HorizontallyScrollableText,
pub filter: HorizontallyScrollableText,
pub edit_path: HorizontallyScrollableText,
pub edit_tags: HorizontallyScrollableText,
pub edit_monitored: Option<bool>,
pub sort_ascending: Option<bool>,
pub prompt_confirm: bool,
@@ -59,8 +63,8 @@ impl RadarrData {
pub fn reset_search(&mut self) {
self.is_searching = false;
self.search = String::default();
self.filter = String::default();
self.search = HorizontallyScrollableText::default();
self.filter = HorizontallyScrollableText::default();
self.filtered_movies = StatefulTable::default();
self.filtered_collections = StatefulTable::default();
self.add_searched_movies = StatefulTable::default();
@@ -68,15 +72,15 @@ impl RadarrData {
pub fn reset_filter(&mut self) {
self.is_filtering = false;
self.filter = String::default();
self.filter = HorizontallyScrollableText::default();
self.filtered_movies = StatefulTable::default();
self.filtered_collections = StatefulTable::default();
}
pub fn reset_edit_movie(&mut self) {
self.edit_monitored = None;
self.edit_path = String::default();
self.edit_tags = String::default();
self.edit_path = HorizontallyScrollableText::default();
self.edit_tags = HorizontallyScrollableText::default();
self.reset_movie_preferences_selections();
}
@@ -119,24 +123,37 @@ impl RadarrData {
self.populate_movie_preferences_lists();
let Movie {
path,
tags,
monitored,
minimum_availability,
quality_profile_id,
..
} = if self.filtered_movies.items.is_empty() {
self.movies.current_selection_clone()
self.movies.current_selection()
} else {
self.filtered_movies.current_selection_clone()
self.filtered_movies.current_selection()
};
self.edit_path = path;
self.edit_monitored = Some(monitored);
self.edit_path = path.clone().into();
self.edit_tags = tags
.iter()
.map(|tag_id| {
self
.tags_map
.get_by_left(&tag_id.as_u64().unwrap())
.unwrap()
.clone()
})
.collect::<Vec<String>>()
.join(", ")
.into();
self.edit_monitored = Some(*monitored);
let minimum_availability_index = self
.movie_minimum_availability_list
.items
.iter()
.position(|&ma| ma == minimum_availability);
.position(|ma| ma == minimum_availability);
self
.movie_minimum_availability_list
.state
@@ -174,6 +191,7 @@ impl Default for RadarrData {
filtered_movies: StatefulTable::default(),
downloads: StatefulTable::default(),
quality_profile_map: HashMap::default(),
tags_map: BiMap::default(),
file_details: String::default(),
audio_details: String::default(),
video_details: String::default(),
@@ -187,10 +205,10 @@ impl Default for RadarrData {
filtered_collections: StatefulTable::default(),
collection_movies: StatefulTable::default(),
prompt_confirm_action: None,
search: String::default(),
filter: String::default(),
edit_path: String::default(),
edit_tags: String::default(),
search: HorizontallyScrollableText::default(),
filter: HorizontallyScrollableText::default(),
edit_path: HorizontallyScrollableText::default(),
edit_tags: HorizontallyScrollableText::default(),
edit_monitored: None,
sort_ascending: None,
is_searching: false,
@@ -519,6 +537,9 @@ impl App {
self
.dispatch_network_event(RadarrEvent::GetQualityProfiles.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetTags.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetRootFolders.into())
.await;
@@ -538,6 +559,12 @@ impl App {
.unwrap_or_else(|| Duration::from_secs(0))
.is_zero()
{
self
.dispatch_network_event(RadarrEvent::GetQualityProfiles.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetTags.into())
.await;
self.dispatch_by_radarr_block(&active_radarr_block).await;
}
}
@@ -548,7 +575,8 @@ impl App {
.data
.radarr_data
.filtered_collections
.current_selection_clone()
.current_selection()
.clone()
.movies
.unwrap_or_default()
} else {
@@ -556,7 +584,8 @@ impl App {
.data
.radarr_data
.collections
.current_selection_clone()
.current_selection()
.clone()
.movies
.unwrap_or_default()
};
@@ -582,10 +611,10 @@ pub mod radarr_test_utils {
let mut radarr_data = RadarrData {
is_searching: true,
is_filtering: true,
search: "test search".to_owned(),
filter: "test filter".to_owned(),
edit_path: "test path".to_owned(),
edit_tags: "test tag".to_owned(),
search: "test search".to_owned().into(),
filter: "test filter".to_owned().into(),
edit_path: "test path".to_owned().into(),
edit_tags: "usenet, test".to_owned().into(),
edit_monitored: Some(true),
file_details: "test file details".to_owned(),
audio_details: "test audio details".to_owned(),
@@ -642,8 +671,8 @@ pub mod radarr_test_utils {
macro_rules! assert_search_reset {
($radarr_data:expr) => {
assert!(!$radarr_data.is_searching);
assert!($radarr_data.search.is_empty());
assert!($radarr_data.filter.is_empty());
assert!($radarr_data.search.text.is_empty());
assert!($radarr_data.filter.text.is_empty());
assert!($radarr_data.filtered_movies.items.is_empty());
assert!($radarr_data.filtered_collections.items.is_empty());
assert!($radarr_data.add_searched_movies.items.is_empty());
@@ -654,8 +683,8 @@ pub mod radarr_test_utils {
macro_rules! assert_edit_movie_reset {
($radarr_data:expr) => {
assert!($radarr_data.edit_monitored.is_none());
assert!($radarr_data.edit_path.is_empty());
assert!($radarr_data.edit_tags.is_empty());
assert!($radarr_data.edit_path.text.is_empty());
assert!($radarr_data.edit_tags.text.is_empty());
};
}
@@ -663,7 +692,7 @@ pub mod radarr_test_utils {
macro_rules! assert_filter_reset {
($radarr_data:expr) => {
assert!(!$radarr_data.is_filtering);
assert!($radarr_data.filter.is_empty());
assert!($radarr_data.filter.text.is_empty());
assert!($radarr_data.filtered_movies.items.is_empty());
assert!($radarr_data.filtered_collections.items.is_empty());
};
@@ -704,7 +733,8 @@ mod tests {
mod radarr_data_tests {
use std::collections::HashMap;
use pretty_assertions::assert_eq;
use bimap::BiMap;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use serde_json::Number;
use strum::IntoEnumIterator;
@@ -712,7 +742,7 @@ mod tests {
use crate::app::radarr::radarr_test_utils::create_test_radarr_data;
use crate::app::radarr::{ActiveRadarrBlock, RadarrData};
use crate::models::radarr_models::{MinimumAvailability, Monitor, Movie};
use crate::models::{Route, StatefulTable};
use crate::models::{HorizontallyScrollableText, Route, StatefulTable};
#[test]
fn test_from_tuple_to_route_with_context() {
@@ -768,8 +798,8 @@ mod tests {
fn test_reset_edit_movie() {
let mut radarr_data = RadarrData {
edit_monitored: Some(true),
edit_path: "test path".to_owned(),
edit_tags: "test tag".to_owned(),
edit_path: "test path".to_owned().into(),
edit_tags: "test tag".to_owned().into(),
..RadarrData::default()
};
@@ -816,13 +846,14 @@ mod tests {
#[rstest]
fn test_populate_edit_movie_fields(#[values(true, false)] test_filtered_movies: bool) {
let mut radarr_data = RadarrData {
edit_path: String::default(),
edit_tags: String::default(),
edit_path: HorizontallyScrollableText::default(),
edit_tags: HorizontallyScrollableText::default(),
edit_monitored: None,
quality_profile_map: HashMap::from([
(2222, "HD - 1080p".to_owned()),
(1111, "Any".to_owned()),
]),
tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]),
filtered_movies: StatefulTable::default(),
..create_test_radarr_data()
};
@@ -831,6 +862,7 @@ mod tests {
monitored: true,
quality_profile_id: Number::from(2222),
minimum_availability: MinimumAvailability::Released,
tags: vec![Number::from(1), Number::from(2)],
..Movie::default()
};
@@ -856,11 +888,12 @@ mod tests {
radarr_data.movie_quality_profile_list.items,
vec!["Any".to_owned(), "HD - 1080p".to_owned()]
);
assert_eq!(
assert_str_eq!(
radarr_data.movie_quality_profile_list.current_selection(),
"HD - 1080p"
);
assert_eq!(radarr_data.edit_path, "/nfs/movies/Test".to_owned());
assert_str_eq!(radarr_data.edit_path.text, "/nfs/movies/Test");
assert_str_eq!(radarr_data.edit_tags.text, "usenet, test");
assert_eq!(radarr_data.edit_monitored, Some(true));
}
}
@@ -1325,6 +1358,10 @@ mod tests {
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetQualityProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetTags.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetRootFolders.into()
@@ -1365,6 +1402,14 @@ mod tests {
.radarr_on_tick(ActiveRadarrBlock::Downloads, false)
.await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetQualityProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetTags.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
@@ -1382,6 +1427,14 @@ mod tests {
.radarr_on_tick(ActiveRadarrBlock::Downloads, false)
.await;
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetQualityProfiles.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetTags.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetDownloads.into()
+1 -1
View File
@@ -20,7 +20,7 @@ pub struct Events {
impl Events {
pub fn new() -> Self {
let (tx, rx) = mpsc::channel();
let tick_rate: Duration = Duration::from_millis(250);
let tick_rate: Duration = Duration::from_millis(50);
thread::spawn(move || {
let mut last_tick = Instant::now();
+89
View File
@@ -65,6 +65,21 @@ fn handle_prompt_toggle(app: &mut App, key: &Key) {
}
}
#[macro_export]
macro_rules! handle_text_box_left_right_keys {
($self:expr, $key:expr, $input:expr) => {
match $self.key {
_ if *$key == DEFAULT_KEYBINDINGS.left.key => {
$input.scroll_left();
}
_ if *$key == DEFAULT_KEYBINDINGS.right.key => {
$input.scroll_right();
}
_ => (),
}
};
}
#[macro_export]
macro_rules! handle_text_box_keys {
($self:expr, $key:expr, $input:expr) => {
@@ -221,6 +236,27 @@ mod test_utils {
);
}
};
($func:ident, $handler:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident) => {
#[rstest]
fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) {
let mut app = App::default();
app.data.radarr_data.$data_ref.set_items($items);
$handler::with(&key, &mut app, &$block, &$context).handle();
assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field,
"Test 2"
);
$handler::with(&key, &mut app, &$block, &$context).handle();
assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field,
"Test 1"
);
}
};
($func:ident, $handler:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident, $conversion_fn:ident) => {
#[rstest]
fn $func(#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key) {
@@ -338,6 +374,27 @@ mod test_utils {
);
}
};
($func:ident, $handler:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident) => {
#[test]
fn $func() {
let mut app = App::default();
app.data.radarr_data.$data_ref.set_items($items);
$handler::with(&DEFAULT_KEYBINDINGS.end.key, &mut app, &$block, &$context).handle();
assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field,
"Test 3"
);
$handler::with(&DEFAULT_KEYBINDINGS.home.key, &mut app, &$block, &$context).handle();
assert_str_eq!(
app.data.radarr_data.$data_ref.current_selection().$field,
"Test 1"
);
}
};
($func:ident, $handler:ident, $data_ref:ident, $items:expr, $block:expr, $context:expr, $field:ident, $conversion_fn:ident) => {
#[test]
fn $func() {
@@ -403,6 +460,38 @@ mod test_utils {
};
}
#[macro_export]
macro_rules! test_text_box_home_end_keys {
($handler:ident, $block:expr, $field:ident) => {
let mut app = App::default();
app.data.radarr_data.$field = "Test".to_owned().into();
$handler::with(&DEFAULT_KEYBINDINGS.home.key, &mut app, &$block, &None).handle();
assert_eq!(*app.data.radarr_data.$field.offset.borrow(), 4);
$handler::with(&DEFAULT_KEYBINDINGS.end.key, &mut app, &$block, &None).handle();
assert_eq!(*app.data.radarr_data.$field.offset.borrow(), 0);
};
}
#[macro_export]
macro_rules! test_text_box_left_right_keys {
($handler:ident, $block:expr, $field:ident) => {
let mut app = App::default();
app.data.radarr_data.$field = "Test".to_owned().into();
$handler::with(&DEFAULT_KEYBINDINGS.left.key, &mut app, &$block, &None).handle();
assert_eq!(*app.data.radarr_data.$field.offset.borrow(), 1);
$handler::with(&DEFAULT_KEYBINDINGS.right.key, &mut app, &$block, &None).handle();
assert_eq!(*app.data.radarr_data.$field.offset.borrow(), 0);
};
}
#[macro_export]
macro_rules! test_handler_delegation {
($base:expr, $active_block:expr) => {
@@ -3,7 +3,7 @@ use crate::app::radarr::ActiveRadarrBlock;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::models::{Scrollable, StatefulTable};
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_text_box_keys, App, Key};
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, App, Key};
pub(super) struct AddMovieHandler<'a> {
key: &'a Key,
@@ -119,6 +119,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> {
.radarr_data
.movie_quality_profile_list
.scroll_to_top(),
ActiveRadarrBlock::AddMovieSearchInput => self.app.data.radarr_data.search.scroll_home(),
_ => (),
}
}
@@ -149,6 +150,7 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> {
.radarr_data
.movie_quality_profile_list
.scroll_to_bottom(),
ActiveRadarrBlock::AddMovieSearchInput => self.app.data.radarr_data.search.reset_offset(),
_ => (),
}
}
@@ -156,8 +158,12 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for AddMovieHandler<'a> {
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {
if let ActiveRadarrBlock::AddMoviePrompt = self.active_radarr_block {
handle_prompt_toggle(self.app, self.key)
match self.active_radarr_block {
ActiveRadarrBlock::AddMoviePrompt => handle_prompt_toggle(self.app, self.key),
ActiveRadarrBlock::AddMovieSearchInput => {
handle_text_box_left_right_keys!(self, self.key, self.app.data.radarr_data.search)
}
_ => (),
}
}
@@ -298,7 +304,7 @@ mod tests {
ActiveRadarrBlock::AddMovieSearchResults,
None,
title,
stationary_style
to_string
);
test_enum_scroll!(
@@ -349,10 +355,12 @@ mod tests {
}
mod test_handle_home_end {
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::{
extended_stateful_iterable_vec, test_enum_home_and_end, test_iterable_home_and_end,
test_text_box_home_end_keys,
};
use super::*;
@@ -365,7 +373,7 @@ mod tests {
ActiveRadarrBlock::AddMovieSearchResults,
None,
title,
stationary_style
to_string
);
test_enum_home_and_end!(
@@ -393,11 +401,22 @@ mod tests {
ActiveRadarrBlock::AddMovieSelectQualityProfile,
None
);
#[test]
fn test_add_movie_search_input_home_end_keys() {
test_text_box_home_end_keys!(
AddMovieHandler,
ActiveRadarrBlock::AddMovieSearchInput,
search
);
}
}
mod test_handle_left_right_action {
use rstest::rstest;
use crate::test_text_box_left_right_keys;
use super::*;
#[rstest]
@@ -412,6 +431,15 @@ mod tests {
assert!(!app.data.radarr_data.prompt_confirm);
}
#[test]
fn test_add_movie_search_input_left_right_keys() {
test_text_box_left_right_keys!(
AddMovieHandler,
ActiveRadarrBlock::AddMovieSearchInput,
search
);
}
}
mod test_handle_submit {
@@ -805,7 +833,7 @@ mod tests {
#[test]
fn test_add_movie_search_input_backspace() {
let mut app = App::default();
app.data.radarr_data.search = "Test".to_owned();
app.data.radarr_data.search = "Test".to_owned().into();
AddMovieHandler::with(
&DEFAULT_KEYBINDINGS.backspace.key,
@@ -815,7 +843,7 @@ mod tests {
)
.handle();
assert_str_eq!(app.data.radarr_data.search, "Tes");
assert_str_eq!(app.data.radarr_data.search.text, "Tes");
}
#[test]
@@ -830,7 +858,7 @@ mod tests {
)
.handle();
assert_str_eq!(app.data.radarr_data.search, "h");
assert_str_eq!(app.data.radarr_data.search.text, "h");
}
}
}
@@ -141,7 +141,7 @@ mod tests {
ActiveRadarrBlock::CollectionDetails,
None,
title,
stationary_style
to_string
);
}
@@ -158,7 +158,7 @@ mod tests {
ActiveRadarrBlock::CollectionDetails,
None,
title,
stationary_style
to_string
);
}
@@ -2,10 +2,10 @@ use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::radarr::ActiveRadarrBlock;
use crate::app::App;
use crate::event::Key;
use crate::handle_text_box_keys;
use crate::handlers::{handle_prompt_toggle, KeyEventHandler};
use crate::models::Scrollable;
use crate::network::radarr_network::RadarrEvent;
use crate::{handle_text_box_keys, handle_text_box_left_right_keys};
pub(super) struct EditMovieHandler<'a> {
key: &'a Key,
@@ -100,6 +100,8 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for EditMovieHandler<'a> {
.radarr_data
.movie_quality_profile_list
.scroll_to_top(),
ActiveRadarrBlock::EditMoviePathInput => self.app.data.radarr_data.edit_path.scroll_home(),
ActiveRadarrBlock::EditMovieTagsInput => self.app.data.radarr_data.edit_tags.scroll_home(),
_ => (),
}
}
@@ -118,6 +120,8 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for EditMovieHandler<'a> {
.radarr_data
.movie_quality_profile_list
.scroll_to_bottom(),
ActiveRadarrBlock::EditMoviePathInput => self.app.data.radarr_data.edit_path.reset_offset(),
ActiveRadarrBlock::EditMovieTagsInput => self.app.data.radarr_data.edit_tags.reset_offset(),
_ => (),
}
}
@@ -125,8 +129,15 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for EditMovieHandler<'a> {
fn handle_delete(&mut self) {}
fn handle_left_right_action(&mut self) {
if let ActiveRadarrBlock::EditMoviePrompt = self.active_radarr_block {
handle_prompt_toggle(self.app, self.key)
match self.active_radarr_block {
ActiveRadarrBlock::EditMoviePrompt => handle_prompt_toggle(self.app, self.key),
ActiveRadarrBlock::EditMoviePathInput => {
handle_text_box_left_right_keys!(self, self.key, self.app.data.radarr_data.edit_path)
}
ActiveRadarrBlock::EditMovieTagsInput => {
handle_text_box_left_right_keys!(self, self.key, self.app.data.radarr_data.edit_tags)
}
_ => (),
}
}
@@ -262,7 +273,7 @@ mod tests {
mod test_handle_home_end {
use strum::IntoEnumIterator;
use crate::{test_enum_home_and_end, test_iterable_home_and_end};
use crate::{test_enum_home_and_end, test_iterable_home_and_end, test_text_box_home_end_keys};
use super::*;
@@ -282,11 +293,31 @@ mod tests {
ActiveRadarrBlock::EditMovieSelectQualityProfile,
None
);
#[test]
fn test_edit_movie_path_input_home_end_keys() {
test_text_box_home_end_keys!(
EditMovieHandler,
ActiveRadarrBlock::EditMoviePathInput,
edit_path
);
}
#[test]
fn test_edit_movie_tags_input_home_end_keys() {
test_text_box_home_end_keys!(
EditMovieHandler,
ActiveRadarrBlock::EditMovieTagsInput,
edit_tags
);
}
}
mod test_handle_left_right_action {
use rstest::rstest;
use crate::{test_text_box_home_end_keys, test_text_box_left_right_keys};
use super::*;
#[rstest]
@@ -301,6 +332,24 @@ mod tests {
assert!(!app.data.radarr_data.prompt_confirm);
}
#[test]
fn test_edit_movie_path_input_left_right_keys() {
test_text_box_left_right_keys!(
EditMovieHandler,
ActiveRadarrBlock::EditMoviePathInput,
edit_path
);
}
#[test]
fn test_edit_movie_tags_input_left_right_keys() {
test_text_box_left_right_keys!(
EditMovieHandler,
ActiveRadarrBlock::EditMovieTagsInput,
edit_tags
);
}
}
mod test_handle_submit {
@@ -321,7 +370,7 @@ mod tests {
fn test_edit_movie_path_input_submit() {
let mut app = App::default();
app.should_ignore_quit_key = true;
app.data.radarr_data.edit_path = "Test Path".to_owned();
app.data.radarr_data.edit_path = "Test Path".to_owned().into();
app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::EditMoviePathInput.into());
@@ -334,7 +383,7 @@ mod tests {
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.data.radarr_data.edit_path.is_empty());
assert!(!app.data.radarr_data.edit_path.text.is_empty());
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::EditMoviePrompt.into()
@@ -345,7 +394,7 @@ mod tests {
fn test_edit_movie_tags_input_submit() {
let mut app = App::default();
app.should_ignore_quit_key = true;
app.data.radarr_data.edit_tags = "Test Tags".to_owned();
app.data.radarr_data.edit_tags = "Test Tags".to_owned().into();
app.push_navigation_stack(ActiveRadarrBlock::EditMoviePrompt.into());
app.push_navigation_stack(ActiveRadarrBlock::EditMoviePathInput.into());
@@ -358,7 +407,7 @@ mod tests {
.handle();
assert!(!app.should_ignore_quit_key);
assert!(!app.data.radarr_data.edit_tags.is_empty());
assert!(!app.data.radarr_data.edit_tags.text.is_empty());
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::EditMoviePrompt.into()
@@ -599,7 +648,7 @@ mod tests {
#[test]
fn test_edit_movie_path_input_backspace() {
let mut app = App::default();
app.data.radarr_data.edit_path = "Test".to_owned();
app.data.radarr_data.edit_path = "Test".to_owned().into();
EditMovieHandler::with(
&DEFAULT_KEYBINDINGS.backspace.key,
@@ -609,13 +658,13 @@ mod tests {
)
.handle();
assert_str_eq!(app.data.radarr_data.edit_path, "Tes");
assert_str_eq!(app.data.radarr_data.edit_path.text, "Tes");
}
#[test]
fn test_edit_movie_tags_input_backspace() {
let mut app = App::default();
app.data.radarr_data.edit_tags = "Test".to_owned();
app.data.radarr_data.edit_tags = "Test".to_owned().into();
EditMovieHandler::with(
&DEFAULT_KEYBINDINGS.backspace.key,
@@ -625,7 +674,7 @@ mod tests {
)
.handle();
assert_str_eq!(app.data.radarr_data.edit_tags, "Tes");
assert_str_eq!(app.data.radarr_data.edit_tags.text, "Tes");
}
#[test]
@@ -640,7 +689,7 @@ mod tests {
)
.handle();
assert_str_eq!(app.data.radarr_data.edit_path, "h");
assert_str_eq!(app.data.radarr_data.edit_path.text, "h");
}
#[test]
@@ -655,7 +704,7 @@ mod tests {
)
.handle();
assert_str_eq!(app.data.radarr_data.edit_tags, "h");
assert_str_eq!(app.data.radarr_data.edit_tags.text, "h");
}
}
}
+91 -48
View File
@@ -11,7 +11,7 @@ use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler
use crate::models::Scrollable;
use crate::network::radarr_network::RadarrEvent;
use crate::utils::strip_non_alphanumeric_characters;
use crate::{handle_text_box_keys, App, Key};
use crate::{handle_text_box_keys, handle_text_box_left_right_keys, App, Key};
mod add_movie_handler;
mod collection_details_handler;
@@ -149,6 +149,12 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> {
}
}
ActiveRadarrBlock::Downloads => self.app.data.radarr_data.downloads.scroll_to_top(),
ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchCollection => {
self.app.data.radarr_data.search.scroll_home()
}
ActiveRadarrBlock::FilterMovies | ActiveRadarrBlock::FilterCollections => {
self.app.data.radarr_data.filter.scroll_home()
}
_ => (),
}
}
@@ -182,6 +188,12 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> {
}
}
ActiveRadarrBlock::Downloads => self.app.data.radarr_data.downloads.scroll_to_bottom(),
ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchCollection => {
self.app.data.radarr_data.search.reset_offset()
}
ActiveRadarrBlock::FilterMovies | ActiveRadarrBlock::FilterCollections => {
self.app.data.radarr_data.filter.reset_offset()
}
_ => (),
}
}
@@ -222,6 +234,12 @@ impl<'a> KeyEventHandler<'a, ActiveRadarrBlock> for RadarrHandler<'a> {
| ActiveRadarrBlock::RefreshAllMoviesPrompt
| ActiveRadarrBlock::RefreshAllCollectionsPrompt
| ActiveRadarrBlock::RefreshDownloadsPrompt => handle_prompt_toggle(self.app, self.key),
ActiveRadarrBlock::SearchMovie | ActiveRadarrBlock::SearchCollection => {
handle_text_box_left_right_keys!(self, self.key, self.app.data.radarr_data.search)
}
ActiveRadarrBlock::FilterMovies | ActiveRadarrBlock::FilterCollections => {
handle_text_box_left_right_keys!(self, self.key, self.app.data.radarr_data.filter)
}
_ => (),
}
}
@@ -476,14 +494,7 @@ impl RadarrHandler<'_> {
where
F: Fn(&T) -> &str,
{
let search_string = self
.app
.data
.radarr_data
.search
.drain(..)
.collect::<String>()
.to_lowercase();
let search_string = self.app.data.radarr_data.search.drain().to_lowercase();
let search_index = rows.iter().position(|item| {
strip_non_alphanumeric_characters(field_selection_fn(item)).contains(&search_string)
});
@@ -503,15 +514,7 @@ impl RadarrHandler<'_> {
F: Fn(&T) -> &str,
T: Clone,
{
let filter = strip_non_alphanumeric_characters(
&self
.app
.data
.radarr_data
.filter
.drain(..)
.collect::<String>(),
);
let filter = strip_non_alphanumeric_characters(&self.app.data.radarr_data.filter.drain());
let filter_matches: Vec<T> = rows
.iter()
.filter(|&item| strip_non_alphanumeric_characters(field_selection_fn(item)).contains(&filter))
@@ -537,21 +540,23 @@ mod radarr_handler_test_utils {
($handler:ident, $block:expr, $context:expr) => {
let mut app = App::default();
let mut radarr_data = RadarrData {
edit_path: String::default(),
edit_tags: String::default(),
edit_path: HorizontallyScrollableText::default(),
edit_tags: HorizontallyScrollableText::default(),
edit_monitored: None,
quality_profile_map: HashMap::from([
(2222, "HD - 1080p".to_owned()),
(1111, "Any".to_owned()),
]),
tags_map: BiMap::from_iter([(1, "test".to_owned())]),
filtered_movies: StatefulTable::default(),
..create_test_radarr_data()
};
radarr_data.movies.set_items(vec![Movie {
path: "/nfs/movies/Test".to_owned(),
path: "/nfs/movies/Test".to_owned().into(),
monitored: true,
quality_profile_id: Number::from(2222),
minimum_availability: MinimumAvailability::Released,
tags: vec![Number::from(1)],
..Movie::default()
}]);
app.data.radarr_data = radarr_data;
@@ -582,7 +587,7 @@ mod radarr_handler_test_utils {
app.data.radarr_data.movie_quality_profile_list.items,
vec!["Any".to_owned(), "HD - 1080p".to_owned()]
);
assert_eq!(
assert_str_eq!(
app
.data
.radarr_data
@@ -590,10 +595,8 @@ mod radarr_handler_test_utils {
.current_selection(),
"HD - 1080p"
);
assert_eq!(
app.data.radarr_data.edit_path,
"/nfs/movies/Test".to_owned()
);
assert_str_eq!(app.data.radarr_data.edit_path.text, "/nfs/movies/Test");
assert_str_eq!(app.data.radarr_data.edit_tags.text, "test");
assert_eq!(app.data.radarr_data.edit_monitored, Some(true));
};
}
@@ -673,8 +676,12 @@ mod tests {
}
mod test_handle_home_end {
use pretty_assertions::assert_eq;
use crate::models::radarr_models::DownloadRecord;
use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end};
use crate::{
extended_stateful_iterable_vec, test_iterable_home_and_end, test_text_box_home_end_keys,
};
use super::*;
@@ -727,6 +734,22 @@ mod tests {
None,
title
);
#[rstest]
fn test_search_boxes_home_end_keys(
#[values(ActiveRadarrBlock::SearchMovie, ActiveRadarrBlock::SearchCollection)]
active_radarr_block: ActiveRadarrBlock,
) {
test_text_box_home_end_keys!(RadarrHandler, active_radarr_block, search);
}
#[rstest]
fn test_filter_boxes_home_end_keys(
#[values(ActiveRadarrBlock::FilterMovies, ActiveRadarrBlock::FilterCollections)]
active_radarr_block: ActiveRadarrBlock,
) {
test_text_box_home_end_keys!(RadarrHandler, active_radarr_block, filter);
}
}
mod test_handle_delete {
@@ -765,6 +788,8 @@ mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::test_text_box_left_right_keys;
use super::*;
#[rstest]
@@ -843,6 +868,22 @@ mod tests {
assert!(!app.data.radarr_data.prompt_confirm);
}
#[rstest]
fn test_search_boxes_left_right_keys(
#[values(ActiveRadarrBlock::SearchMovie, ActiveRadarrBlock::SearchCollection)]
active_radarr_block: ActiveRadarrBlock,
) {
test_text_box_left_right_keys!(RadarrHandler, active_radarr_block, search);
}
#[rstest]
fn test_filter_boxes_left_right_keys(
#[values(ActiveRadarrBlock::FilterMovies, ActiveRadarrBlock::FilterCollections)]
active_radarr_block: ActiveRadarrBlock,
) {
test_text_box_left_right_keys!(RadarrHandler, active_radarr_block, filter);
}
}
mod test_handle_submit {
@@ -877,7 +918,7 @@ mod tests {
.radarr_data
.movies
.set_items(extended_stateful_iterable_vec!(Movie));
app.data.radarr_data.search = "Test 2".to_owned();
app.data.radarr_data.search = "Test 2".to_owned().into();
RadarrHandler::with(
&SUBMIT_KEY,
@@ -901,7 +942,7 @@ mod tests {
.radarr_data
.filtered_movies
.set_items(extended_stateful_iterable_vec!(Movie));
app.data.radarr_data.search = "Test 2".to_owned();
app.data.radarr_data.search = "Test 2".to_owned().into();
RadarrHandler::with(
&SUBMIT_KEY,
@@ -930,7 +971,7 @@ mod tests {
.radarr_data
.collections
.set_items(extended_stateful_iterable_vec!(Collection));
app.data.radarr_data.search = "Test 2".to_owned();
app.data.radarr_data.search = "Test 2".to_owned().into();
RadarrHandler::with(
&SUBMIT_KEY,
@@ -954,7 +995,7 @@ mod tests {
.radarr_data
.filtered_collections
.set_items(extended_stateful_iterable_vec!(Collection));
app.data.radarr_data.search = "Test 2".to_owned();
app.data.radarr_data.search = "Test 2".to_owned().into();
RadarrHandler::with(
&SUBMIT_KEY,
@@ -983,7 +1024,7 @@ mod tests {
.radarr_data
.movies
.set_items(extended_stateful_iterable_vec!(Movie));
app.data.radarr_data.filter = "Test".to_owned();
app.data.radarr_data.filter = "Test".to_owned().into();
RadarrHandler::with(
&SUBMIT_KEY,
@@ -1013,7 +1054,7 @@ mod tests {
.radarr_data
.collections
.set_items(extended_stateful_iterable_vec!(Collection));
app.data.radarr_data.filter = "Test".to_owned();
app.data.radarr_data.filter = "Test".to_owned().into();
RadarrHandler::with(
&SUBMIT_KEY,
@@ -1210,7 +1251,8 @@ mod tests {
mod test_handle_key_char {
use std::collections::HashMap;
use pretty_assertions::assert_eq;
use bimap::BiMap;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use serde_json::Number;
use strum::IntoEnumIterator;
@@ -1219,6 +1261,7 @@ mod tests {
use crate::app::radarr::radarr_test_utils::create_test_radarr_data;
use crate::app::radarr::RadarrData;
use crate::models::radarr_models::MinimumAvailability;
use crate::models::HorizontallyScrollableText;
use crate::models::StatefulTable;
use super::*;
@@ -1328,7 +1371,7 @@ mod tests {
active_radarr_block: ActiveRadarrBlock,
) {
let mut app = App::default();
app.data.radarr_data.search = "Test".to_owned();
app.data.radarr_data.search = "Test".to_owned().into();
RadarrHandler::with(
&DEFAULT_KEYBINDINGS.backspace.key,
@@ -1338,7 +1381,7 @@ mod tests {
)
.handle();
assert_str_eq!(app.data.radarr_data.search, "Tes");
assert_str_eq!(app.data.radarr_data.search.text, "Tes");
}
#[rstest]
@@ -1347,7 +1390,7 @@ mod tests {
active_radarr_block: ActiveRadarrBlock,
) {
let mut app = App::default();
app.data.radarr_data.filter = "Test".to_owned();
app.data.radarr_data.filter = "Test".to_owned().into();
RadarrHandler::with(
&DEFAULT_KEYBINDINGS.backspace.key,
@@ -1357,7 +1400,7 @@ mod tests {
)
.handle();
assert_str_eq!(app.data.radarr_data.filter, "Tes");
assert_str_eq!(app.data.radarr_data.filter.text, "Tes");
}
#[rstest]
@@ -1369,7 +1412,7 @@ mod tests {
RadarrHandler::with(&Key::Char('h'), &mut app, &active_radarr_block, &None).handle();
assert_str_eq!(app.data.radarr_data.search, "h");
assert_str_eq!(app.data.radarr_data.search.text, "h");
}
#[rstest]
@@ -1381,7 +1424,7 @@ mod tests {
RadarrHandler::with(&Key::Char('h'), &mut app, &active_radarr_block, &None).handle();
assert_str_eq!(app.data.radarr_data.filter, "h");
assert_str_eq!(app.data.radarr_data.filter.text, "h");
}
}
@@ -1393,7 +1436,7 @@ mod tests {
.radarr_data
.movies
.set_items(extended_stateful_iterable_vec!(Movie));
app.data.radarr_data.search = "Test 2".to_owned();
app.data.radarr_data.search = "Test 2".to_owned().into();
app.data.radarr_data.is_searching = true;
app.should_ignore_quit_key = true;
app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into());
@@ -1412,7 +1455,7 @@ mod tests {
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
assert!(!app.data.radarr_data.is_searching);
assert!(!app.should_ignore_quit_key);
assert!(app.data.radarr_data.search.is_empty());
assert!(app.data.radarr_data.search.text.is_empty());
}
#[test]
@@ -1423,7 +1466,7 @@ mod tests {
.radarr_data
.movies
.set_items(extended_stateful_iterable_vec!(Movie));
app.data.radarr_data.search = "Test 5".to_owned();
app.data.radarr_data.search = "Test 5".to_owned().into();
app.data.radarr_data.is_searching = true;
app.should_ignore_quit_key = true;
app.push_navigation_stack(ActiveRadarrBlock::SearchMovie.into());
@@ -1445,7 +1488,7 @@ mod tests {
);
assert!(!app.data.radarr_data.is_searching);
assert!(!app.should_ignore_quit_key);
assert!(app.data.radarr_data.search.is_empty());
assert!(app.data.radarr_data.search.text.is_empty());
}
#[test]
@@ -1456,7 +1499,7 @@ mod tests {
.radarr_data
.movies
.set_items(extended_stateful_iterable_vec!(Movie));
app.data.radarr_data.filter = "Test 2".to_owned();
app.data.radarr_data.filter = "Test 2".to_owned().into();
app.data.radarr_data.is_searching = true;
app.should_ignore_quit_key = true;
app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into());
@@ -1476,7 +1519,7 @@ mod tests {
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
assert!(!app.data.radarr_data.is_filtering);
assert!(!app.should_ignore_quit_key);
assert!(app.data.radarr_data.filter.is_empty());
assert!(app.data.radarr_data.filter.text.is_empty());
}
#[test]
@@ -1487,7 +1530,7 @@ mod tests {
.radarr_data
.movies
.set_items(extended_stateful_iterable_vec!(Movie));
app.data.radarr_data.filter = "Test 5".to_owned();
app.data.radarr_data.filter = "Test 5".to_owned().into();
app.data.radarr_data.is_filtering = true;
app.should_ignore_quit_key = true;
app.push_navigation_stack(ActiveRadarrBlock::FilterMovies.into());
@@ -1509,7 +1552,7 @@ mod tests {
);
assert!(!app.data.radarr_data.is_searching);
assert!(!app.should_ignore_quit_key);
assert!(app.data.radarr_data.filter.is_empty());
assert!(app.data.radarr_data.filter.text.is_empty());
}
#[rstest]
@@ -271,12 +271,7 @@ fn sort_releases_by_selected_field(
ReleaseField::Source => |release_a, release_b| release_a.protocol.cmp(&release_b.protocol),
ReleaseField::Age => |release_a, release_b| release_a.age.as_u64().cmp(&release_b.age.as_u64()),
ReleaseField::Rejected => |release_a, release_b| release_a.rejected.cmp(&release_b.rejected),
ReleaseField::Title => |release_a, release_b| {
release_a
.title
.stationary_style()
.cmp(&release_b.title.stationary_style())
},
ReleaseField::Title => |release_a, release_b| release_a.title.text.cmp(&release_b.title.text),
ReleaseField::Indexer => |release_a, release_b| release_a.indexer.cmp(&release_b.indexer),
ReleaseField::Size => |release_a, release_b| {
release_a
@@ -394,7 +389,7 @@ mod tests {
ActiveRadarrBlock::MovieHistory,
None,
source_title,
stationary_style
to_string
);
test_iterable_scroll!(
@@ -427,7 +422,7 @@ mod tests {
ActiveRadarrBlock::ManualSearch,
None,
title,
stationary_style
to_string
);
test_enum_scroll!(
@@ -484,7 +479,7 @@ mod tests {
ActiveRadarrBlock::MovieHistory,
None,
source_title,
stationary_style
to_string
);
test_iterable_home_and_end!(
@@ -517,7 +512,7 @@ mod tests {
ActiveRadarrBlock::ManualSearch,
None,
title,
stationary_style
to_string
);
test_enum_home_and_end!(
@@ -717,11 +712,13 @@ mod tests {
}
mod test_handle_esc {
use pretty_assertions::assert_eq;
use bimap::BiMap;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use crate::app::radarr::radarr_test_utils::create_test_radarr_data;
use crate::assert_movie_info_tabs_reset;
use crate::models::HorizontallyScrollableText;
use super::*;
@@ -775,7 +772,8 @@ mod tests {
mod test_handle_key_char {
use std::collections::HashMap;
use pretty_assertions::assert_eq;
use bimap::BiMap;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator;
@@ -783,6 +781,7 @@ mod tests {
use crate::app::radarr::RadarrData;
use crate::handlers::radarr_handlers::RadarrHandler;
use crate::models::radarr_models::{MinimumAvailability, Movie};
use crate::models::HorizontallyScrollableText;
use crate::models::StatefulTable;
use crate::test_edit_movie_key;
+142 -56
View File
@@ -101,10 +101,6 @@ macro_rules! stateful_iterable {
pub fn current_selection(&self) -> &T {
&self.items[self.state.selected().unwrap_or(0)]
}
pub fn current_selection_clone(&self) -> T {
self.items[self.state.selected().unwrap_or(0)].clone()
}
}
};
}
@@ -169,8 +165,8 @@ pub struct HorizontallyScrollableText {
}
impl From<String> for HorizontallyScrollableText {
fn from(input: String) -> HorizontallyScrollableText {
HorizontallyScrollableText::new(input)
fn from(text: String) -> HorizontallyScrollableText {
HorizontallyScrollableText::new(text)
}
}
@@ -193,32 +189,60 @@ impl Display for HorizontallyScrollableText {
}
impl HorizontallyScrollableText {
pub fn new(input: String) -> HorizontallyScrollableText {
pub fn new(text: String) -> HorizontallyScrollableText {
HorizontallyScrollableText {
text: format!("{} ", input),
text,
offset: RefCell::new(0),
}
}
pub fn scroll_text(&self) {
let new_offset = *self.offset.borrow() + 1;
*self.offset.borrow_mut() = new_offset % self.text.len();
pub fn scroll_left(&self) {
if *self.offset.borrow() < self.text.len() {
let new_offset = *self.offset.borrow() + 1;
*self.offset.borrow_mut() = new_offset;
}
}
pub fn scroll_right(&self) {
if *self.offset.borrow() > 0 {
let new_offset = *self.offset.borrow() - 1;
*self.offset.borrow_mut() = new_offset;
}
}
pub fn scroll_home(&self) {
*self.offset.borrow_mut() = self.text.len();
}
pub fn reset_offset(&self) {
*self.offset.borrow_mut() = 0;
}
pub fn scroll_or_reset(&self, width: usize, is_current_selection: bool) {
if is_current_selection && self.text.len().saturating_sub(4) > width {
self.scroll_text();
pub fn scroll_left_or_reset(&self, width: usize, is_current_selection: bool) {
if is_current_selection && self.text.len() >= width && *self.offset.borrow() < self.text.len() {
self.scroll_left();
} else {
self.reset_offset();
}
}
pub fn stationary_style(&self) -> String {
self.text.clone().trim().to_owned()
pub fn drain(&mut self) -> String {
self.reset_offset();
self.text.drain(..).collect()
}
pub fn pop(&mut self) {
if *self.offset.borrow() < self.text.len() {
self
.text
.remove(self.text.len() - *self.offset.borrow() - 1);
}
}
pub fn push(&mut self, character: char) {
self
.text
.insert(self.text.len() - *self.offset.borrow(), character);
}
}
@@ -345,23 +369,6 @@ mod tests {
assert_str_eq!(stateful_table.current_selection(), &stateful_table.items[1]);
}
#[test]
fn test_stateful_table_current_selection_clone() {
let mut stateful_table = create_test_stateful_table();
assert_str_eq!(
stateful_table.current_selection_clone(),
stateful_table.items[0]
);
stateful_table.state.select(Some(1));
assert_str_eq!(
stateful_table.current_selection_clone(),
stateful_table.items[1]
);
}
#[test]
fn test_stateful_table_select_index() {
let mut stateful_table = create_test_stateful_table();
@@ -444,10 +451,7 @@ mod tests {
let horizontally_scrollable_text = HorizontallyScrollableText::from(test_text.to_owned());
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_str_eq!(
horizontally_scrollable_text.text,
format!("{} ", test_text)
);
assert_str_eq!(horizontally_scrollable_text.text, test_text);
}
#[test]
@@ -455,10 +459,7 @@ mod tests {
let test_text = "Test string";
let horizontally_scrollable_text = HorizontallyScrollableText::from(test_text.to_owned());
assert_str_eq!(
horizontally_scrollable_text.to_string(),
format!("{} ", test_text)
);
assert_str_eq!(horizontally_scrollable_text.to_string(), test_text);
let horizontally_scrollable_text = HorizontallyScrollableText {
text: test_text.to_owned(),
@@ -481,29 +482,60 @@ mod tests {
let horizontally_scrollable_text = HorizontallyScrollableText::new(test_text.to_owned());
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
assert_str_eq!(
horizontally_scrollable_text.text,
format!("{} ", test_text)
);
assert_str_eq!(horizontally_scrollable_text.text, test_text);
}
#[test]
fn test_horizontally_scrollable_text_scroll_text() {
fn test_horizontally_scrollable_text_scroll_text_left() {
let horizontally_scrollable_text = HorizontallyScrollableText::from("Test string".to_owned());
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
for i in 1..horizontally_scrollable_text.text.len() {
horizontally_scrollable_text.scroll_text();
for i in 1..horizontally_scrollable_text.text.len() - 1 {
horizontally_scrollable_text.scroll_left();
assert_eq!(*horizontally_scrollable_text.offset.borrow(), i);
}
horizontally_scrollable_text.scroll_text();
horizontally_scrollable_text.scroll_left();
assert_eq!(
*horizontally_scrollable_text.offset.borrow(),
horizontally_scrollable_text.text.len() - 1
);
}
#[test]
fn test_horizontally_scrollable_text_scroll_text_right() {
let horizontally_scrollable_text = HorizontallyScrollableText::from("Test string".to_owned());
*horizontally_scrollable_text.offset.borrow_mut() = horizontally_scrollable_text.text.len();
for i in 1..horizontally_scrollable_text.text.len() {
horizontally_scrollable_text.scroll_right();
assert_eq!(
*horizontally_scrollable_text.offset.borrow(),
horizontally_scrollable_text.text.len() - i
);
}
horizontally_scrollable_text.scroll_right();
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
}
#[test]
fn test_horizontally_scrollable_text_scroll_home() {
let horizontally_scrollable_text = HorizontallyScrollableText::from("Test string".to_owned());
horizontally_scrollable_text.scroll_home();
assert_eq!(
*horizontally_scrollable_text.offset.borrow(),
horizontally_scrollable_text.text.len()
);
}
#[test]
fn test_horizontally_scrollable_text_reset_offset() {
let horizontally_scrollable_text = HorizontallyScrollableText {
@@ -522,29 +554,83 @@ mod tests {
let test_text = "Test string";
let horizontally_scrollable_text = HorizontallyScrollableText::from(test_text.to_owned());
horizontally_scrollable_text.scroll_or_reset(width, true);
horizontally_scrollable_text.scroll_left_or_reset(width, true);
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 1);
horizontally_scrollable_text.scroll_or_reset(width, false);
horizontally_scrollable_text.scroll_left_or_reset(width, false);
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
horizontally_scrollable_text.scroll_or_reset(width, true);
horizontally_scrollable_text.scroll_left_or_reset(width, true);
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 1);
horizontally_scrollable_text.scroll_or_reset(test_text.len(), false);
horizontally_scrollable_text.scroll_left_or_reset(test_text.len(), false);
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
}
#[test]
fn test_horizontally_scrollable_text_stationary_style() {
fn test_horizontally_scrollable_text_drain() {
let test_text = "Test string";
let horizontally_scrollable_text = HorizontallyScrollableText::from(test_text.to_owned());
let mut horizontally_scrollable_text = HorizontallyScrollableText::from(test_text.to_owned());
assert_eq!(horizontally_scrollable_text.stationary_style(), test_text);
assert_str_eq!(horizontally_scrollable_text.drain(), test_text);
assert!(horizontally_scrollable_text.text.is_empty());
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
}
#[test]
fn test_horizontally_scrollable_text_pop() {
let test_text = "Test string";
let mut horizontally_scrollable_text = HorizontallyScrollableText::from(test_text.to_owned());
horizontally_scrollable_text.pop();
assert_str_eq!(horizontally_scrollable_text.text, "Test strin");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
horizontally_scrollable_text.scroll_left();
horizontally_scrollable_text.pop();
assert_str_eq!(horizontally_scrollable_text.text, "Test strn");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 1);
horizontally_scrollable_text.scroll_right();
horizontally_scrollable_text.scroll_right();
horizontally_scrollable_text.pop();
assert_str_eq!(horizontally_scrollable_text.text, "Test str");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
horizontally_scrollable_text.scroll_home();
horizontally_scrollable_text.pop();
assert_str_eq!(horizontally_scrollable_text.text, "Test str");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 8);
}
#[test]
fn test_horizontally_scrollable_text_push() {
let test_text = "Test string";
let mut horizontally_scrollable_text = HorizontallyScrollableText::from(test_text.to_owned());
horizontally_scrollable_text.push('h');
assert_str_eq!(horizontally_scrollable_text.text, "Test stringh");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
horizontally_scrollable_text.scroll_left();
horizontally_scrollable_text.push('l');
assert_str_eq!(horizontally_scrollable_text.text, "Test stringlh");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 1);
horizontally_scrollable_text.scroll_right();
horizontally_scrollable_text.scroll_right();
horizontally_scrollable_text.push('0');
assert_str_eq!(horizontally_scrollable_text.text, "Test stringlh0");
assert_eq!(*horizontally_scrollable_text.offset.borrow(), 0);
}
#[test]
+10 -1
View File
@@ -57,6 +57,7 @@ pub struct Movie {
pub quality_profile_id: Number,
pub minimum_availability: MinimumAvailability,
pub certification: Option<String>,
pub tags: Vec<Number>,
pub ratings: RatingsList,
pub movie_file: Option<MovieFile>,
pub collection: Option<Collection>,
@@ -169,13 +170,20 @@ pub struct DownloadRecord {
#[derive(Derivative, Deserialize, Debug)]
#[derivative(Default)]
#[serde(rename_all = "camelCase")]
pub struct QualityProfile {
#[derivative(Default(value = "Number::from(0)"))]
pub id: Number,
pub name: String,
}
#[derive(Derivative, Deserialize, Debug)]
#[derivative(Default)]
pub struct Tag {
#[derivative(Default(value = "Number::from(0)"))]
pub id: Number,
pub label: String,
}
#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct MovieHistoryItem {
@@ -265,6 +273,7 @@ pub struct AddMovieBody {
pub quality_profile_id: u64,
pub minimum_availability: String,
pub monitored: bool,
pub tags: Vec<u64>,
pub add_options: AddOptions,
}
+29 -25
View File
@@ -22,7 +22,6 @@ pub enum NetworkEvent {
pub struct Network<'a> {
pub client: Client,
pub app: &'a Arc<Mutex<App>>,
}
@@ -40,12 +39,12 @@ impl<'a> Network<'a> {
app.is_loading = false;
}
pub async fn handle_request<T, R>(
pub async fn handle_request<B, R>(
&self,
request_props: RequestProps<T>,
request_props: RequestProps<B>,
mut app_update_fn: impl FnMut(R, MutexGuard<'_, App>),
) where
T: Serialize + Default + Debug,
B: Serialize + Default + Debug,
R: DeserializeOwned,
{
let method = request_props.method;
@@ -53,21 +52,23 @@ impl<'a> Network<'a> {
Ok(response) => {
if response.status().is_success() {
match method {
RequestMethod::Get => match utils::parse_response::<R>(response).await {
Ok(value) => {
let app = self.app.lock().await;
app_update_fn(value, app);
RequestMethod::Get | RequestMethod::Post => {
match utils::parse_response::<R>(response).await {
Ok(value) => {
let app = self.app.lock().await;
app_update_fn(value, app);
}
Err(e) => {
error!("Failed to parse response! {:?}", e);
self
.app
.lock()
.await
.handle_error(anyhow!("Failed to parse response! {:?}", e));
}
}
Err(e) => {
error!("Failed to parse response! {:?}", e);
self
.app
.lock()
.await
.handle_error(anyhow!("Failed to parse response! {:?}", e));
}
},
RequestMethod::Delete | RequestMethod::Post | RequestMethod::Put => (),
}
RequestMethod::Delete | RequestMethod::Put => (),
}
} else {
error!(
@@ -224,16 +225,19 @@ mod tests {
async_server.assert_async().await;
}
#[rstest]
#[tokio::test]
async fn test_handle_request_get() {
let (async_server, app_arc, server) = mock_api(RequestMethod::Get, 200, true).await;
async fn test_handle_request_with_response_body(
#[values(RequestMethod::Get, RequestMethod::Post)] request_method: RequestMethod,
) {
let (async_server, app_arc, server) = mock_api(request_method, 200, true).await;
let network = Network::new(reqwest::Client::new(), &app_arc);
network
.handle_request::<(), Test>(
RequestProps {
uri: format!("{}/test", server.url()),
method: RequestMethod::Get,
method: request_method,
body: None,
api_token: "test1234".to_owned(),
},
@@ -242,7 +246,7 @@ mod tests {
.await;
async_server.assert_async().await;
assert_str_eq!(app_arc.lock().await.error.stationary_style(), "Test");
assert_str_eq!(app_arc.lock().await.error.text, "Test");
}
#[tokio::test]
@@ -275,7 +279,7 @@ mod tests {
.lock()
.await
.error
.stationary_style()
.text
.starts_with("Failed to parse response!"));
}
@@ -300,7 +304,7 @@ mod tests {
.lock()
.await
.error
.stationary_style()
.text
.starts_with("Failed to send request."));
}
@@ -332,7 +336,7 @@ mod tests {
async_server.assert_async().await;
assert_str_eq!(
app_arc.lock().await.error.stationary_style(),
app_arc.lock().await.error.text,
"Request failed. Received 404 Not Found response code"
);
}
+277 -85
View File
@@ -11,7 +11,7 @@ use crate::app::RadarrConfig;
use crate::models::radarr_models::{
AddMovieBody, AddMovieSearchResult, AddOptions, Collection, CollectionMovie, CommandBody, Credit,
CreditType, DiskSpace, DownloadRecord, DownloadsResponse, Movie, MovieCommandBody,
MovieHistoryItem, QualityProfile, Release, ReleaseDownloadBody, RootFolder, SystemStatus,
MovieHistoryItem, QualityProfile, Release, ReleaseDownloadBody, RootFolder, SystemStatus, Tag,
};
use crate::models::{Route, ScrollableText};
use crate::network::{Network, NetworkEvent, RequestMethod, RequestProps};
@@ -35,6 +35,7 @@ pub enum RadarrEvent {
GetReleases,
GetRootFolders,
GetStatus,
GetTags,
HealthCheck,
RefreshAndScan,
RefreshCollections,
@@ -62,6 +63,7 @@ impl RadarrEvent {
RadarrEvent::GetReleases | RadarrEvent::DownloadRelease => "/release",
RadarrEvent::GetRootFolders => "/rootfolder",
RadarrEvent::GetStatus => "/system/status",
RadarrEvent::GetTags => "/tag",
RadarrEvent::TriggerAutomaticSearch
| RadarrEvent::RefreshAndScan
| RadarrEvent::UpdateAllMovies
@@ -97,6 +99,7 @@ impl<'a> Network<'a> {
RadarrEvent::GetReleases => self.get_releases().await,
RadarrEvent::GetRootFolders => self.get_root_folders().await,
RadarrEvent::GetStatus => self.get_status().await,
RadarrEvent::GetTags => self.get_tags().await,
RadarrEvent::HealthCheck => self.get_healthcheck().await,
RadarrEvent::RefreshAndScan => self.refresh_and_scan().await,
RadarrEvent::RefreshCollections => self.refresh_collections().await,
@@ -205,13 +208,13 @@ impl<'a> Network<'a> {
async fn search_movie(&self) {
info!("Searching for specific Radarr movie");
let search_string = self.app.lock().await.data.radarr_data.search.clone();
let search_string = &self.app.lock().await.data.radarr_data.search.text;
let request_props = self
.radarr_request_props_from(
format!(
"{}?term={}",
RadarrEvent::SearchNewMovie.resource(),
encode(&search_string)
encode(search_string)
)
.as_str(),
RequestMethod::Get,
@@ -567,13 +570,56 @@ impl<'a> Network<'a> {
self
.handle_request::<(), Vec<QualityProfile>>(request_props, |quality_profiles, mut app| {
app.data.radarr_data.quality_profile_map = quality_profiles
.iter()
.map(|profile| (profile.id.as_u64().unwrap(), profile.name.clone()))
.into_iter()
.map(|profile| (profile.id.as_u64().unwrap(), profile.name))
.collect();
})
.await;
}
async fn get_tags(&self) {
info!("Fetching Radarr tags");
let request_props = self
.radarr_request_props_from(
RadarrEvent::GetTags.resource(),
RequestMethod::Get,
None::<()>,
)
.await;
self
.handle_request::<(), Vec<Tag>>(request_props, |tags_vec, mut app| {
app.data.radarr_data.tags_map = tags_vec
.into_iter()
.map(|tag| (tag.id.as_u64().unwrap(), tag.label))
.collect();
})
.await;
}
async fn add_tag(&self, tag: String) {
info!("Adding a new Radarr tag");
let request_props = self
.radarr_request_props_from(
RadarrEvent::GetTags.resource(),
RequestMethod::Post,
Some(json!({ "label": tag })),
)
.await;
self
.handle_request::<Value, Tag>(request_props, |tag, mut app| {
app
.data
.radarr_data
.tags_map
.insert(tag.id.as_u64().unwrap(), tag.label);
})
.await;
}
async fn get_root_folders(&self) {
info!("Fetching Radarr root folders");
@@ -677,34 +723,26 @@ impl<'a> Network<'a> {
async fn add_movie(&self) {
info!("Adding new movie to Radarr");
let body = {
let quality_profile_id = self.extract_quality_profile_id().await;
let tag_ids_vec = self.extract_and_add_tag_ids_vec().await;
let app = self.app.lock().await;
let root_folders = app.data.radarr_data.root_folders.to_vec();
let (tmdb_id, title) = if let Route::Radarr(active_radarr_block, _) = app.get_current_route()
{
if *active_radarr_block == ActiveRadarrBlock::CollectionDetails {
let CollectionMovie { tmdb_id, title, .. } = app
.data
.radarr_data
.collection_movies
.current_selection_clone();
(tmdb_id, title.stationary_style())
let CollectionMovie { tmdb_id, title, .. } =
app.data.radarr_data.collection_movies.current_selection();
(tmdb_id, title.text.clone())
} else {
let AddMovieSearchResult { tmdb_id, title, .. } = app
.data
.radarr_data
.add_searched_movies
.current_selection_clone();
(tmdb_id, title.stationary_style())
let AddMovieSearchResult { tmdb_id, title, .. } =
app.data.radarr_data.add_searched_movies.current_selection();
(tmdb_id, title.text.clone())
}
} else {
let AddMovieSearchResult { tmdb_id, title, .. } = app
.data
.radarr_data
.add_searched_movies
.current_selection_clone();
(tmdb_id, title.stationary_style())
let AddMovieSearchResult { tmdb_id, title, .. } =
app.data.radarr_data.add_searched_movies.current_selection();
(tmdb_id, title.text.clone())
};
let quality_profile_map = app.data.radarr_data.quality_profile_map.clone();
let RootFolder { path, .. } = root_folders
.iter()
@@ -729,17 +767,6 @@ impl<'a> Network<'a> {
.movie_minimum_availability_list
.current_selection()
.to_string();
let quality_profile = app
.data
.radarr_data
.movie_quality_profile_list
.current_selection_clone();
let quality_profile_id = quality_profile_map
.iter()
.filter(|(_, value)| **value == quality_profile)
.map(|(key, _)| key)
.next()
.unwrap();
AddMovieBody {
tmdb_id: tmdb_id.as_u64().unwrap(),
@@ -747,7 +774,8 @@ impl<'a> Network<'a> {
root_folder_path: path.to_owned(),
minimum_availability,
monitored: true,
quality_profile_id: *quality_profile_id,
quality_profile_id,
tags: tag_ids_vec,
add_options: AddOptions {
monitor,
search_for_movie: true,
@@ -785,22 +813,20 @@ impl<'a> Network<'a> {
self
.handle_request::<(), Value>(request_props, |detailed_movie_body, mut app| {
app.data.radarr_data.movie_details =
ScrollableText::with_string(detailed_movie_body.to_string())
app.response = detailed_movie_body.to_string()
})
.await;
info!("Constructing edit movie body");
let body = {
let quality_profile_id = self.extract_quality_profile_id().await;
let tag_ids_vec = self.extract_and_add_tag_ids_vec().await;
let mut app = self.app.lock().await;
let mut detailed_movie_body: Value =
serde_json::from_str(&app.data.radarr_data.movie_details.get_text()).unwrap();
app.data.radarr_data.movie_details = ScrollableText::default();
let mut detailed_movie_body: Value = serde_json::from_str(&app.response).unwrap();
app.response = String::default();
let quality_profile_map = app.data.radarr_data.quality_profile_map.clone();
let path: String = app.data.radarr_data.edit_path.drain(..).collect();
let _tags: String = app.data.radarr_data.edit_tags.drain(..).collect();
let path: String = app.data.radarr_data.edit_path.drain();
let monitored = app.data.radarr_data.edit_monitored.unwrap_or_default();
let minimum_availability = app
@@ -809,22 +835,12 @@ impl<'a> Network<'a> {
.movie_minimum_availability_list
.current_selection()
.to_string();
let quality_profile = app
.data
.radarr_data
.movie_quality_profile_list
.current_selection_clone();
let quality_profile_id = quality_profile_map
.iter()
.filter(|(_, value)| **value == quality_profile)
.map(|(key, _)| key)
.next()
.unwrap();
*detailed_movie_body.get_mut("monitored").unwrap() = json!(monitored);
*detailed_movie_body.get_mut("minimumAvailability").unwrap() = json!(minimum_availability);
*detailed_movie_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id);
*detailed_movie_body.get_mut("path").unwrap() = json!(path);
*detailed_movie_body.get_mut("tags").unwrap() = json!(tag_ids_vec);
detailed_movie_body
};
@@ -845,25 +861,21 @@ impl<'a> Network<'a> {
}
async fn download_release(&self) {
let Release {
guid,
title,
indexer_id,
..
} = self
.app
.lock()
.await
.data
.radarr_data
.movie_releases
.current_selection_clone();
let (guid, title, indexer_id) = {
let app = self.app.lock().await;
let Release {
guid,
title,
indexer_id,
..
} = app.data.radarr_data.movie_releases.current_selection();
(guid.clone(), title.clone(), indexer_id.as_u64().unwrap())
};
info!("Downloading release: {}", title);
let download_release_body = ReleaseDownloadBody {
guid,
indexer_id: indexer_id.as_u64().unwrap(),
};
let download_release_body = ReleaseDownloadBody { guid, indexer_id };
let request_props = self
.radarr_request_props_from(
@@ -878,6 +890,65 @@ impl<'a> Network<'a> {
.await;
}
async fn extract_quality_profile_id(&self) -> u64 {
let app = self.app.lock().await;
let quality_profile = app
.data
.radarr_data
.movie_quality_profile_list
.current_selection();
*app
.data
.radarr_data
.quality_profile_map
.iter()
.filter(|(_, value)| *value == quality_profile)
.map(|(key, _)| key)
.next()
.unwrap()
}
async fn extract_and_add_tag_ids_vec(&self) -> Vec<u64> {
let tags_map = self.app.lock().await.data.radarr_data.tags_map.clone();
let edit_tags = &self
.app
.lock()
.await
.data
.radarr_data
.edit_tags
.text
.clone();
let missing_tags_vec = edit_tags
.split(',')
.into_iter()
.filter(|&tag| !tag.is_empty() && tags_map.get_by_right(tag.trim()).is_none())
.collect::<Vec<&str>>();
for tag in missing_tags_vec {
self.add_tag(tag.trim().to_owned()).await;
}
let app = self.app.lock().await;
app
.data
.radarr_data
.edit_tags
.text
.split(',')
.into_iter()
.filter(|tag| !tag.is_empty())
.map(|tag| {
*app
.data
.radarr_data
.tags_map
.get_by_right(tag.trim())
.unwrap()
})
.collect()
}
async fn extract_movie_id(&self) -> u64 {
if !self
.app
@@ -898,7 +969,6 @@ impl<'a> Network<'a> {
.filtered_movies
.current_selection()
.id
.clone()
.as_u64()
.unwrap()
} else {
@@ -911,7 +981,6 @@ impl<'a> Network<'a> {
.movies
.current_selection()
.id
.clone()
.as_u64()
.unwrap()
}
@@ -972,6 +1041,7 @@ mod test {
use std::collections::HashMap;
use std::sync::Arc;
use bimap::BiMap;
use chrono::{DateTime, Utc};
use mockito::{Matcher, Mock, Server, ServerGuard};
use pretty_assertions::{assert_eq, assert_str_eq};
@@ -1010,6 +1080,7 @@ mod test {
"qualityProfileId": 2222,
"minimumAvailability": "announced",
"certification": "R",
"tags": [1],
"ratings": {
"imdb": {
"value": 9.9
@@ -1324,7 +1395,7 @@ mod test {
.as_str(),
)
.await;
app_arc.lock().await.data.radarr_data.search = "test term".to_owned();
app_arc.lock().await.data.radarr_data.search = "test term".to_owned().into();
let network = Network::new(reqwest::Client::new(), &app_arc);
network
@@ -1565,6 +1636,7 @@ mod test {
"runtime": 120,
"tmdbId": 1234,
"qualityProfileId": 2222,
"tags": [1],
"minimumAvailability": "released",
"ratings": {}
});
@@ -1782,6 +1854,56 @@ mod test {
);
}
#[tokio::test]
async fn test_handle_get_tags_event() {
let tags_json = json!([{
"id": 2222,
"label": "usenet"
}]);
let (async_server, app_arc, _server) = mock_radarr_api(
RequestMethod::Get,
None,
Some(tags_json),
RadarrEvent::GetTags.resource(),
)
.await;
let network = Network::new(reqwest::Client::new(), &app_arc);
network.handle_radarr_event(RadarrEvent::GetTags).await;
async_server.assert_async().await;
assert_eq!(
app_arc.lock().await.data.radarr_data.tags_map,
BiMap::from_iter([(2222u64, "usenet".to_owned())])
);
}
#[tokio::test]
async fn test_add_tag() {
let (async_server, app_arc, _server) = mock_radarr_api(
RequestMethod::Post,
Some(json!({ "label": "testing" })),
Some(json!({ "id": 3, "label": "testing" })),
RadarrEvent::GetTags.resource(),
)
.await;
app_arc.lock().await.data.radarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]);
let network = Network::new(reqwest::Client::new(), &app_arc);
network.add_tag("testing".to_owned()).await;
async_server.assert_async().await;
assert_eq!(
app_arc.lock().await.data.radarr_data.tags_map,
BiMap::from_iter([
(1, "usenet".to_owned()),
(2, "test".to_owned()),
(3, "testing".to_owned())
])
);
}
#[tokio::test]
async fn test_handle_get_root_folders_event() {
let root_folder_json = json!([{
@@ -1915,6 +2037,7 @@ mod test {
"minimumAvailability": "announced",
"monitored": true,
"qualityProfileId": 2222,
"tags": [1, 2],
"addOptions": {
"monitor": "movieOnly",
"searchForMovie": true
@@ -1940,6 +2063,9 @@ mod test {
},
];
app.data.radarr_data.quality_profile_map = HashMap::from([(2222, "HD - 1080p".to_owned())]);
app.data.radarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
app.data.radarr_data.edit_tags = "usenet, testing".to_owned().into();
app
.data
.radarr_data
@@ -1984,6 +2110,7 @@ mod test {
*expected_body.get_mut("minimumAvailability").unwrap() = json!("announced");
*expected_body.get_mut("qualityProfileId").unwrap() = json!(1111);
*expected_body.get_mut("path").unwrap() = json!("/nfs/Test Path");
*expected_body.get_mut("tags").unwrap() = json!([1, 2]);
let (async_details_server, app_arc, mut server) = mock_radarr_api(
RequestMethod::Get,
@@ -2004,8 +2131,10 @@ mod test {
.await;
{
let mut app = app_arc.lock().await;
app.data.radarr_data.edit_tags = "test tag".to_owned();
app.data.radarr_data.edit_path = "/nfs/Test Path".to_owned();
app.data.radarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
app.data.radarr_data.edit_tags = "usenet, testing".to_owned().into();
app.data.radarr_data.edit_path = "/nfs/Test Path".to_owned().into();
app.data.radarr_data.edit_monitored = Some(false);
app
.data
@@ -2033,8 +2162,7 @@ mod test {
{
let app = app_arc.lock().await;
assert!(app.data.radarr_data.edit_path.is_empty());
assert!(app.data.radarr_data.edit_tags.is_empty());
assert!(app.data.radarr_data.edit_path.text.is_empty());
assert!(app.data.radarr_data.movie_details.items.is_empty());
}
}
@@ -2067,9 +2195,74 @@ mod test {
async_server.assert_async().await;
}
#[tokio::test]
async fn test_extract_quality_profile_id() {
let app_arc = Arc::new(Mutex::new(App::default()));
{
let mut app = app_arc.lock().await;
app
.data
.radarr_data
.movie_quality_profile_list
.set_items(vec!["Any".to_owned(), "HD - 1080p".to_owned()]);
app.data.radarr_data.quality_profile_map =
HashMap::from_iter([(1, "Any".to_owned()), (2, "HD - 1080p".to_owned())]);
}
let network = Network::new(reqwest::Client::new(), &app_arc);
assert_eq!(network.extract_quality_profile_id().await, 1);
}
#[tokio::test]
async fn test_extract_and_add_tag_ids_vec() {
let app_arc = Arc::new(Mutex::new(App::default()));
{
let mut app = app_arc.lock().await;
app.data.radarr_data.edit_tags = " test,hi ,, usenet ".to_owned().into();
app.data.radarr_data.tags_map = BiMap::from_iter([
(1, "usenet".to_owned()),
(2, "test".to_owned()),
(3, "hi".to_owned()),
]);
}
let network = Network::new(reqwest::Client::new(), &app_arc);
assert_eq!(network.extract_and_add_tag_ids_vec().await, vec![2, 3, 1]);
}
#[tokio::test]
async fn test_extract_and_add_tag_ids_vec_add_missing_tags_first() {
let (async_server, app_arc, _server) = mock_radarr_api(
RequestMethod::Post,
Some(json!({ "label": "testing" })),
Some(json!({ "id": 3, "label": "testing" })),
RadarrEvent::GetTags.resource(),
)
.await;
{
let mut app = app_arc.lock().await;
app.data.radarr_data.edit_tags = "usenet, test, testing".to_owned().into();
app.data.radarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]);
}
let network = Network::new(reqwest::Client::new(), &app_arc);
let tag_ids_vec = network.extract_and_add_tag_ids_vec().await;
async_server.assert_async().await;
assert_eq!(tag_ids_vec, vec![1, 2, 3]);
assert_eq!(
app_arc.lock().await.data.radarr_data.tags_map,
BiMap::from_iter([
(1, "usenet".to_owned()),
(2, "test".to_owned()),
(3, "testing".to_owned())
])
);
}
#[tokio::test]
async fn test_extract_movie_id() {
let id = Number::from(1);
let app_arc = Arc::new(Mutex::new(App::default()));
app_arc
.lock()
@@ -2078,7 +2271,7 @@ mod test {
.radarr_data
.movies
.set_items(vec![Movie {
id: id.clone(),
id: Number::from(1),
..Movie::default()
}]);
let network = Network::new(reqwest::Client::new(), &app_arc);
@@ -2088,7 +2281,6 @@ mod test {
#[tokio::test]
async fn test_extract_movie_id_filtered_movies() {
let id = Number::from(1);
let app_arc = Arc::new(Mutex::new(App::default()));
app_arc
.lock()
@@ -2097,7 +2289,7 @@ mod test {
.radarr_data
.filtered_movies
.set_items(vec![Movie {
id: id.clone(),
id: Number::from(1),
..Movie::default()
}]);
let network = Network::new(reqwest::Client::new(), &app_arc);
@@ -2107,7 +2299,6 @@ mod test {
#[tokio::test]
async fn test_append_movie_id_param() {
let id = Number::from(1);
let app_arc = Arc::new(Mutex::new(App::default()));
app_arc
.lock()
@@ -2116,7 +2307,7 @@ mod test {
.radarr_data
.movies
.set_items(vec![Movie {
id: id.clone(),
id: Number::from(1),
..Movie::default()
}]);
let network = Network::new(reqwest::Client::new(), &app_arc);
@@ -2342,6 +2533,7 @@ mod test {
quality_profile_id: Number::from(2222),
minimum_availability: MinimumAvailability::Announced,
certification: Some("R".to_owned()),
tags: vec![Number::from(1)],
ratings: ratings_list(),
movie_file: Some(movie_file()),
collection: Some(collection()),
+5 -2
View File
@@ -93,7 +93,7 @@ fn draw_error<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, area: Rect) {
.borders(Borders::ALL);
if app.error.text.len() > area.width as usize {
app.error.scroll_text();
app.error.scroll_left();
}
let mut text = Text::from(app.error.to_string());
@@ -497,6 +497,7 @@ pub fn draw_text_box<B: Backend>(
text_box_area: Rect,
block_title: Option<&str>,
block_content: &str,
offset: usize,
should_show_cursor: bool,
is_selected: bool,
) {
@@ -518,7 +519,7 @@ pub fn draw_text_box<B: Backend>(
f.render_widget(search_paragraph, text_box_area);
if should_show_cursor {
show_cursor(f, text_box_area, block_content);
show_cursor(f, text_box_area, offset, block_content);
}
}
@@ -527,6 +528,7 @@ pub fn draw_text_box_with_label<B: Backend>(
area: Rect,
label: &str,
text: &str,
offset: usize,
is_selected: bool,
should_show_cursor: bool,
) {
@@ -547,6 +549,7 @@ pub fn draw_text_box_with_label<B: Backend>(
horizontal_chunks[1],
None,
text,
offset,
should_show_cursor,
is_selected,
);
+27 -9
View File
@@ -70,7 +70,8 @@ fn draw_add_movie_search<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, area:
.data
.radarr_data
.add_searched_movies
.current_selection_clone()
.current_selection()
.clone()
};
let chunks = vertical_chunks_with_margin(
@@ -82,12 +83,21 @@ fn draw_add_movie_search<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, area:
area,
1,
);
let block_content = app.data.radarr_data.search.as_str();
let block_content = &app.data.radarr_data.search.text;
let offset = *app.data.radarr_data.search.offset.borrow();
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
match active_radarr_block {
ActiveRadarrBlock::AddMovieSearchInput => {
draw_text_box(f, chunks[0], Some("Add Movie"), block_content, true, false);
draw_text_box(
f,
chunks[0],
Some("Add Movie"),
block_content,
offset,
true,
false,
);
f.render_widget(layout_block(), chunks[1]);
let mut help_text = Text::from("<esc> close");
@@ -184,7 +194,7 @@ fn draw_add_movie_search<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, area:
""
};
movie.title.scroll_or_reset(
movie.title.scroll_left_or_reset(
get_width_from_percentage(area, 27),
*movie == current_selection,
);
@@ -208,7 +218,15 @@ fn draw_add_movie_search<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, area:
}
}
draw_text_box(f, chunks[0], Some("Add Movie"), block_content, false, false);
draw_text_box(
f,
chunks[0],
Some("Add Movie"),
block_content,
offset,
false,
false,
);
}
fn draw_confirmation_popup<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, prompt_area: Rect) {
@@ -260,13 +278,13 @@ fn draw_confirmation_prompt<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, pro
let title = "Add Movie";
let (movie_title, movie_overview) = if let Route::Radarr(_, Some(_)) = app.get_current_route() {
(
app
&app
.data
.radarr_data
.collection_movies
.current_selection()
.title
.stationary_style(),
.text,
app
.data
.radarr_data
@@ -277,13 +295,13 @@ fn draw_confirmation_prompt<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, pro
)
} else {
(
app
&app
.data
.radarr_data
.add_searched_movies
.current_selection()
.title
.stationary_style(),
.text,
app
.data
.radarr_data
+5 -3
View File
@@ -75,7 +75,8 @@ pub(super) fn draw_collection_details<B: Backend>(
.data
.radarr_data
.collection_movies
.current_selection_clone()
.current_selection()
.clone()
};
let mut help_text =
Text::from("<↑↓> scroll table | <enter> show overview/add movie | <esc> close");
@@ -151,7 +152,7 @@ pub(super) fn draw_collection_details<B: Backend>(
} else {
""
};
movie.title.scroll_or_reset(
movie.title.scroll_left_or_reset(
get_width_from_percentage(chunks[1], 20),
current_selection == *movie,
);
@@ -212,7 +213,8 @@ fn draw_movie_overview<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, content_
.data
.radarr_data
.collection_movies
.current_selection_clone()
.current_selection()
.clone()
.overview,
);
overview.patch_style(style_default());
+4 -2
View File
@@ -140,7 +140,8 @@ fn draw_edit_confirmation_prompt<B: Backend>(
f,
chunks[4],
"Path",
&app.data.radarr_data.edit_path,
&app.data.radarr_data.edit_path.text,
*app.data.radarr_data.edit_path.offset.borrow(),
*selected_block == ActiveRadarrBlock::EditMoviePathInput,
active_radarr_block == ActiveRadarrBlock::EditMoviePathInput,
);
@@ -148,7 +149,8 @@ fn draw_edit_confirmation_prompt<B: Backend>(
f,
chunks[5],
"Tags",
&app.data.radarr_data.edit_tags,
&app.data.radarr_data.edit_tags.text,
*app.data.radarr_data.edit_tags.offset.borrow(),
*selected_block == ActiveRadarrBlock::EditMovieTagsInput,
active_radarr_block == ActiveRadarrBlock::EditMovieTagsInput,
);
+42 -26
View File
@@ -158,6 +158,7 @@ pub(super) fn draw_radarr_context_row<B: Backend>(f: &mut Frame<'_, B>, app: &Ap
fn draw_library<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, area: Rect) {
let quality_profile_map = &app.data.radarr_data.quality_profile_map;
let tags_map = &app.data.radarr_data.tags_map;
let downloads_vec = &app.data.radarr_data.downloads.items;
let content = if !app.data.radarr_data.filtered_movies.items.is_empty()
&& !app.data.radarr_data.is_filtering
@@ -208,7 +209,21 @@ fn draw_library<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, area: Rect) {
let (hours, minutes) = convert_runtime(movie.runtime.as_u64().unwrap());
let file_size: f64 = convert_to_gb(movie.size_on_disk.as_u64().unwrap());
let certification = movie.certification.clone().unwrap_or_else(|| "".to_owned());
let tags = "";
let quality_profile = quality_profile_map
.get(&movie.quality_profile_id.as_u64().unwrap())
.unwrap()
.to_owned();
let tags = movie
.tags
.iter()
.map(|tag_id| {
tags_map
.get_by_left(&tag_id.as_u64().unwrap())
.unwrap()
.clone()
})
.collect::<Vec<String>>()
.join(", ");
Row::new(vec![
Cell::from(movie.title.to_owned()),
@@ -218,14 +233,9 @@ fn draw_library<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, area: Rect) {
Cell::from(certification),
Cell::from(movie.original_language.name.to_owned()),
Cell::from(format!("{:.2} GB", file_size)),
Cell::from(
quality_profile_map
.get(&movie.quality_profile_id.as_u64().unwrap())
.unwrap()
.to_owned(),
),
Cell::from(quality_profile),
Cell::from(monitored.to_owned()),
Cell::from(tags.to_owned()),
Cell::from(tags),
])
.style(determine_row_style(downloads_vec, movie))
},
@@ -322,20 +332,23 @@ fn draw_search_box<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, area: Rect)
f.render_widget(input, chunks[0]);
} else {
let (block_title, block_content) = match app.get_current_route() {
let default_content = String::default();
let (block_title, offset, block_content) = match app.get_current_route() {
Route::Radarr(active_radarr_block, _) => match active_radarr_block {
_ if SEARCH_BLOCKS.contains(active_radarr_block) => {
("Search", app.data.radarr_data.search.as_str())
}
_ => ("", ""),
_ if SEARCH_BLOCKS.contains(active_radarr_block) => (
"Search",
*app.data.radarr_data.search.offset.borrow(),
&app.data.radarr_data.search.text,
),
_ => ("", 0, &default_content),
},
_ => ("", ""),
_ => ("", 0, &default_content),
};
let input = Paragraph::new(block_content)
let input = Paragraph::new(block_content.as_str())
.style(style_default())
.block(title_block_centered(block_title));
show_cursor(f, chunks[0], block_content);
show_cursor(f, chunks[0], offset, block_content);
f.render_widget(input, chunks[0]);
}
@@ -360,20 +373,23 @@ fn draw_filter_box<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, area: Rect)
f.render_widget(input, chunks[0]);
} else {
let (block_title, block_content) = match app.get_current_route() {
let default_content = String::default();
let (block_title, offset, block_content) = match app.get_current_route() {
Route::Radarr(active_radarr_block, _) => match active_radarr_block {
_ if FILTER_BLOCKS.contains(active_radarr_block) => {
("Filter", app.data.radarr_data.filter.as_str())
}
_ => ("", ""),
_ if FILTER_BLOCKS.contains(active_radarr_block) => (
"Filter",
*app.data.radarr_data.filter.offset.borrow(),
&app.data.radarr_data.filter.text,
),
_ => ("", 0, &default_content),
},
_ => ("", ""),
_ => ("", 0, &default_content),
};
let input = Paragraph::new(block_content)
let input = Paragraph::new(block_content.as_str())
.style(style_default())
.block(title_block_centered(block_title));
show_cursor(f, chunks[0], block_content);
show_cursor(f, chunks[0], offset, block_content);
f.render_widget(input, chunks[0]);
}
@@ -413,7 +429,7 @@ fn draw_downloads<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, area: Rect) {
let current_selection = if app.data.radarr_data.downloads.items.is_empty() {
DownloadRecord::default()
} else {
app.data.radarr_data.downloads.current_selection_clone()
app.data.radarr_data.downloads.current_selection().clone()
};
draw_table(
@@ -456,7 +472,7 @@ fn draw_downloads<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, area: Rect) {
} = download_record;
let path = output_path.clone().unwrap_or_default();
path.scroll_or_reset(
path.scroll_left_or_reset(
get_width_from_percentage(area, 18),
current_selection == *download_record,
);
+13 -7
View File
@@ -113,7 +113,7 @@ fn draw_refresh_and_scan_prompt<B: Backend>(
"Refresh and Scan",
format!(
"Do you want to trigger a refresh and disk scan for the movie: {}?",
app.data.radarr_data.movies.current_selection_clone().title
app.data.radarr_data.movies.current_selection().title
)
.as_str(),
&app.data.radarr_data.prompt_confirm,
@@ -223,7 +223,12 @@ fn draw_movie_history<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, content_a
let current_selection = if app.data.radarr_data.movie_history.items.is_empty() {
MovieHistoryItem::default()
} else {
app.data.radarr_data.movie_history.current_selection_clone()
app
.data
.radarr_data
.movie_history
.current_selection()
.clone()
};
let block = layout_block_top_border();
@@ -263,7 +268,7 @@ fn draw_movie_history<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, content_a
event_type,
} = movie_history_item;
movie_history_item.source_title.scroll_or_reset(
movie_history_item.source_title.scroll_left_or_reset(
get_width_from_percentage(content_area, 34),
current_selection == *movie_history_item,
);
@@ -362,7 +367,8 @@ fn draw_movie_releases<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, content_
.data
.radarr_data
.movie_releases
.current_selection_clone()
.current_selection()
.clone()
};
let current_route = *app.get_current_route();
let mut table_headers_vec = vec![
@@ -432,7 +438,7 @@ fn draw_movie_releases<B: Backend>(f: &mut Frame<'_, B>, app: &mut App, content_
..
} = release;
let age = format!("{} days", age.as_u64().unwrap_or(0));
title.scroll_or_reset(
title.scroll_left_or_reset(
get_width_from_percentage(content_area, 30),
current_selection == *release
&& current_route != ActiveRadarrBlock::ManualSearchConfirmPrompt.into(),
@@ -488,12 +494,12 @@ fn draw_manual_search_confirm_prompt<B: Backend>(
let prompt = if current_selection.rejected {
format!(
"Do you really want to download the rejected release: {}?",
current_selection.title.stationary_style()
&current_selection.title.text
)
} else {
format!(
"Do you want to download the release: {}?",
current_selection.title.stationary_style()
&current_selection.title.text
)
};
+2 -2
View File
@@ -266,8 +266,8 @@ pub fn line_gauge_with_label(title: &str, ratio: f64) -> LineGauge<'_> {
.label(Spans::from(format!("{}: {:.0}%", title, ratio * 100.0)))
}
pub fn show_cursor<B: Backend>(f: &mut Frame<'_, B>, area: Rect, string: &str) {
f.set_cursor(area.x + string.len() as u16 + 1, area.y + 1);
pub fn show_cursor<B: Backend>(f: &mut Frame<'_, B>, area: Rect, offset: usize, string: &str) {
f.set_cursor(area.x + (string.len() - offset) as u16 + 1, area.y + 1);
}
pub fn get_width_from_percentage(area: Rect, percentage: u16) -> usize {