diff --git a/src/app/lidarr/lidarr_tests.rs b/src/app/lidarr/lidarr_tests.rs index 8b994c7..918dba6 100644 --- a/src/app/lidarr/lidarr_tests.rs +++ b/src/app/lidarr/lidarr_tests.rs @@ -113,6 +113,23 @@ mod tests { assert_eq!(app.tick_count, 0); } + #[tokio::test] + async fn test_dispatch_by_root_folders_block() { + let (tx, mut rx) = mpsc::channel::(500); + let mut app = App::test_default(); + app.data.lidarr_data.prompt_confirm = true; + app.network_tx = Some(tx); + + app + .dispatch_by_lidarr_block(&ActiveLidarrBlock::RootFolders) + .await; + + assert!(app.is_loading); + assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetRootFolders.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + assert_eq!(app.tick_count, 0); + } + #[tokio::test] async fn test_extract_add_new_artist_search_query() { let app = App::test_default_fully_populated(); diff --git a/src/app/lidarr/mod.rs b/src/app/lidarr/mod.rs index e20830f..2f1db7d 100644 --- a/src/app/lidarr/mod.rs +++ b/src/app/lidarr/mod.rs @@ -50,6 +50,11 @@ impl App<'_> { .dispatch_network_event(LidarrEvent::GetHistory(500).into()) .await } + ActiveLidarrBlock::RootFolders => { + self + .dispatch_network_event(LidarrEvent::GetRootFolders.into()) + .await; + } _ => (), } diff --git a/src/cli/lidarr/add_command_handler.rs b/src/cli/lidarr/add_command_handler.rs index 3aeb718..889aeb4 100644 --- a/src/cli/lidarr/add_command_handler.rs +++ b/src/cli/lidarr/add_command_handler.rs @@ -5,6 +5,7 @@ use clap::{ArgAction, Subcommand, arg}; use tokio::sync::Mutex; use super::LidarrCommand; +use crate::models::servarr_models::AddRootFolderBody; use crate::{ app::App, cli::{CliCommandHandler, Command}, @@ -75,6 +76,11 @@ pub enum LidarrAddCommand { )] no_search_for_missing_albums: bool, }, + #[command(about = "Add a new root folder")] + RootFolder { + #[arg(long, help = "The path of the new root folder", required = true)] + root_folder_path: String, + }, #[command(about = "Add new tag")] Tag { #[arg(long, help = "The name of the tag to be added", required = true)] @@ -142,6 +148,16 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrAddCommand> for LidarrAddCommandHan .await?; serde_json::to_string_pretty(&resp)? } + LidarrAddCommand::RootFolder { root_folder_path } => { + let add_root_folder_body = AddRootFolderBody { + path: root_folder_path, + }; + let resp = self + .network + .handle_network_event(LidarrEvent::AddRootFolder(add_root_folder_body).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } LidarrAddCommand::Tag { name } => { let resp = self .network diff --git a/src/cli/lidarr/add_command_handler_tests.rs b/src/cli/lidarr/add_command_handler_tests.rs index 5f24baf..7659cfa 100644 --- a/src/cli/lidarr/add_command_handler_tests.rs +++ b/src/cli/lidarr/add_command_handler_tests.rs @@ -27,6 +27,41 @@ mod tests { use super::*; use pretty_assertions::assert_eq; + #[test] + fn test_add_root_folder_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "root-folder"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_add_root_folder_success() { + let expected_args = LidarrAddCommand::RootFolder { + root_folder_path: "/nfs/test".to_owned(), + }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "add", + "root-folder", + "--root-folder-path", + "/nfs/test", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Add(add_command))) = result.unwrap().command else { + panic!("Unexpected command type"); + }; + assert_eq!(add_command, expected_args); + } + #[test] fn test_add_tag_requires_arguments() { let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "add", "tag"]); @@ -383,12 +418,44 @@ mod tests { use crate::models::lidarr_models::{ AddArtistBody, AddArtistOptions, LidarrSerdeable, MonitorType, NewItemMonitorType, }; + use crate::models::servarr_models::AddRootFolderBody; use crate::network::lidarr_network::LidarrEvent; use crate::{ app::App, network::{MockNetworkTrait, NetworkEvent}, }; + #[tokio::test] + async fn test_handle_add_root_folder_command() { + let expected_root_folder_path = "/nfs/test".to_owned(); + let expected_add_root_folder_body = AddRootFolderBody { + path: expected_root_folder_path.clone(), + }; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::AddRootFolder(expected_add_root_folder_body.clone()).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let add_root_folder_command = LidarrAddCommand::RootFolder { + root_folder_path: expected_root_folder_path, + }; + + let result = + LidarrAddCommandHandler::with(&app_arc, add_root_folder_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_handle_add_tag_command() { let expected_tag_name = "test".to_owned(); diff --git a/src/cli/lidarr/delete_command_handler.rs b/src/cli/lidarr/delete_command_handler.rs index e69d185..789c718 100644 --- a/src/cli/lidarr/delete_command_handler.rs +++ b/src/cli/lidarr/delete_command_handler.rs @@ -42,6 +42,11 @@ pub enum LidarrDeleteCommand { #[arg(long, help = "The ID of the download to delete", required = true)] download_id: i64, }, + #[command(about = "Delete the root folder with the given ID")] + RootFolder { + #[arg(long, help = "The ID of the root folder to delete", required = true)] + root_folder_id: i64, + }, #[command(about = "Delete the tag with the specified ID")] Tag { #[arg(long, help = "The ID of the tag to delete", required = true)] @@ -115,6 +120,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrDeleteCommand> for LidarrDeleteComm .await?; serde_json::to_string_pretty(&resp)? } + LidarrDeleteCommand::RootFolder { root_folder_id } => { + let resp = self + .network + .handle_network_event(LidarrEvent::DeleteRootFolder(root_folder_id).into()) + .await?; + serde_json::to_string_pretty(&resp)? + } LidarrDeleteCommand::Tag { tag_id } => { let resp = self .network diff --git a/src/cli/lidarr/delete_command_handler_tests.rs b/src/cli/lidarr/delete_command_handler_tests.rs index ac42ab4..f951906 100644 --- a/src/cli/lidarr/delete_command_handler_tests.rs +++ b/src/cli/lidarr/delete_command_handler_tests.rs @@ -179,6 +179,40 @@ mod tests { assert_eq!(delete_command, expected_args); } + #[test] + fn test_delete_root_folder_requires_arguments() { + let result = + Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "root-folder"]); + + assert_err!(&result); + assert_eq!( + result.unwrap_err().kind(), + ErrorKind::MissingRequiredArgument + ); + } + + #[test] + fn test_delete_root_folder_success() { + let expected_args = LidarrDeleteCommand::RootFolder { root_folder_id: 1 }; + + let result = Cli::try_parse_from([ + "managarr", + "lidarr", + "delete", + "root-folder", + "--root-folder-id", + "1", + ]); + + assert_ok!(&result); + + let Some(Command::Lidarr(LidarrCommand::Delete(delete_command))) = result.unwrap().command + else { + panic!("Unexpected command type"); + }; + assert_eq!(delete_command, expected_args); + } + #[test] fn test_delete_tag_requires_arguments() { let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "delete", "tag"]); @@ -320,6 +354,32 @@ mod tests { assert_ok!(&result); } + #[tokio::test] + async fn test_handle_delete_root_folder_command() { + let expected_root_folder_id = 1; + let mut mock_network = MockNetworkTrait::new(); + mock_network + .expect_handle_network_event() + .with(eq::( + LidarrEvent::DeleteRootFolder(expected_root_folder_id).into(), + )) + .times(1) + .returning(|_| { + Ok(Serdeable::Lidarr(LidarrSerdeable::Value( + json!({"testResponse": "response"}), + ))) + }); + let app_arc = Arc::new(Mutex::new(App::test_default())); + let delete_root_folder_command = LidarrDeleteCommand::RootFolder { root_folder_id: 1 }; + + let result = + LidarrDeleteCommandHandler::with(&app_arc, delete_root_folder_command, &mut mock_network) + .handle() + .await; + + assert_ok!(&result); + } + #[tokio::test] async fn test_handle_delete_tag_command() { let expected_tag_id = 1; diff --git a/src/cli/lidarr/list_command_handler.rs b/src/cli/lidarr/list_command_handler.rs index e0f457f..a3d1ff1 100644 --- a/src/cli/lidarr/list_command_handler.rs +++ b/src/cli/lidarr/list_command_handler.rs @@ -43,6 +43,8 @@ pub enum LidarrListCommand { MetadataProfiles, #[command(about = "List all Lidarr quality profiles")] QualityProfiles, + #[command(about = "List all root folders in Lidarr")] + RootFolders, #[command(about = "List all Lidarr tags")] Tags, } @@ -116,6 +118,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, LidarrListCommand> for LidarrListCommandH .await?; serde_json::to_string_pretty(&resp)? } + LidarrListCommand::RootFolders => { + let resp = self + .network + .handle_network_event(LidarrEvent::GetRootFolders.into()) + .await?; + serde_json::to_string_pretty(&resp)? + } LidarrListCommand::Tags => { let resp = self .network diff --git a/src/cli/lidarr/list_command_handler_tests.rs b/src/cli/lidarr/list_command_handler_tests.rs index cc61ac6..2aa1b7c 100644 --- a/src/cli/lidarr/list_command_handler_tests.rs +++ b/src/cli/lidarr/list_command_handler_tests.rs @@ -25,7 +25,14 @@ mod tests { #[rstest] fn test_list_commands_have_no_arg_requirements( - #[values("artists", "metadata-profiles", "quality-profiles", "tags")] subcommand: &str, + #[values( + "artists", + "metadata-profiles", + "quality-profiles", + "tags", + "root-folders" + )] + subcommand: &str, ) { let result = Cli::command().try_get_matches_from(["managarr", "lidarr", "list", subcommand]); @@ -127,6 +134,7 @@ mod tests { #[case(LidarrListCommand::Artists, LidarrEvent::ListArtists)] #[case(LidarrListCommand::MetadataProfiles, LidarrEvent::GetMetadataProfiles)] #[case(LidarrListCommand::QualityProfiles, LidarrEvent::GetQualityProfiles)] + #[case(LidarrListCommand::RootFolders, LidarrEvent::GetRootFolders)] #[case(LidarrListCommand::Tags, LidarrEvent::GetTags)] #[tokio::test] async fn test_handle_list_command( diff --git a/src/handlers/lidarr_handlers/history/history_handler_tests.rs b/src/handlers/lidarr_handlers/history/history_handler_tests.rs index 814203b..8cd4d3f 100644 --- a/src/handlers/lidarr_handlers/history/history_handler_tests.rs +++ b/src/handlers/lidarr_handlers/history/history_handler_tests.rs @@ -63,9 +63,12 @@ mod tests { assert_eq!( app.data.lidarr_data.main_tabs.get_active_route(), - ActiveLidarrBlock::Artists.into() + ActiveLidarrBlock::RootFolders.into() + ); + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::RootFolders.into() ); - assert_eq!(app.get_current_route(), ActiveLidarrBlock::Artists.into()); } } diff --git a/src/handlers/lidarr_handlers/lidarr_handler_tests.rs b/src/handlers/lidarr_handlers/lidarr_handler_tests.rs index 1bbf8f5..14176f0 100644 --- a/src/handlers/lidarr_handlers/lidarr_handler_tests.rs +++ b/src/handlers/lidarr_handlers/lidarr_handler_tests.rs @@ -52,9 +52,10 @@ mod tests { } #[rstest] - #[case(0, ActiveLidarrBlock::History, ActiveLidarrBlock::Downloads)] + #[case(0, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::Downloads)] #[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)] - #[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::Artists)] + #[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::RootFolders)] + #[case(3, ActiveLidarrBlock::History, ActiveLidarrBlock::Artists)] fn test_lidarr_handler_change_tab_left_right_keys( #[case] index: usize, #[case] left_block: ActiveLidarrBlock, @@ -83,9 +84,10 @@ mod tests { } #[rstest] - #[case(0, ActiveLidarrBlock::History, ActiveLidarrBlock::Downloads)] + #[case(0, ActiveLidarrBlock::RootFolders, ActiveLidarrBlock::Downloads)] #[case(1, ActiveLidarrBlock::Artists, ActiveLidarrBlock::History)] - #[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::Artists)] + #[case(2, ActiveLidarrBlock::Downloads, ActiveLidarrBlock::RootFolders)] + #[case(3, ActiveLidarrBlock::History, ActiveLidarrBlock::Artists)] fn test_lidarr_handler_change_tab_left_right_keys_alt_navigation( #[case] index: usize, #[case] left_block: ActiveLidarrBlock, @@ -117,6 +119,7 @@ mod tests { #[case(0, ActiveLidarrBlock::Artists)] #[case(1, ActiveLidarrBlock::Downloads)] #[case(2, ActiveLidarrBlock::History)] + #[case(3, ActiveLidarrBlock::RootFolders)] fn test_lidarr_handler_change_tab_left_right_keys_alt_navigation_no_op_when_ignoring_quit_key( #[case] index: usize, #[case] block: ActiveLidarrBlock, @@ -207,4 +210,20 @@ mod tests { active_lidarr_block ); } + + #[rstest] + fn test_delegates_root_folders_blocks_to_root_folders_handler( + #[values( + ActiveLidarrBlock::RootFolders, + ActiveLidarrBlock::AddRootFolderPrompt, + ActiveLidarrBlock::DeleteRootFolderPrompt + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + test_handler_delegation!( + LidarrHandler, + ActiveLidarrBlock::RootFolders, + active_lidarr_block + ); + } } diff --git a/src/handlers/lidarr_handlers/mod.rs b/src/handlers/lidarr_handlers/mod.rs index 9d38a39..e03b857 100644 --- a/src/handlers/lidarr_handlers/mod.rs +++ b/src/handlers/lidarr_handlers/mod.rs @@ -3,6 +3,7 @@ use library::LibraryHandler; use super::KeyEventHandler; use crate::handlers::lidarr_handlers::downloads::DownloadsHandler; +use crate::handlers::lidarr_handlers::root_folders::RootFoldersHandler; use crate::models::Route; use crate::{ app::App, event::Key, matches_key, models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock, @@ -15,6 +16,7 @@ mod downloads; #[cfg(test)] #[path = "lidarr_handler_tests.rs"] mod lidarr_handler_tests; +mod root_folders; pub(super) struct LidarrHandler<'a, 'b> { key: Key, @@ -35,6 +37,10 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b _ if HistoryHandler::accepts(self.active_lidarr_block) => { HistoryHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle(); } + _ if RootFoldersHandler::accepts(self.active_lidarr_block) => { + RootFoldersHandler::new(self.key, self.app, self.active_lidarr_block, self.context) + .handle(); + } _ => self.handle_key_event(), } } diff --git a/src/handlers/lidarr_handlers/root_folders/mod.rs b/src/handlers/lidarr_handlers/root_folders/mod.rs new file mode 100644 index 0000000..716076c --- /dev/null +++ b/src/handlers/lidarr_handlers/root_folders/mod.rs @@ -0,0 +1,235 @@ +use crate::app::App; +use crate::event::Key; +use crate::handlers::lidarr_handlers::handle_change_tab_left_right_keys; +use crate::handlers::table_handler::{TableHandlingConfig, handle_table}; +use crate::handlers::{KeyEventHandler, handle_clear_errors, handle_prompt_toggle}; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, ROOT_FOLDERS_BLOCKS}; +use crate::models::servarr_models::AddRootFolderBody; +use crate::models::{HorizontallyScrollableText, Route}; +use crate::network::lidarr_network::LidarrEvent; +use crate::{handle_text_box_keys, handle_text_box_left_right_keys, matches_key}; + +#[cfg(test)] +#[path = "root_folders_handler_tests.rs"] +mod root_folders_handler_tests; + +pub(super) struct RootFoldersHandler<'a, 'b> { + key: Key, + app: &'a mut App<'b>, + active_lidarr_block: ActiveLidarrBlock, + _context: Option, +} + +impl RootFoldersHandler<'_, '_> { + fn build_add_root_folder_body(&mut self) -> AddRootFolderBody { + let root_folder = self + .app + .data + .lidarr_data + .edit_root_folder + .take() + .expect("EditRootFolder is None") + .text; + AddRootFolderBody { path: root_folder } + } + + fn extract_root_folder_id(&self) -> i64 { + self + .app + .data + .lidarr_data + .root_folders + .current_selection() + .id + } +} + +impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for RootFoldersHandler<'a, 'b> { + fn handle(&mut self) { + let root_folders_table_handling_config = + TableHandlingConfig::new(ActiveLidarrBlock::RootFolders.into()); + + if !handle_table( + self, + |app| &mut app.data.lidarr_data.root_folders, + root_folders_table_handling_config, + ) { + self.handle_key_event(); + } + } + + fn accepts(active_block: ActiveLidarrBlock) -> bool { + ROOT_FOLDERS_BLOCKS.contains(&active_block) + } + + fn new( + key: Key, + app: &'a mut App<'b>, + active_block: ActiveLidarrBlock, + _context: Option, + ) -> RootFoldersHandler<'a, 'b> { + RootFoldersHandler { + key, + app, + active_lidarr_block: active_block, + _context, + } + } + + fn get_key(&self) -> Key { + self.key + } + + fn ignore_special_keys(&self) -> bool { + self.app.ignore_special_keys_for_textbox_input + } + + fn is_ready(&self) -> bool { + !self.app.is_loading && !self.app.data.lidarr_data.root_folders.is_empty() + } + + fn handle_scroll_up(&mut self) {} + + fn handle_scroll_down(&mut self) {} + + fn handle_home(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::AddRootFolderPrompt { + self + .app + .data + .lidarr_data + .edit_root_folder + .as_mut() + .unwrap() + .scroll_home() + } + } + + fn handle_end(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::AddRootFolderPrompt { + self + .app + .data + .lidarr_data + .edit_root_folder + .as_mut() + .unwrap() + .reset_offset() + } + } + + fn handle_delete(&mut self) { + if self.active_lidarr_block == ActiveLidarrBlock::RootFolders { + self + .app + .push_navigation_stack(ActiveLidarrBlock::DeleteRootFolderPrompt.into()) + } + } + + fn handle_left_right_action(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::RootFolders => handle_change_tab_left_right_keys(self.app, self.key), + ActiveLidarrBlock::DeleteRootFolderPrompt => handle_prompt_toggle(self.app, self.key), + ActiveLidarrBlock::AddRootFolderPrompt => { + handle_text_box_left_right_keys!( + self, + self.key, + self.app.data.lidarr_data.edit_root_folder.as_mut().unwrap() + ) + } + _ => (), + } + } + + fn handle_submit(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::DeleteRootFolderPrompt => { + if self.app.data.lidarr_data.prompt_confirm { + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteRootFolder(self.extract_root_folder_id())); + } + + self.app.pop_navigation_stack(); + } + _ if self.active_lidarr_block == ActiveLidarrBlock::AddRootFolderPrompt + && !self + .app + .data + .lidarr_data + .edit_root_folder + .as_ref() + .unwrap() + .text + .is_empty() => + { + self.app.data.lidarr_data.prompt_confirm_action = Some(LidarrEvent::AddRootFolder( + self.build_add_root_folder_body(), + )); + self.app.data.lidarr_data.prompt_confirm = true; + self.app.ignore_special_keys_for_textbox_input = false; + self.app.pop_navigation_stack(); + } + _ => (), + } + } + + fn handle_esc(&mut self) { + match self.active_lidarr_block { + ActiveLidarrBlock::AddRootFolderPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.edit_root_folder = None; + self.app.data.lidarr_data.prompt_confirm = false; + self.app.ignore_special_keys_for_textbox_input = false; + } + ActiveLidarrBlock::DeleteRootFolderPrompt => { + self.app.pop_navigation_stack(); + self.app.data.lidarr_data.prompt_confirm = false; + } + _ => handle_clear_errors(self.app), + } + } + + fn handle_char_key_event(&mut self) { + let key = self.key; + match self.active_lidarr_block { + ActiveLidarrBlock::RootFolders => match self.key { + _ if matches_key!(refresh, key) => { + self.app.should_refresh = true; + } + _ if matches_key!(add, key) => { + self + .app + .push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + self.app.data.lidarr_data.edit_root_folder = Some(HorizontallyScrollableText::default()); + self.app.ignore_special_keys_for_textbox_input = true; + } + _ => (), + }, + ActiveLidarrBlock::AddRootFolderPrompt => { + handle_text_box_keys!( + self, + key, + self.app.data.lidarr_data.edit_root_folder.as_mut().unwrap() + ) + } + ActiveLidarrBlock::DeleteRootFolderPrompt => { + if matches_key!(confirm, key) { + self.app.data.lidarr_data.prompt_confirm = true; + self.app.data.lidarr_data.prompt_confirm_action = + Some(LidarrEvent::DeleteRootFolder(self.extract_root_folder_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() + } +} diff --git a/src/handlers/lidarr_handlers/root_folders/root_folders_handler_tests.rs b/src/handlers/lidarr_handlers/root_folders/root_folders_handler_tests.rs new file mode 100644 index 0000000..2a562db --- /dev/null +++ b/src/handlers/lidarr_handlers/root_folders/root_folders_handler_tests.rs @@ -0,0 +1,746 @@ +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use rstest::rstest; + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::app::key_binding::DEFAULT_KEYBINDINGS; + use crate::assert_modal_absent; + use crate::assert_modal_present; + use crate::assert_navigation_pushed; + use crate::event::Key; + use crate::handlers::KeyEventHandler; + use crate::handlers::lidarr_handlers::root_folders::RootFoldersHandler; + use crate::models::HorizontallyScrollableText; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, ROOT_FOLDERS_BLOCKS}; + use crate::models::servarr_models::{AddRootFolderBody, RootFolder}; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::root_folder; + + mod test_handle_home_end { + use crate::models::servarr_models::RootFolder; + use pretty_assertions::assert_eq; + use std::sync::atomic::Ordering; + + use super::*; + + #[test] + fn test_add_root_folder_prompt_home_end_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.data.lidarr_data.edit_root_folder = Some("Test".into()); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.home.key, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_root_folder + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 4 + ); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.end.key, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_root_folder + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_delete { + use pretty_assertions::assert_eq; + + use super::*; + + const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key; + + #[test] + fn test_delete_root_folder_prompt() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + + RootFoldersHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::RootFolders, None).handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::DeleteRootFolderPrompt.into()); + } + + #[test] + fn test_delete_root_folder_prompt_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + + RootFoldersHandler::new(DELETE_KEY, &mut app, ActiveLidarrBlock::RootFolders, None).handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::RootFolders.into() + ); + } + } + + mod test_handle_left_right_action { + use std::sync::atomic::Ordering; + + use pretty_assertions::assert_eq; + use rstest::rstest; + + use super::*; + use crate::assert_navigation_pushed; + + #[rstest] + fn test_root_folders_tab_left(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(3); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::History.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::History.into()); + } + + #[rstest] + fn test_root_folders_tab_right(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.is_loading = is_ready; + app.data.lidarr_data.main_tabs.set_index(3); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.data.lidarr_data.main_tabs.get_active_route(), + ActiveLidarrBlock::Artists.into() + ); + assert_navigation_pushed!(app, ActiveLidarrBlock::Artists.into()); + } + + #[rstest] + fn test_left_right_delete_root_folder_prompt_toggle( + #[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key, + ) { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + + RootFoldersHandler::new( + key, + &mut app, + ActiveLidarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + + RootFoldersHandler::new( + key, + &mut app, + ActiveLidarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[test] + fn test_add_root_folder_prompt_left_right_keys() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.data.lidarr_data.edit_root_folder = Some("Test".into()); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.left.key, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_root_folder + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 1 + ); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.right.key, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_eq!( + app + .data + .lidarr_data + .edit_root_folder + .as_ref() + .unwrap() + .offset + .load(Ordering::SeqCst), + 0 + ); + } + } + + mod test_handle_submit { + use pretty_assertions::assert_eq; + + use crate::network::lidarr_network::LidarrEvent; + + use super::*; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::root_folder; + use crate::{assert_modal_absent, assert_navigation_popped}; + + const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key; + + #[test] + fn test_add_root_folder_prompt_confirm_submit() { + let mut app = App::test_default(); + let expected_add_root_folder_body = AddRootFolderBody { + path: "Test".to_owned(), + }; + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.data.lidarr_data.edit_root_folder = Some("Test".into()); + app.data.lidarr_data.prompt_confirm = true; + app.ignore_special_keys_for_textbox_input = true; + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + + RootFoldersHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert!(!app.ignore_special_keys_for_textbox_input); + assert_eq!( + app.data.lidarr_data.prompt_confirm_action, + Some(LidarrEvent::AddRootFolder(expected_add_root_folder_body)) + ); + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + assert_modal_absent!(app.data.lidarr_data.edit_root_folder); + } + + #[test] + fn test_add_root_folder_prompt_confirm_submit_noop_on_empty_folder() { + let mut app = App::test_default(); + app.data.lidarr_data.edit_root_folder = Some(HorizontallyScrollableText::default()); + app.data.lidarr_data.prompt_confirm = false; + app.ignore_special_keys_for_textbox_input = true; + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + + RootFoldersHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert!(app.ignore_special_keys_for_textbox_input); + assert_modal_absent!(app.data.lidarr_data.prompt_confirm_action); + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::AddRootFolderPrompt.into() + ); + } + + #[test] + fn test_delete_root_folder_prompt_confirm_submit() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .root_folders + .set_items(vec![root_folder()]); + app.data.lidarr_data.prompt_confirm = true; + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteRootFolderPrompt.into()); + + RootFoldersHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::DeleteRootFolder(1) + ); + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + } + + #[test] + fn test_delete_root_folder_prompt_decline_submit() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteRootFolderPrompt.into()); + + RootFoldersHandler::new( + SUBMIT_KEY, + &mut app, + ActiveLidarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(!app.data.lidarr_data.prompt_confirm); + assert_none!(app.data.lidarr_data.prompt_confirm_action); + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + } + } + + mod test_handle_esc { + use super::*; + use crate::assert_navigation_popped; + use rstest::rstest; + + const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key; + + #[test] + fn test_delete_root_folder_prompt_block_esc() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteRootFolderPrompt.into()); + app.data.lidarr_data.prompt_confirm = true; + + RootFoldersHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + assert!(!app.data.lidarr_data.prompt_confirm); + } + + #[test] + fn test_add_root_folder_prompt_esc() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::AddRootFolderPrompt.into()); + app.data.lidarr_data.edit_root_folder = Some("/nfs/test".into()); + app.ignore_special_keys_for_textbox_input = true; + + RootFoldersHandler::new( + ESC_KEY, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + + assert_modal_absent!(app.data.lidarr_data.edit_root_folder); + assert!(!app.data.lidarr_data.prompt_confirm); + assert!(!app.ignore_special_keys_for_textbox_input); + } + + #[rstest] + fn test_default_esc(#[values(true, false)] is_ready: bool) { + let mut app = App::test_default(); + app.is_loading = is_ready; + app.error = "test error".to_owned().into(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + + RootFoldersHandler::new(ESC_KEY, &mut app, ActiveLidarrBlock::RootFolders, None).handle(); + + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + assert_is_empty!(app.error.text); + } + } + + mod test_handle_key_char { + use super::*; + use crate::assert_navigation_popped; + use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::root_folder; + use pretty_assertions::{assert_eq, assert_str_eq}; + + #[test] + fn test_root_folder_add() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.add.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::AddRootFolderPrompt.into()); + assert!(app.ignore_special_keys_for_textbox_input); + assert_modal_present!(app.data.lidarr_data.edit_root_folder); + } + + #[test] + fn test_root_folder_add_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.add.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::RootFolders.into() + ); + assert!(!app.ignore_special_keys_for_textbox_input); + assert_modal_absent!(app.data.lidarr_data.edit_root_folder); + } + + #[test] + fn test_refresh_root_folders_key() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ) + .handle(); + + assert_navigation_pushed!(app, ActiveLidarrBlock::RootFolders.into()); + assert!(app.should_refresh); + } + + #[test] + fn test_refresh_root_folders_key_no_op_when_not_ready() { + let mut app = App::test_default(); + app.is_loading = true; + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.refresh.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ) + .handle(); + + assert_eq!( + app.get_current_route(), + ActiveLidarrBlock::RootFolders.into() + ); + assert!(!app.should_refresh); + } + + #[test] + fn test_add_root_folder_prompt_backspace_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.data.lidarr_data.edit_root_folder = Some("/nfs/test".into()); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.backspace.key, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.edit_root_folder.as_ref().unwrap().text, + "/nfs/tes" + ); + } + + #[test] + fn test_add_root_folder_prompt_char_key() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + app.data.lidarr_data.edit_root_folder = Some(HorizontallyScrollableText::default()); + + RootFoldersHandler::new( + Key::Char('a'), + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ) + .handle(); + + assert_str_eq!( + app.data.lidarr_data.edit_root_folder.as_ref().unwrap().text, + "a" + ); + } + + #[test] + fn test_delete_root_folder_prompt_confirm() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .root_folders + .set_items(vec![root_folder()]); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.push_navigation_stack(ActiveLidarrBlock::DeleteRootFolderPrompt.into()); + + RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.confirm.key, + &mut app, + ActiveLidarrBlock::DeleteRootFolderPrompt, + None, + ) + .handle(); + + assert!(app.data.lidarr_data.prompt_confirm); + assert_some_eq_x!( + &app.data.lidarr_data.prompt_confirm_action, + &LidarrEvent::DeleteRootFolder(1) + ); + assert_navigation_popped!(app, ActiveLidarrBlock::RootFolders.into()); + } + } + + #[test] + fn test_root_folders_handler_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if ROOT_FOLDERS_BLOCKS.contains(&active_lidarr_block) { + assert!(RootFoldersHandler::accepts(active_lidarr_block)); + } else { + assert!(!RootFoldersHandler::accepts(active_lidarr_block)); + } + }) + } + + #[rstest] + fn test_root_folders_handler_ignore_special_keys( + #[values(true, false)] ignore_special_keys_for_textbox_input: bool, + ) { + let mut app = App::test_default(); + app.ignore_special_keys_for_textbox_input = ignore_special_keys_for_textbox_input; + let handler = RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::default(), + None, + ); + + assert_eq!( + handler.ignore_special_keys(), + ignore_special_keys_for_textbox_input + ); + } + + #[test] + fn test_extract_root_folder_id() { + let mut app = App::test_default(); + app + .data + .lidarr_data + .root_folders + .set_items(vec![root_folder()]); + + let root_folder_id = RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::DeleteRootFolderPrompt, + None, + ) + .extract_root_folder_id(); + + assert_eq!(root_folder_id, 1); + } + + #[test] + fn test_build_add_root_folder_body() { + let mut app = App::test_default(); + app.data.lidarr_data.edit_root_folder = Some("/nfs/test".into()); + let expected_add_root_folder_body = AddRootFolderBody { + path: "/nfs/test".to_owned(), + }; + + let root_folder = RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::AddRootFolderPrompt, + None, + ) + .build_add_root_folder_body(); + + assert_eq!(root_folder, expected_add_root_folder_body); + assert_modal_absent!(app.data.lidarr_data.edit_root_folder); + } + + #[test] + fn test_root_folders_handler_not_ready_when_loading() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.is_loading = true; + + let handler = RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_root_folders_handler_not_ready_when_root_folders_is_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.is_loading = false; + + let handler = RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ); + + assert!(!handler.is_ready()); + } + + #[test] + fn test_root_folders_handler_ready_when_not_loading_and_root_folders_is_not_empty() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + app.is_loading = false; + + app + .data + .lidarr_data + .root_folders + .set_items(vec![RootFolder::default()]); + let handler = RootFoldersHandler::new( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveLidarrBlock::RootFolders, + None, + ); + + assert!(handler.is_ready()); + } +} diff --git a/src/models/servarr_data/lidarr/lidarr_data.rs b/src/models/servarr_data/lidarr/lidarr_data.rs index ef8edea..c96eff6 100644 --- a/src/models/servarr_data/lidarr/lidarr_data.rs +++ b/src/models/servarr_data/lidarr/lidarr_data.rs @@ -1,7 +1,9 @@ use serde_json::Number; use super::modals::{AddArtistModal, EditArtistModal}; -use crate::app::context_clues::{DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES}; +use crate::app::context_clues::{ + DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, +}; use crate::app::lidarr::lidarr_context_clues::{ ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, }; @@ -45,6 +47,7 @@ pub struct LidarrData<'a> { pub disk_space_vec: Vec, pub downloads: StatefulTable, pub edit_artist_modal: Option, + pub edit_root_folder: Option, pub history: StatefulTable, pub main_tabs: TabState, pub metadata_profile_map: BiMap, @@ -114,6 +117,7 @@ impl<'a> Default for LidarrData<'a> { disk_space_vec: Vec::new(), downloads: StatefulTable::default(), edit_artist_modal: None, + edit_root_folder: None, history: StatefulTable::default(), metadata_profile_map: BiMap::new(), prompt_confirm: false, @@ -143,6 +147,12 @@ impl<'a> Default for LidarrData<'a> { contextual_help: Some(&HISTORY_CONTEXT_CLUES), config: None, }, + TabRoute { + title: "Root Folders".to_string(), + route: ActiveLidarrBlock::RootFolders.into(), + contextual_help: Some(&ROOT_FOLDERS_CONTEXT_CLUES), + config: None, + }, ]), artist_info_tabs: TabState::new(vec![TabRoute { title: "Albums".to_string(), @@ -198,6 +208,7 @@ impl LidarrData<'_> { quality_profile_map: quality_profile_map(), metadata_profile_map: metadata_profile_map(), edit_artist_modal: Some(edit_artist_modal), + edit_root_folder: Some("/nfs".into()), add_artist_modal: Some(add_artist_modal), tags_map: tags_map(), ..LidarrData::default() @@ -249,6 +260,7 @@ pub enum ActiveLidarrBlock { AddArtistSelectQualityProfile, AddArtistSelectRootFolder, AddArtistTagsInput, + AddRootFolderPrompt, AutomaticallySearchArtistPrompt, DeleteAlbumPrompt, DeleteAlbumConfirmPrompt, @@ -259,6 +271,7 @@ pub enum ActiveLidarrBlock { DeleteArtistToggleDeleteFile, DeleteArtistToggleAddListExclusion, DeleteDownloadPrompt, + DeleteRootFolderPrompt, Downloads, EditArtistPrompt, EditArtistConfirmPrompt, @@ -275,6 +288,7 @@ pub enum ActiveLidarrBlock { History, HistoryItemDetails, HistorySortPrompt, + RootFolders, SearchAlbums, SearchAlbumsError, SearchArtists, @@ -392,6 +406,12 @@ pub const EDIT_ARTIST_SELECTION_BLOCKS: &[&[ActiveLidarrBlock]] = &[ &[ActiveLidarrBlock::EditArtistConfirmPrompt], ]; +pub const ROOT_FOLDERS_BLOCKS: [ActiveLidarrBlock; 3] = [ + ActiveLidarrBlock::RootFolders, + ActiveLidarrBlock::AddRootFolderPrompt, + ActiveLidarrBlock::DeleteRootFolderPrompt, +]; + impl From for Route { fn from(active_lidarr_block: ActiveLidarrBlock) -> Route { Route::Lidarr(active_lidarr_block, None) diff --git a/src/models/servarr_data/lidarr/lidarr_data_tests.rs b/src/models/servarr_data/lidarr/lidarr_data_tests.rs index 03611ad..ddcba92 100644 --- a/src/models/servarr_data/lidarr/lidarr_data_tests.rs +++ b/src/models/servarr_data/lidarr/lidarr_data_tests.rs @@ -1,6 +1,8 @@ #[cfg(test)] mod tests { - use crate::app::context_clues::{DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES}; + use crate::app::context_clues::{ + DOWNLOADS_CONTEXT_CLUES, HISTORY_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES, + }; use crate::app::lidarr::lidarr_context_clues::{ ARTIST_DETAILS_CONTEXT_CLUES, ARTISTS_CONTEXT_CLUES, }; @@ -9,6 +11,7 @@ mod tests { ADD_ARTIST_BLOCKS, ADD_ARTIST_SELECTION_BLOCKS, ARTIST_DETAILS_BLOCKS, DELETE_ALBUM_BLOCKS, DELETE_ALBUM_SELECTION_BLOCKS, DELETE_ARTIST_BLOCKS, DELETE_ARTIST_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_ARTIST_BLOCKS, EDIT_ARTIST_SELECTION_BLOCKS, HISTORY_BLOCKS, + ROOT_FOLDERS_BLOCKS, }; use crate::models::{ BlockSelectionState, Route, @@ -135,6 +138,7 @@ mod tests { assert_is_empty!(lidarr_data.disk_space_vec); assert_is_empty!(lidarr_data.downloads); assert_none!(lidarr_data.edit_artist_modal); + assert_none!(lidarr_data.edit_root_folder); assert_is_empty!(lidarr_data.history); assert_is_empty!(lidarr_data.metadata_profile_map); assert!(!lidarr_data.prompt_confirm); @@ -146,7 +150,7 @@ mod tests { assert_is_empty!(lidarr_data.tags_map); assert_is_empty!(lidarr_data.version); - assert_eq!(lidarr_data.main_tabs.tabs.len(), 3); + assert_eq!(lidarr_data.main_tabs.tabs.len(), 4); assert_str_eq!(lidarr_data.main_tabs.tabs[0].title, "Library"); assert_eq!( @@ -181,6 +185,17 @@ mod tests { ); assert_none!(lidarr_data.main_tabs.tabs[2].config); + assert_str_eq!(lidarr_data.main_tabs.tabs[3].title, "Root Folders"); + assert_eq!( + lidarr_data.main_tabs.tabs[3].route, + ActiveLidarrBlock::RootFolders.into() + ); + assert_some_eq_x!( + &lidarr_data.main_tabs.tabs[3].contextual_help, + &ROOT_FOLDERS_CONTEXT_CLUES + ); + assert_none!(lidarr_data.main_tabs.tabs[3].config); + assert_eq!(lidarr_data.artist_info_tabs.tabs.len(), 1); assert_str_eq!(lidarr_data.artist_info_tabs.tabs[0].title, "Albums"); assert_eq!( @@ -391,4 +406,12 @@ mod tests { ); assert_none!(edit_artist_block_iter.next()); } + + #[test] + fn test_root_folders_blocks_contents() { + assert_eq!(ROOT_FOLDERS_BLOCKS.len(), 3); + assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveLidarrBlock::RootFolders)); + assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveLidarrBlock::AddRootFolderPrompt)); + assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveLidarrBlock::DeleteRootFolderPrompt)); + } } diff --git a/src/network/lidarr_network/lidarr_network_tests.rs b/src/network/lidarr_network/lidarr_network_tests.rs index 2ca52e9..d8c6ff6 100644 --- a/src/network/lidarr_network/lidarr_network_tests.rs +++ b/src/network/lidarr_network/lidarr_network_tests.rs @@ -87,11 +87,22 @@ mod tests { assert_str_eq!(event.resource(), "/album"); } + #[rstest] + fn test_resource_root_folder( + #[values( + LidarrEvent::GetRootFolders, + LidarrEvent::DeleteRootFolder(0), + LidarrEvent::AddRootFolder(Default::default()) + )] + event: LidarrEvent, + ) { + assert_str_eq!(event.resource(), "/rootfolder"); + } + #[rstest] #[case(LidarrEvent::GetDiskSpace, "/diskspace")] #[case(LidarrEvent::GetMetadataProfiles, "/metadataprofile")] #[case(LidarrEvent::GetQualityProfiles, "/qualityprofile")] - #[case(LidarrEvent::GetRootFolders, "/rootfolder")] #[case(LidarrEvent::GetStatus, "/system/status")] #[case(LidarrEvent::GetTags, "/tag")] #[case(LidarrEvent::HealthCheck, "/health")] diff --git a/src/network/lidarr_network/mod.rs b/src/network/lidarr_network/mod.rs index dbef5e4..985344d 100644 --- a/src/network/lidarr_network/mod.rs +++ b/src/network/lidarr_network/mod.rs @@ -5,7 +5,7 @@ use super::{NetworkEvent, NetworkResource}; use crate::models::lidarr_models::{ AddArtistBody, DeleteParams, EditArtistParams, LidarrSerdeable, MetadataProfile, }; -use crate::models::servarr_models::{QualityProfile, Tag}; +use crate::models::servarr_models::{AddRootFolderBody, QualityProfile, Tag}; use crate::network::{Network, RequestMethod}; mod downloads; @@ -25,10 +25,12 @@ pub mod lidarr_network_test_utils; #[derive(Debug, Eq, PartialEq, Clone)] pub enum LidarrEvent { AddArtist(AddArtistBody), + AddRootFolder(AddRootFolderBody), AddTag(String), DeleteAlbum(DeleteParams), DeleteArtist(DeleteParams), DeleteDownload(i64), + DeleteRootFolder(i64), DeleteTag(i64), EditArtist(EditArtistParams), GetAlbums(i64), @@ -81,7 +83,9 @@ impl NetworkResource for LidarrEvent { | LidarrEvent::UpdateDownloads => "/command", LidarrEvent::GetMetadataProfiles => "/metadataprofile", LidarrEvent::GetQualityProfiles => "/qualityprofile", - LidarrEvent::GetRootFolders => "/rootfolder", + LidarrEvent::GetRootFolders + | LidarrEvent::AddRootFolder(_) + | LidarrEvent::DeleteRootFolder(_) => "/rootfolder", LidarrEvent::GetStatus => "/system/status", LidarrEvent::HealthCheck => "/health", LidarrEvent::SearchNewArtist(_) => "/artist/lookup", @@ -102,6 +106,10 @@ impl Network<'_, '_> { ) -> Result { match lidarr_event { LidarrEvent::AddTag(tag) => self.add_lidarr_tag(tag).await.map(LidarrSerdeable::from), + LidarrEvent::AddRootFolder(path) => self + .add_lidarr_root_folder(path) + .await + .map(LidarrSerdeable::from), LidarrEvent::DeleteAlbum(params) => { self.delete_album(params).await.map(LidarrSerdeable::from) } @@ -112,6 +120,10 @@ impl Network<'_, '_> { .delete_lidarr_download(download_id) .await .map(LidarrSerdeable::from), + LidarrEvent::DeleteRootFolder(root_folder_id) => self + .delete_lidarr_root_folder(root_folder_id) + .await + .map(LidarrSerdeable::from), LidarrEvent::DeleteTag(tag_id) => self .delete_lidarr_tag(tag_id) .await diff --git a/src/network/lidarr_network/root_folders/lidarr_root_folders_network_tests.rs b/src/network/lidarr_network/root_folders/lidarr_root_folders_network_tests.rs index f8bfaf5..5fae7a9 100644 --- a/src/network/lidarr_network/root_folders/lidarr_root_folders_network_tests.rs +++ b/src/network/lidarr_network/root_folders/lidarr_root_folders_network_tests.rs @@ -1,19 +1,67 @@ #[cfg(test)] mod tests { use crate::models::lidarr_models::LidarrSerdeable; - use crate::models::servarr_models::RootFolder; + use crate::models::servarr_models::{AddRootFolderBody, RootFolder}; use crate::network::lidarr_network::LidarrEvent; + use crate::network::lidarr_network::lidarr_network_test_utils::test_utils::root_folder; use crate::network::network_tests::test_utils::{MockServarrApi, test_network}; use pretty_assertions::assert_eq; use serde_json::json; + #[tokio::test] + async fn test_handle_add_lidarr_root_folder_event() { + let expected_add_root_folder_body = AddRootFolderBody { + path: "/nfs/test".to_owned(), + }; + let (mock, app, _server) = MockServarrApi::post() + .with_request_body(json!({ + "path": "/nfs/test" + })) + .returns(json!({})) + .build_for(LidarrEvent::AddRootFolder( + expected_add_root_folder_body.clone(), + )) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::AddRootFolder(expected_add_root_folder_body)) + .await + .is_ok() + ); + + mock.assert_async().await; + assert!(app.lock().await.data.lidarr_data.edit_root_folder.is_none()); + } + + #[tokio::test] + async fn test_handle_delete_lidarr_root_folder_event() { + let (mock, app, _server) = MockServarrApi::delete() + .path("/1") + .build_for(LidarrEvent::DeleteRootFolder(1)) + .await; + app.lock().await.server_tabs.set_index(2); + let mut network = test_network(&app); + + assert!( + network + .handle_lidarr_event(LidarrEvent::DeleteRootFolder(1)) + .await + .is_ok() + ); + + mock.assert_async().await; + } + #[tokio::test] async fn test_handle_get_root_folders_event() { let root_folders_json = json!([{ "id": 1, - "path": "/music", + "path": "/nfs", "accessible": true, - "freeSpace": 50000000000i64 + "freeSpace": 219902325555200i64 }]); let response: Vec = serde_json::from_value(root_folders_json.clone()).unwrap(); let (mock, app, _server) = MockServarrApi::get() @@ -34,6 +82,9 @@ mod tests { }; assert_eq!(root_folders, response); - assert!(!app.lock().await.data.lidarr_data.root_folders.is_empty()); + assert_eq!( + app.lock().await.data.lidarr_data.root_folders.items, + vec![root_folder()] + ); } } diff --git a/src/network/lidarr_network/root_folders/mod.rs b/src/network/lidarr_network/root_folders/mod.rs index 274c24c..4c7d08a 100644 --- a/src/network/lidarr_network/root_folders/mod.rs +++ b/src/network/lidarr_network/root_folders/mod.rs @@ -1,15 +1,61 @@ -use anyhow::Result; -use log::info; - -use crate::models::servarr_models::RootFolder; +use crate::models::servarr_models::{AddRootFolderBody, RootFolder}; use crate::network::lidarr_network::LidarrEvent; use crate::network::{Network, RequestMethod}; +use anyhow::Result; +use log::{debug, info}; +use serde_json::Value; #[cfg(test)] #[path = "lidarr_root_folders_network_tests.rs"] mod lidarr_root_folders_network_tests; impl Network<'_, '_> { + pub(in crate::network::lidarr_network) async fn add_lidarr_root_folder( + &mut self, + add_root_folder_body: AddRootFolderBody, + ) -> Result { + info!("Adding new root folder to Lidarr"); + let event = LidarrEvent::AddRootFolder(AddRootFolderBody::default()); + + debug!("Add root folder body: {add_root_folder_body:?}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Post, + Some(add_root_folder_body), + None, + None, + ) + .await; + + self + .handle_request::(request_props, |_, _| ()) + .await + } + + pub(in crate::network::lidarr_network) async fn delete_lidarr_root_folder( + &mut self, + root_folder_id: i64, + ) -> Result<()> { + let event = LidarrEvent::DeleteRootFolder(root_folder_id); + info!("Deleting Lidarr root folder for folder with id: {root_folder_id}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Delete, + None::<()>, + Some(format!("/{root_folder_id}")), + None, + ) + .await; + + self + .handle_request::<(), ()>(request_props, |_, _| ()) + .await + } + pub(in crate::network::lidarr_network) async fn get_lidarr_root_folders( &mut self, ) -> Result> { diff --git a/src/ui/lidarr_ui/lidarr_ui_tests.rs b/src/ui/lidarr_ui/lidarr_ui_tests.rs index 62a8663..a5c98d4 100644 --- a/src/ui/lidarr_ui/lidarr_ui_tests.rs +++ b/src/ui/lidarr_ui/lidarr_ui_tests.rs @@ -24,6 +24,7 @@ mod tests { #[case(ActiveLidarrBlock::Artists, 0)] #[case(ActiveLidarrBlock::Downloads, 1)] #[case(ActiveLidarrBlock::History, 2)] + #[case(ActiveLidarrBlock::RootFolders, 3)] fn test_lidarr_ui_renders_lidarr_tabs( #[case] active_lidarr_block: ActiveLidarrBlock, #[case] index: usize, diff --git a/src/ui/lidarr_ui/mod.rs b/src/ui/lidarr_ui/mod.rs index 112db62..243631a 100644 --- a/src/ui/lidarr_ui/mod.rs +++ b/src/ui/lidarr_ui/mod.rs @@ -24,6 +24,7 @@ use super::{ widgets::loading_block::LoadingBlock, }; use crate::ui::lidarr_ui::downloads::DownloadsUi; +use crate::ui::lidarr_ui::root_folders::RootFoldersUi; use crate::{ app::App, logos::LIDARR_LOGO, @@ -44,6 +45,7 @@ mod lidarr_ui_utils; #[cfg(test)] #[path = "lidarr_ui_tests.rs"] mod lidarr_ui_tests; +mod root_folders; pub(super) struct LidarrUi; @@ -60,6 +62,7 @@ impl DrawUi for LidarrUi { _ if LibraryUi::accepts(route) => LibraryUi::draw(f, app, content_area), _ if DownloadsUi::accepts(route) => DownloadsUi::draw(f, app, content_area), _ if HistoryUi::accepts(route) => HistoryUi::draw(f, app, content_area), + _ if RootFoldersUi::accepts(route) => RootFoldersUi::draw(f, app, content_area), _ => (), } } diff --git a/src/ui/lidarr_ui/root_folders/mod.rs b/src/ui/lidarr_ui/root_folders/mod.rs new file mode 100644 index 0000000..518d14a --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/mod.rs @@ -0,0 +1,109 @@ +use ratatui::Frame; +use ratatui::layout::{Constraint, Rect}; +use ratatui::widgets::{Cell, Row}; + +use crate::app::App; +use crate::models::Route; +use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, ROOT_FOLDERS_BLOCKS}; +use crate::models::servarr_models::RootFolder; +use crate::ui::styles::ManagarrStyle; +use crate::ui::utils::layout_block_top_border; +use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt; +use crate::ui::widgets::managarr_table::ManagarrTable; +use crate::ui::widgets::popup::{Popup, Size}; +use crate::ui::{DrawUi, draw_input_box_popup, draw_popup}; +use crate::utils::convert_to_gb; + +#[cfg(test)] +#[path = "root_folders_ui_tests.rs"] +mod root_folders_ui_tests; + +pub(super) struct RootFoldersUi; + +impl DrawUi for RootFoldersUi { + fn accepts(route: Route) -> bool { + if let Route::Lidarr(active_lidarr_block, _) = route { + return ROOT_FOLDERS_BLOCKS.contains(&active_lidarr_block); + } + + false + } + + fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() { + draw_root_folders(f, app, area); + + match active_lidarr_block { + ActiveLidarrBlock::AddRootFolderPrompt => { + draw_popup(f, app, draw_add_root_folder_prompt_box, Size::InputBox) + } + ActiveLidarrBlock::DeleteRootFolderPrompt => { + let prompt = format!( + "Do you really want to delete this root folder: \n{}?", + app.data.lidarr_data.root_folders.current_selection().path + ); + let confirmation_prompt = ConfirmationPrompt::new() + .title("Delete Root Folder") + .prompt(&prompt) + .yes_no_value(app.data.lidarr_data.prompt_confirm); + + f.render_widget( + Popup::new(confirmation_prompt).size(Size::MediumPrompt), + f.area(), + ); + } + _ => (), + } + } + } +} + +fn draw_root_folders(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + let root_folders_row_mapping = |root_folders: &RootFolder| { + let RootFolder { + path, + free_space, + unmapped_folders, + .. + } = root_folders; + + let space: f64 = convert_to_gb(*free_space); + + Row::new(vec![ + Cell::from(path.to_owned()), + Cell::from(format!("{space:.2} GB")), + Cell::from( + unmapped_folders + .as_ref() + .unwrap_or(&Vec::new()) + .len() + .to_string(), + ), + ]) + .primary() + }; + + let root_folders_table = ManagarrTable::new( + Some(&mut app.data.lidarr_data.root_folders), + root_folders_row_mapping, + ) + .block(layout_block_top_border()) + .loading(app.is_loading) + .headers(["Path", "Free Space", "Unmapped Folders"]) + .constraints([ + Constraint::Ratio(3, 5), + Constraint::Ratio(1, 5), + Constraint::Ratio(1, 5), + ]); + + f.render_widget(root_folders_table, area); +} + +fn draw_add_root_folder_prompt_box(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { + draw_input_box_popup( + f, + area, + "Add Root Folder", + app.data.lidarr_data.edit_root_folder.as_ref().unwrap(), + ); +} diff --git a/src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs b/src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs new file mode 100644 index 0000000..6d5d1ac --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs @@ -0,0 +1,72 @@ +#[cfg(test)] +mod tests { + use strum::IntoEnumIterator; + + use crate::app::App; + use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, ROOT_FOLDERS_BLOCKS}; + use crate::ui::DrawUi; + use crate::ui::lidarr_ui::root_folders::RootFoldersUi; + use crate::ui::ui_test_utils::test_utils::render_to_string_with_app; + + #[test] + fn test_root_folders_ui_accepts() { + ActiveLidarrBlock::iter().for_each(|active_lidarr_block| { + if ROOT_FOLDERS_BLOCKS.contains(&active_lidarr_block) { + assert!(RootFoldersUi::accepts(active_lidarr_block.into())); + } else { + assert!(!RootFoldersUi::accepts(active_lidarr_block.into())); + } + }); + } + + mod snapshot_tests { + use crate::ui::ui_test_utils::test_utils::TerminalSize; + use rstest::rstest; + + use super::*; + + #[test] + fn test_root_folders_ui_renders_loading() { + let mut app = App::test_default(); + app.is_loading = true; + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + RootFoldersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[test] + fn test_root_folders_ui_renders_empty_root_folders() { + let mut app = App::test_default(); + app.push_navigation_stack(ActiveLidarrBlock::RootFolders.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + RootFoldersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(output); + } + + #[rstest] + fn test_root_folders_ui_renders_root_folders_tab( + #[values( + ActiveLidarrBlock::RootFolders, + ActiveLidarrBlock::AddRootFolderPrompt, + ActiveLidarrBlock::DeleteRootFolderPrompt + )] + active_lidarr_block: ActiveLidarrBlock, + ) { + let mut app = App::test_default_fully_populated(); + app.push_navigation_stack(active_lidarr_block.into()); + + let output = render_to_string_with_app(TerminalSize::Large, &mut app, |f, app| { + RootFoldersUi::draw(f, app, f.area()); + }); + + insta::assert_snapshot!(active_lidarr_block.to_string(), output); + } + } +} diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderPrompt.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderPrompt.snap new file mode 100644 index 0000000..043bf8a --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__AddRootFolderPrompt.snap @@ -0,0 +1,31 @@ +--- +source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Path Free Space Unmapped Folders +=> /nfs 204800.00 GB 0 + + + + + + + + + + + + + + + + + + + + + ╭───────────── Add Root Folder ─────────────╮ + │/nfs │ + ╰─────────────────────────────────────────────╯ + cancel diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__DeleteRootFolderPrompt.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__DeleteRootFolderPrompt.snap new file mode 100644 index 0000000..65b7259 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__DeleteRootFolderPrompt.snap @@ -0,0 +1,38 @@ +--- +source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Path Free Space Unmapped Folders +=> /nfs 204800.00 GB 0 + + + + + + + + + + + + + + ╭────────────────── Delete Root Folder ───────────────────╮ + │ Do you really want to delete this root folder: │ + │ /nfs? │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │╭────────────────────────────╮╭───────────────────────────╮│ + ││ Yes ││ No ││ + │╰────────────────────────────╯╰───────────────────────────╯│ + ╰───────────────────────────────────────────────────────────╯ diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__RootFolders.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__RootFolders.snap new file mode 100644 index 0000000..4e2666f --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__RootFolders.snap @@ -0,0 +1,7 @@ +--- +source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Path Free Space Unmapped Folders +=> /nfs 204800.00 GB 0 diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__root_folders_ui_renders_empty_root_folders.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__root_folders_ui_renders_empty_root_folders.snap new file mode 100644 index 0000000..5d3f79d --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__root_folders_ui_renders_empty_root_folders.snap @@ -0,0 +1,5 @@ +--- +source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── diff --git a/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__root_folders_ui_renders_loading.snap b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__root_folders_ui_renders_loading.snap new file mode 100644 index 0000000..fca4f13 --- /dev/null +++ b/src/ui/lidarr_ui/root_folders/snapshots/managarr__ui__lidarr_ui__root_folders__root_folders_ui_tests__tests__snapshot_tests__root_folders_ui_renders_loading.snap @@ -0,0 +1,8 @@ +--- +source: src/ui/lidarr_ui/root_folders/root_folders_ui_tests.rs +expression: output +--- +───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + Loading ... diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap index 7fc3b4c..5fde7c7 100644 --- a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Artists.snap @@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs expression: output --- ╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Downloads │ History │ +│ Library │ Downloads │ History │ Root Folders │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Name ▼ Type Status Quality Profile Metadata Profile Albums Tracks Size Monitored Tags │ │=> Alex Person Continuing Lossless Standard 1 15/15 0.00 GB 🏷 alex │ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap index 8e14902..91fd750 100644 --- a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_Downloads.snap @@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs expression: output --- ╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Downloads │ History │ +│ Library │ Downloads │ History │ Root Folders │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Title Percent Complete Size Output Path Indexer Download Client │ │=> Test download title 50% 3.30 GB /nfs/music/alex/album kickass torrents transmission │ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap index 50a3497..6975d78 100644 --- a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_History.snap @@ -3,7 +3,7 @@ source: src/ui/lidarr_ui/lidarr_ui_tests.rs expression: output --- ╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Library │ Downloads │ History │ +│ Library │ Downloads │ History │ Root Folders │ │───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ │ Source Title ▼ Event Type Quality Date │ │=> Test source title grabbed Lossless 2023-01-01 00:00:00 UTC │ diff --git a/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_RootFolders.snap b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_RootFolders.snap new file mode 100644 index 0000000..4a8698b --- /dev/null +++ b/src/ui/lidarr_ui/snapshots/managarr__ui__lidarr_ui__lidarr_ui_tests__tests__snapshot_tests__lidarr_tabs_RootFolders.snap @@ -0,0 +1,54 @@ +--- +source: src/ui/lidarr_ui/lidarr_ui_tests.rs +expression: output +--- +╭ Artists ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ Library │ Downloads │ History │ Root Folders │ +│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│ +│ Path Free Space Unmapped Folders │ +│=> /nfs 204800.00 GB 0 │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯